You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于Redis分布式锁与SQL事务实现购物车多商品库存扣减并保障一致性

分布式库存扣减Redis锁实现方案

现有代码核心问题

当前实现存在三个致命问题,会直接导致超卖、死锁等线上故障:

  1. 锁释放时机错误:循环内修改完单条库存就立即解锁,但此时数据库事务尚未提交,修改未持久化到数据库。其他请求拿到锁后查询到的仍是旧库存值,直接触发超卖。
  2. 分布式死锁风险:不同请求如果同时操作多个相同商品、但加锁顺序相反时,会出现永久互相等待的死锁问题。比如请求1先锁商品A再申请锁商品B,请求2先锁商品B再申请锁商品A,两者都会卡住无法执行。
  3. 锁保护范围不匹配: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.27 00:24:04