一、基本数据准备
-
1、数据表的创建
mysql-- ---------------- -- 库存表 -- ---------------- DROP TABLE IF EXISTS `inventory`; CREATE TABLE `inventory` ( `id` int NOT NULL AUTO_INCREMENT primary key COMMENT '主键id', `goods_id` int(11) default 1 comment '商品id', `stocks` int(11) default 1 comment '商品库存', `version` int(11) default 0 comment '商品版本号', `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted_at` timestamp NULL DEFAULT NULL COMMENT '软删除时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存表'; -
2、根据实体类创建数据模型
-
3、手动在数据库中插入商品库存数据
-
4、创建一个基本的连接
gorm的方法gopackage utils import ( "fmt" _ "github.com/go-sql-driver/mysql" "gorm.io/driver/mysql" "gorm.io/gorm" "gorm.io/gorm/logger" "gorm.io/gorm/schema" ) var GormDb *gorm.DB func init() { var err error sqlStr := "root:123456@tcp(localhost:3306)/gorm_demo?charset=utf8mb4&parseTime=true&loc=Local" GormDb, err = gorm.Open(mysql.Open(sqlStr), &gorm.Config{ Logger: logger.Default.LogMode(logger.Info), //DisableForeignKeyConstraintWhenMigrating: true, // 禁止创建外键 NamingStrategy: schema.NamingStrategy{ SingularTable: true, // 全部的表名前面加前缀 //TablePrefix: "mall_", }, }) if err != nil { fmt.Println("数据库连接错误", err) return } }
二、模拟并发下单
-
1、模拟并发下单扣减库存
gotype InventoryDto struct { GoodsID int64 `json:"goodsId"` // 商品id Num int64 `json:"num"` // 下单数量 } func Sell(ws *sync.WaitGroup) { reqList := make([]InventoryDto, 0) reqList = append(reqList, InventoryDto{ GoodsID: 1, Num: 1, }) defer ws.Done() tx := utils.GormDb.Begin() for _, item := range reqList { // 扣减库存 inventoryEntity := model.InventoryEntity{} if err := tx.Where("goods_id = ?", item.GoodsID).First(&inventoryEntity).Error; err != nil { fmt.Println("查询错误") tx.Rollback() return } // 库存减少 stocks := inventoryEntity.Stocks - item.Num if stocks < 0 { fmt.Println("库存不足..") tx.Rollback() return } fmt.Println("开始扣减库存...") if err := tx.Model(&model.InventoryEntity{}).Where("goods_id = ?", item.GoodsID).UpdateColumn("stocks", stocks).Error; err != nil { fmt.Println("扣减库存失败", err) tx.Rollback() return } fmt.Println("扣减库存成功...") } tx.Commit() } func main() { // 开始模拟下单 wg := sync.WaitGroup{} for i := 0; i < 20; i++ { wg.Add(1) go Sell(&wg) } wg.Wait() } -
2、查看数据库库存信息,执行了20个并发,但是实际没有扣减20个库存
三、使用加锁的方式来实现并发安全
-
1、使用
go的锁的机制来实现并发安全gopackage main type InventoryDto struct { GoodsID int64 `json:"goodsId"` // 商品id Num int64 `json:"num"` // 下单数量 } var m = sync.Mutex{} func Sell(ws *sync.WaitGroup) { reqList := make([]InventoryDto, 0) reqList = append(reqList, InventoryDto{ GoodsID: 1, Num: 1, }) defer ws.Done() tx := utils.GormDb.Begin() for _, item := range reqList { m.Lock() defer m.Unlock() // 扣减库存 inventoryEntity := model.InventoryEntity{} if err := tx.Where("goods_id = ?", item.GoodsID).First(&inventoryEntity).Error; err != nil { fmt.Println("查询错误") tx.Rollback() return } // 库存减少 stocks := inventoryEntity.Stocks - item.Num if stocks < 0 { fmt.Println("库存不足..") tx.Rollback() return } fmt.Println("开始扣减库存...") if err := tx.Model(&model.InventoryEntity{}).Where("goods_id = ?", item.GoodsID).UpdateColumn("stocks", stocks).Error; err != nil { fmt.Println("扣减库存失败", err) tx.Rollback() return } fmt.Println("扣减库存成功...") } tx.Commit() } func main() { // 开始模拟下单 wg := sync.WaitGroup{} for i := 0; i < 20; i++ { wg.Add(1) go Sell(&wg) } wg.Wait() } -
2、查看数据库库存这次是每次减少20个了
四、使用mysql的悲观锁来实现并发安全
-
1、使用行锁,类似这样的
mysqlSELECT * FROM user WHERE id = 1 FOR UPDATE; -
2、在
gorm中使用行锁gopackage main type InventoryDto struct { GoodsID int64 `json:"goodsId"` // 商品id Num int64 `json:"num"` // 下单数量 } func Sell(ws *sync.WaitGroup) { reqList := make([]InventoryDto, 0) reqList = append(reqList, InventoryDto{ GoodsID: 1, Num: 1, }) defer ws.Done() tx := utils.GormDb.Begin() for _, item := range reqList { // 扣减库存 inventoryEntity := model.InventoryEntity{} if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("goods_id = ?", item.GoodsID).First(&inventoryEntity).Error; err != nil { fmt.Println("查询错误") tx.Rollback() return } // 库存减少 stocks := inventoryEntity.Stocks - item.Num if stocks < 0 { fmt.Println("库存不足..") tx.Rollback() return } fmt.Println("开始扣减库存...") if err := tx.Model(&model.InventoryEntity{}).Where("goods_id = ?", item.GoodsID).UpdateColumn("stocks", stocks).Error; err != nil { fmt.Println("扣减库存失败", err) tx.Rollback() return } fmt.Println("扣减库存成功...") } tx.Commit() } func main() { // 开始模拟下单 wg := sync.WaitGroup{} for i := 0; i < 20; i++ { wg.Add(1) go Sell(&wg) } wg.Wait() } -
3、使用悲观锁的另外一种实现方式
gopackage main type InventoryDto struct { GoodsID int64 `json:"goodsId"` // 商品id Num int64 `json:"num"` // 下单数量 } func Sell(ws *sync.WaitGroup) { reqList := make([]InventoryDto, 0) reqList = append(reqList, InventoryDto{ GoodsID: 1, Num: 1, }) defer ws.Done() tx := utils.GormDb.Begin() for _, item := range reqList { fmt.Println("开始扣减库存...") result := tx.Model(&model.InventoryEntity{}). Where("goods_id = ? and stocks >= ?", item.GoodsID, item.Num). Update("stocks", gorm.Expr("stocks - ?", item.Num)) if result.Error != nil { fmt.Println("扣减库存失败", result.Error) tx.Rollback() return } fmt.Println("扣减库存成功...") } tx.Commit() } func main() { // 开始模拟下单 wg := sync.WaitGroup{} for i := 0; i < 20; i++ { wg.Add(1) go Sell(&wg) } wg.Wait() }
五、使用mysql的乐观锁来实现
-
1、官方插件
optimisticlock -
2、修改数据库模型
version的数据类型goconst TableNameInventoryEntity = "inventory" // InventoryEntity 库存表 type InventoryEntity struct { ID int64 `gorm:"column:id;type:int;primaryKey;autoIncrement:true;comment:主键id" json:"id,string"` // 主键id GoodsID int64 `gorm:"column:goods_id;type:int;default:1;comment:商品id" json:"goodsId"` // 商品id Stocks int64 `gorm:"column:stocks;type:int;default:1;comment:商品库存" json:"stocks"` // 商品库存 Version optimisticlock.Version `gorm:"column:version;type:int;comment:商品版本号" json:"version"` // 商品版本号 CreatedAt LocalTime `gorm:"column:created_at;comment:创建时间" json:"createdAt"` // 创建时间 UpdatedAt LocalTime `gorm:"column:updated_at;comment:更新时间" json:"updatedAt"` // 更新时间 DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp;comment:软删除时间" json:"-"` // 软删除时间 } // TableName InventoryEntity's table name func (*InventoryEntity) TableName() string { return TableNameInventoryEntity } -
3、具体乐观锁的实现
gopackage main type InventoryDto struct { GoodsID int64 `json:"goodsId"` Num int64 `json:"num"` } func Sell(ws *sync.WaitGroup) { defer ws.Done() reqList := []InventoryDto{ {GoodsID: 1, Num: 1}, } for _, item := range reqList { for { entity := model.InventoryEntity{} if err := utils.GormDb.Where("goods_id = ?", item.GoodsID).First(&entity).Error; err != nil { fmt.Println("查询错误:", err) break } stocks := entity.Stocks - item.Num if stocks < 0 { fmt.Println("库存不足..") break } fmt.Println("开始扣减库存...") result := utils.GormDb.Model(&model.InventoryEntity{}). Where("goods_id = ? AND version = ?", item.GoodsID, entity.Version). Updates(map[string]interface{}{ "stocks": stocks, "version": gorm.Expr("version + 1"), }) if result.Error != nil { fmt.Println("更新错误:", result.Error) } if result.RowsAffected != 0 { break } } } } func main() { wg := sync.WaitGroup{} for i := 0; i < 20; i++ { wg.Add(1) go Sell(&wg) } wg.Wait() }
六、使用redis分布式锁来实现
-
1、文档地址
-
2、简单的案例实现
gopackage main import ( "fmt" "sync" "time" goredislib "github.com/go-redis/redis/v8" "github.com/go-redsync/redsync/v4" "github.com/go-redsync/redsync/v4/redis/goredis/v8" ) func main() { // 配置redis连接 client := goredislib.NewClient(&goredislib.Options{ Addr: "localhost:6379", }) // 关闭客户端连接 defer client.Close() // 创建 redsync 需要的池 pool := goredis.NewPool(client) // 创建 redsync 实例 rs := redsync.New(pool) // 设置锁名称 mutexname := "my-global-mutex" gNum := 2 var wg sync.WaitGroup wg.Add(gNum) for i := 0; i < gNum; i++ { go func(id int) { defer wg.Done() mutex := rs.NewMutex(mutexname, redsync.WithExpiry(10*time.Second), redsync.WithTries(50), // 重试直到成功 redsync.WithRetryDelay(200*time.Millisecond)) fmt.Printf("goroutine %d: 开始获取锁...\n", id) if err := mutex.Lock(); err != nil { fmt.Printf("goroutine %d: 获取锁失败: %v\n", id, err) return } fmt.Printf("goroutine %d: 获取锁成功...\n", id) time.Sleep(time.Second * 5) fmt.Printf("goroutine %d: 开始释放锁...\n", id) if ok, err := mutex.Unlock(); !ok || err != nil { fmt.Printf("goroutine %d: 释放锁失败: %v\n", id, err) return } fmt.Printf("goroutine %d: 释放锁成功\n", id) }(i) } wg.Wait() fmt.Println("主进程结束..") } -
3、简单粗暴的实现
gopackage main import ( "fmt" "gin-admin-api/model" goredislib "github.com/go-redis/redis/v8" "github.com/go-redsync/redsync/v4" "github.com/go-redsync/redsync/v4/redis/goredis/v8" "sync" ) type InventoryDto1 struct { GoodsID int64 `json:"goodsId"` Num int64 `json:"num"` } // DeductStock1 使用Redis分布式锁扣减库存(优化版) func DeductStock1(ws *sync.WaitGroup) { client := goredislib.NewClient(&goredislib.Options{ Addr: "localhost:6379", }) pool := goredis.NewPool(client) // or, pool := redigo.NewPool(...) rs := redsync.New(pool) // 锁名要加上单号 mutex := rs.NewMutex("cell_order") if err := mutex.Lock(); err != nil { fmt.Println("获取锁失败") return } tx := utils.GormDb.Begin() defer ws.Done() reqList := []InventoryDto1{ {GoodsID: 1, Num: 2}, } for _, item := range reqList { inventoryEntity := model.InventoryEntity{} if err := utils.GormDb.Where("goods_id = ?", item.GoodsID).First(&inventoryEntity).Error; err != nil { fmt.Println("查询错误") tx.Rollback() break } if inventoryEntity.Stocks < item.Num { fmt.Printf("库存不足,剩余%d个,按实际扣减%d个\n", inventoryEntity.Stocks, item.Num) tx.Rollback() break } fmt.Println("开始扣减库存...") stocks := inventoryEntity.Stocks - item.Num result := tx.Model(&model.InventoryEntity{}). Where("goods_id = ?", item.GoodsID). Updates(map[string]interface{}{ "stocks": stocks, }) if result.Error != nil { fmt.Println("扣减库存失败", result.Error) tx.Rollback() break } } tx.Commit() // 先提交事务 mutex.Unlock() } func main() { wg := sync.WaitGroup{} // 模拟20并发 for i := 0; i < 20; i++ { wg.Add(1) go DeductStock1(&wg) } wg.Wait() fmt.Println("全部执行完成") }