Go 库存扣减的几种实现方法

Go 库存扣减的几种实现方法

!!! 本篇文章只是简单提供个实现的思路,如果你要用到生产环境,请自行优化方法。尤其多个微服务之间。!!!

这里使用了 grpc、proto、gorm、zap、go-redis、go-redsync 等 package

Go Mutex 实现
var m sync.Mutex func (*InventoryServer) LockSell(ctx context.Context, req *proto.SellInfo) (*emptypb.Empty, error) { tx := global.DB.Begin() m.Lock() for _, good := range req.GoodsInfo { var i model.Inventory if result := global.DB.Where(&model.Inventory{Goods: good.GoodsId}).First(&i); result.RowsAffected == 0 { tx.Rollback() // 回滚 return nil, status.Errorf(codes.InvalidArgument, "未找到此商品的库存信息。") } if i.Stocks < good.Num { tx.Rollback() return nil, status.Errorf(codes.ResourceExhausted, "此商品的库存不足") } i.Stocks -= good.Num tx.Save(&i) } tx.Commit() m.Unlock() return &emptypb.Empty{}, nil }
MySQL 悲观锁实现
func (*InventoryServer) ForUpdateSell(ctx context.Context, req *proto.SellInfo) (*emptypb.Empty, error) { tx := global.DB.Begin() for _, good := range req.GoodsInfo { var i model.Inventory if result := tx.Clauses(clause.Locking{ Strength: "UPDATE", }).Where(&model.Inventory{Goods: good.GoodsId}).First(&i); result.RowsAffected == 0 { tx.Rollback() return nil, status.Errorf(codes.InvalidArgument, "未找到此商品的库存信息。") } if i.Stocks < good.Num { tx.Rollback() return nil, status.Errorf(codes.ResourceExhausted, "此商品的库存不足") } i.Stocks -= good.Num tx.Save(&i) } tx.Commit() return &emptypb.Empty{}, nil } 
MySQL 乐观锁实现
func (*InventoryServer) VersionSell(ctx context.Context, req *proto.SellInfo) (*emptypb.Empty, error) { tx := global.DB.Begin() for _, good := range req.GoodsInfo { var i model.Inventory for { // 并发请求相同条件比较多,防止放弃掉一些请求 if result := global.DB.Where(&model.Inventory{Goods: good.GoodsId}).First(&i); result.RowsAffected == 0 { tx.Rollback() return nil, status.Errorf(codes.InvalidArgument, "未找到此商品的库存信息.") } if i.Stocks < good.Num { tx.Rollback() // 回滚 return nil, status.Errorf(codes.ResourceExhausted, "此商品的库存不足") } i.Stocks -= good.Num version := i.Version + 1 if result := tx.Model(&model.Inventory{}). Select("Stocks", "Version"). Where("goods = ? and version= ?", good.GoodsId, i.Version). Updates(model.Inventory{Stocks: i.Stocks, Version: version}); result.RowsAffected == 0 { ​ zap.S().Info("库存扣减失败!") } else { break } } } tx.Commit() // 提交 return &emptypb.Empty{}, nil }
Redis 分布式锁实现
func (*InventoryServer) RedisSell(ctx context.Context, req *proto.SellInfo) (*emptypb.Empty, error) { // redis 分布式锁 pool := goredis.NewPool(global.Redis) rs := redsync.New(pool) tx := global.DB.Begin() for _, good := range req.GoodsInfo { mutex := rs.NewMutex(fmt.Sprintf("goods_%d", good.GoodsId)) if err := mutex.Lock(); err != nil { return nil, status.Errorf(codes.Internal, "redis:分布式锁获取异常") } var i model.Inventory if result := global.DB.Where(&model.Inventory{Goods: good.GoodsId}).First(&i); result.RowsAffected == 0 { tx.Rollback() return nil, status.Errorf(codes.InvalidArgument, "未找到此商品的库存信息") } if i.Stocks < good.Num { tx.Rollback() return nil, status.Errorf(codes.ResourceExhausted, "此商品的库存不足") } i.Stocks -= good.Num tx.Save(&i) if ok, err := mutex.Unlock(); !ok || err != nil { return nil, status.Errorf(codes.Internal, "redis:分布式锁释放异常") } } tx.Commit() return &emptypb.Empty{}, nil }

测试

涉及到服务、数据库等环境,此测试为伪代码

func main() { var w sync.WaitGroup w.Add(20) for i := 0; i < 20; i++ { go TestForUpdateSell(&w) // 模拟并发请求 } w.Wait() } ​ func TestForUpdateSell(wg *sync.WaitGroup) { defer wg.Done() _, err := invClient.Sell(context.Background(), &proto.SellInfo{ GoodsInfo: []*proto.GoodsInvInfo{ {GoodsId: 16, Num: 1}, //{GoodsId: 16, Num: 10}, }, }) if err != nil { panic(err) } fmt.Println("库存扣减成功") }
本作品采用《CC 协议》,转载必须注明作者和本文链接
微信搜索:上帝喜爱笨人
本帖由系统于 3年前 自动加精
讨论数量: 16
zhaocrazy

大佬 牛

4年前 评论

我有个疑问,为什么不直接用redis的INCR DECR,有什么安全隐患吗?

3年前 评论
Aliliin (楼主) 3年前

mutex实现有点问题,锁粒度太粗了。而且部署了多个实例,根本锁不住

3年前 评论
leoliang (作者) 3年前
leoliang (作者) 3年前
leoliang (作者) 3年前
Aliliin (楼主) 3年前
 func (*InventoryServer) VersionSell(ctx context.Context, req *proto.SellInfo) (*emptypb.Empty, error) { //获取req里的goods_id列表 goodsIds := getGoodsIds(req.GoodsInfo) //批量查询数据库,确保商品是存在的 var invertoryList []model.Inventory result := global.DB.Where(`goods in ?`, goodsIds).Find(&invinvertoryList) //检查商品是否存在 if len(invertoryList) != len(goodsIds) { return nil, status.Errorf(codes.InvalidArgument, "未找到此商品的库存信息.") } tx := global.DB.Begin() //这里的技巧是利用mysql的自减操作,所以不需要查询出来判断库存和版本号再更新,只需要确保自减操作不会变成负数,商品库存没有超卖就行了,所以where条件要加上stock >= num const rawSql = `update inventory set stocks = stocks - ?, updated_at = ? where goods = ? and where stocks >= ?` for _, good := range req.GoodsInfo { //从上面查到列表中取出商品信息 inventory := getIninvertoryById(ininvertoryList, good.GoogoodsId) if result := global.DB.Raw(rawSql, good.Num, time.Now.Unix(), good.goodsId, good.Num).Scan(); result.RowsAffected == 0 { tx.Rollback() //秒杀扣库存失败,回滚事务 return nil, status.Errorf(codes.InvalidArgument, "库存扣减失败.") } } tx.Commit() // 提交 return &emptypb.Empty{}, nil }
3年前 评论
leoliang (作者) 3年前
ab0029 3年前
leoliang (作者) 3年前

file
事务未提交就进行解锁操作,并发情况下是不是会出现超卖的情况?

3年前 评论
Aliliin (楼主) 3年前

mutex.Unlock() 应该使用defer 调用,不然中间return出去,就没解锁了

3年前 评论
Aliliin (楼主) 3年前