如何基于Redis分布式锁与SQL事务实现购物车多商品库存扣减并保障一致性
分布式库存扣减Redis锁实现方案
现有代码核心问题
当前实现存在三个致命问题,会直接导致超卖、死锁等线上故障:
- 锁释放时机错误:循环内修改完单条库存就立即解锁,但此时数据库事务尚未提交,修改未持久化到数据库。其他请求拿到锁后查询到的仍是旧库存值,直接触发超卖。
- 分布式死锁风险:不同请求如果同时操作多个相同商品、但加锁顺序相反时,会出现永久互相等待的死锁问题。比如请求1先锁商品A再申请锁商品B,请求2先锁商品B再申请锁商品A,两者都会卡住无法执行。
- 锁保护范围不匹配:Redis锁的生命周期没有覆盖到事务提交的完整阶段,完全失去了分布式锁保证数据一致性的作用。
正确实现逻辑
基于细粒度Redis锁的实现需要严格遵循以下步骤,既能保证一致性,又能最大化并发性能:
- 首先对本次请求要操作的所有商品ID做统一排序(比如全局升序),所有请求都按照固定顺序加锁,从根源避免死锁。
- 开启数据库事务之前,按排序后的顺序依次申请所有商品对应的Redis锁,只要有一个锁申请失败,立即释放已经拿到的所有锁,终止请求返回重试提示即可。
- 所有锁都申请成功后,再开启数据库事务,依次执行库存查询、扣减、保存操作。
- 事务提交或回滚完成后,再统一释放所有Redis锁。
- 额外优化:Redis锁的过期时间需设置为超过预估的最长事务执行时间,生产环境建议搭配锁看门狗自动续期,避免事务执行过久导致锁提前过期失效。
修正后的Go代码示例
import "time" import "sort" func (*InventoryServer) Sell(ctx context.Context, req *proto.SellInfo) (*emptypb.Empty, error) { client := goredislib.NewClient(&goredislib.Options{ Addr: "localhost:6379", }) pool := goredis.NewPool(client) rs := redsync.New(pool) // 1. 对商品列表按ID升序排序,固定加锁顺序避免死锁 sort.Slice(req.GoodsInfo, func(i, j int) bool { return req.GoodsInfo[i].GoodsId < req.GoodsInfo[j].GoodsId }) var mutexList []*redsync.Mutex // 兜底逻辑:请求结束统一释放所有已获取的锁 defer func() { for _, mutex := range mutexList { _, _ = mutex.Unlock() } }() // 2. 提前获取所有商品的分布式锁 for _, goodInfo := range req.GoodsInfo { mutex := rs.NewMutex( fmt.Sprintf("goodsID:%d", goodInfo.GoodsId), redsync.WithExpiry(5*time.Second), // 锁过期时间可根据实际业务耗时调整 ) // 支持上下文取消,避免请求超时后仍阻塞等锁 if err := mutex.LockContext(ctx); err != nil { return nil, status.Errorf(codes.Internal, "获取商品%d分布式锁失败,请重试", goodInfo.GoodsId) } mutexList = append(mutexList, mutex) } // 3. 所有锁获取成功后再开启数据库事务 tx := global.DB.Begin() defer func() { if r := recover(); r != nil { tx.Rollback() } }() // 执行库存扣减逻辑 for _, goodInfo := range req.GoodsInfo { var inv model.Inventory if result := global.DB.Where(&model.Inventory{Goods: goodInfo.GoodsId}).First(&inv); result.RowsAffected == 0 { tx.Rollback() return nil, status.Errorf(codes.NotFound, "商品%d无库存信息", goodInfo.GoodsId) } if inv.Stocks < goodInfo.Num { tx.Rollback() return nil, status.Errorf(codes.ResourceExhausted, "商品%d库存不足", goodInfo.GoodsId) } inv.Stocks -= goodInfo.Num if result := tx.Save(&inv); result.Error != nil { tx.Rollback() return nil, status.Errorf(codes.Internal, "商品%d库存扣减失败", goodInfo.GoodsId) } } // 4. 提交事务 if err := tx.Commit().Error; err != nil { tx.Rollback() return nil, status.Errorf(codes.Internal, "事务提交失败,请重试") } return &emptypb.Empty{}, nil }
内容的提问来源于stack exchange,提问作者W.W.G
相关产品推荐
相关产品推荐

