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 协议》,转载必须注明作者和本文链接
微信搜索:上帝喜爱笨人
本帖由系统于 2个月前 自动加精
讨论数量: 12

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

2个月前 评论
Aliliin (楼主) 2个月前

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

2个月前 评论
leoliang (作者) 2个月前
leoliang (作者) 2个月前
leoliang (作者) 2个月前
Aliliin (楼主) 2个月前

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
}
2个月前 评论
leoliang (作者) 2个月前
ab0029 1周前
leoliang (作者) 1周前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!