看到重构 库存归还 和 库存扣减 还是有点问题

看到重构 库存归还 和 库存扣减 还是有点问题

我这里是想求证 是不是我这样想的  

问题我已经按照下面的想法 实现了 

不知道是否想错了  所有提出来 问一下的 是否正确


问题1
在库存扣减中
redis分布式锁 是 锁订单号的情况下

也就是说 如果 同时来2个不同的订单的情况下 
扣减相同商品 一样会导致 最终商品数量不一致的问题  因为 你锁的是订单号

解决办法

这里应该修改成为 redis分布式锁 锁 商品id

并且在外面再加一把锁 锁住订单号  防止 用户连续点击库存扣减 重复执行

问题2 
在库存归还中
reids分布式锁 是锁订单号的情况下
为了 防止 用户连续点击库存扣减 重复执行
但是 在高并发的情况下
商品没被锁住  一样 会导致 库存扣减产生问题 

解决办法

一样需要一把锁 确保 当前时间下 只有一个程序进行操作某商品的库存


这2个问题结合

说明 这个2个接口分别少了 2把锁
并且 2个接口的4个锁中     

    锁 订单号的锁名称 应该一致

    锁 商品id 的锁名称 也应该一致






正在回答

登陆购买课程后可参与讨论,去登陆

3回答
锁住订单号,一个订单扣减A商品1件,订单B来了也扣一件,这个时候没有库存了,B就回滚 为什么会不一致?你举个具体的例子说明一下呢
usechatgpt init success
  • 请问老师,如果锁住订单号,订单A扣减 1号商品。订单B获取的是另一个锁,也是扣减1号商品(1号商品有库存充足),这不就相当于没锁住吗?两个订单同时操作1号商品。

    2023-05-15 22:39:23
  • https://img1.sycdn.imooc.com//climg/64624647092afde613690677.jpg

    2023-05-15 22:48:42
  • 城中城 提问者 #3

    额  这个同学已经把我想问的 说出来了
    我不是问库存不足的情况, 其实是想问并发的情况

    库存扣减:
        为什么要锁商品id:

        前提条件(只锁了订单的情况下,进行商品扣减时):

        小明同学 和 小城同学 同时下了一个订单,并且 是扣减相同的商品,也可能导致后端并发进行扣减(如果没有锁商品的情况下,你只是锁了订单号不锁商品的问题导致的)

        结果就是:

        可能会导致 相同商品重复扣减

        为什么要锁订单号:

        订单的重复提交,如果锁住订单号, 可以稍微减少对 商品的 锁

        虽然加不加 最后 我们都会判断是否有这个订单 如果有回滚的处理


    库存归还:

        为什么要锁商品id:

        前提条件(只锁了订单的情况下,进行商品归还时):

        你库存服务宕机了一段时间恢复了 或处于高峰期, 然后 rocketmq 或者 dtm 给一次性提交了 一堆订单归还的请求,这个时候 你进行库存归还 可能导致

        因为 相同商品 可能同时进行这 库存的扣减 和 归还 或 不同订单 相同商品的归还

        结果就是:

        可能会导致 相同商品重复归还

        并且 库存扣减 和 归还都要用相同的锁

        为什么要锁订单号:

        和库存扣减也一样, 减少 对 商品的锁



    2023-05-16 10:51:21
提问者 城中城 2023-05-22 16:22:46
package srv

import (
    "context"
    "database/sql"
    "github.com/dtm-labs/client/dtmgrpc"
    redsyncredis "github.com/go-redsync/redsync/v4/redis"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
    "mxshop/app/mxshop_srv/inventory/srv/internal/data/v2"
    "mxshop/app/mxshop_srv/inventory/srv/internal/domain/do"
    "mxshop/app/mxshop_srv/inventory/srv/internal/domain/dto"
    "mxshop/app/mxshop_srv/inventory/srv/internal/service"
    code2 "mxshop/gmicro/pkg/code"
    "mxshop/gmicro/pkg/errors"
    "mxshop/gmicro/pkg/log"
    "mxshop/pkg/code"
    
    "sort"
)

const (
    inventoryLockPrefix = "inventory" // 锁商品库存用
)

type inventoryService struct {
    data data.DataFactory
    pool redsyncredis.Pool // redis 池
}

func (is *inventoryService) Create(ctx context.Context, inv *dto.InventoryDTO) error {
    return is.data.Inventory().Create(ctx, &inv.InventoryDO)
}

func (is *inventoryService) Get(ctx context.Context, goodsID int64) (*dto.InventoryDTO, error) {
    inv, err := is.data.Inventory().Get(ctx, goodsID)
    if err != nil {
       return nil, err
    }
    return &dto.InventoryDTO{InventoryDO: *inv}, nil
}

func (is *inventoryService) Sell(ctx context.Context, ordersn string, details []do.GoodsDetail) error {
    log.Infof("订单 %s 扣减库存", ordersn)

    //rs := redsync.New(is.pool)

    barrier, _ := dtmgrpc.BarrierFromGrpc(ctx)
    txn := is.data.Begin()
    sourceTx := txn.Statement.ConnPool.(*sql.Tx)

    err := barrier.Call(sourceTx, func(tx *sql.Tx) error {
       // 先按照商品的id排序, 然后从小到大逐个扣减库存, 防止锁竞争 和 防止死锁
       var detail = do.GoodsDetailList(details)
       sort.Sort(&detail)

       sellDetail := do.StockSellDetailDO{
          OrderSn: ordersn,
          Status:  1,
          Detail:  detail,
       }

       for _, goodsInfo := range detail {
          //mutexGoods := rs.NewMutex(strings.Join([]string{inventoryLockPrefix, strconv.Itoa(int(goodsInfo.Goods))}, "_"))
          //if err := mutexGoods.Lock(); err != nil {
          // log.InfofC(ctx, "商品 %d 获取锁失败", goodsInfo.Goods)
          // return status.Error(codes.Aborted, err.Error()) // 回滚
          //
          //}
          //defer mutexGoods.Unlock()

          // 查询库存信息是否存在
          var inv *do.InventoryDO
          inv, err := is.data.Inventory().Get(ctx, goodsInfo.Goods)
          if err != nil {
             log.Errorf("订单 %s 获取库存失败", ordersn)
             return status.Error(codes.FailedPrecondition, err.Error()) // 重试
          }

          // 判断库存是否充足
          if inv.Stocks < goodsInfo.Num {
             log.Errorf("商品 %d 库存 %d 不足, 现有库存 %d", goodsInfo.Goods, goodsInfo.Num, inv.Stocks)
             return status.Error(codes.Aborted, "库存不足") // 回滚
          }
          inv.Stocks -= goodsInfo.Num

          result, err := is.data.Inventory().Reduce(ctx, sourceTx, goodsInfo.Goods, goodsInfo.Num)
          if err != nil {
             log.Errorf("订单 %s 扣减库存失败", ordersn)
             return status.Error(codes.FailedPrecondition, err.Error()) // 重试
          }
          if rows, _ := result.RowsAffected(); rows == 0 {
             return status.Error(codes.Aborted, "查询不到商品库存信息") // 回滚
          }
       }

       _, err := is.data.Inventory().CreateStockSellDetail(ctx, sourceTx, &sellDetail)
       if err != nil {
          if errors.Code(err) != code2.ErrDecodingJSON {
             log.Errorf("订单 %s 创建扣减库存记录失败", ordersn)
             return status.Error(codes.FailedPrecondition, err.Error()) // 数据库:重试
          } else {
             log.Errorf("订单 %s JSON 映射失败回滚", ordersn)
             return status.Error(codes.Aborted, err.Error()) // json:回滚
          }

       }
       return nil
    })
    if err != nil {
       return err
    }
    return nil
}

func (is *inventoryService) Repack(ctx context.Context, ordersn string, details []do.GoodsDetail) error {
    //log.Infof("订单 %s 归还库存", ordersn)

    //rs := redsync.New(is.pool)

    barrier, _ := dtmgrpc.BarrierFromGrpc(ctx)
    txn := is.data.Begin()
    sourceTx := txn.Statement.ConnPool.(*sql.Tx)

    err := barrier.Call(sourceTx, func(tx *sql.Tx) error {
       // 获取订单
       sellDetail, err := is.data.Inventory().GetSellDetail(ctx, ordersn)
       if err != nil {
          if errors.IsCode(err, code.ErrInvSellDetailNotFound) {
             log.Errorf("[忽略]订单 %s 扣减库存记录不存在", ordersn)
             return nil // 订单不存在 说明还没下单 忽略 (理论上不存在这个问题)
          }
          log.Errorf("订单 %s 获取扣减库存记录失败", ordersn)
          return status.Error(codes.FailedPrecondition, err.Error()) // 重试 可能 mysql 出现问题
       }

       if sellDetail.Status == 2 {
          log.Infof("订单 %s 扣减库存记录已经归还, 忽略", ordersn)
          return nil // 已经归还 忽略
       }

       var detail = do.GoodsDetailList(details)
       sort.Sort(&detail)

       for _, goodsInfo := range sellDetail.Detail {
          //mutexGoods := rs.NewMutex(strings.Join([]string{inventoryLockPrefix, strconv.Itoa(int(goodsInfo.Goods))}, "_"))
          //if err = mutexGoods.Lock(); err != nil {
          // log.InfofC(ctx, "订单 %s 获取锁失败", ordersn)
          // return status.Error(codes.FailedPrecondition, err.Error()) // 重试 redis 出现问题
          //}
          //defer mutexGoods.Unlock()

          inv, err := is.data.Inventory().Get(ctx, goodsInfo.Goods)
          if err != nil {
             log.Errorf("订单 %s 获取商品库存失败", ordersn)
             return status.Error(codes.FailedPrecondition, err.Error()) // 重试
          }
          inv.Stocks += goodsInfo.Num

          result, err := is.data.Inventory().Increase(ctx, sourceTx, goodsInfo.Goods, goodsInfo.Num)
          if err != nil {
             log.Errorf("订单 %s 归还库存失败", ordersn)
             return status.Error(codes.FailedPrecondition, err.Error()) // 重试
          }
          if rows, _ := result.RowsAffected(); rows == 0 {
             return status.Error(codes.Aborted, "查询不到商品库存信息") // 回滚
          }
       }
       result, err := is.data.Inventory().UpdateStockSellDetailStatus(ctx, sourceTx, ordersn, 2)
       if err != nil {
          log.Errorf("订单 %s 更新订单状态失败", ordersn)
          return status.Error(codes.FailedPrecondition, err.Error()) // 重试
       }
       if rows, _ := result.RowsAffected(); rows == 0 {
          return status.Error(codes.Aborted, "查询不到此订单信息") // 回滚
       }
       return nil
    })
    if err != nil {
       return err
    }
    return nil
}

func newInventory(srv *serviceFactory) service.InventorySrv {
    return &inventoryService{
       data: srv.data,
       pool: srv.pool,
    }
}

代码是  三层代码结构 的 server 层 
使用 apifox测试循环测试了 看起来好像没问题
这里不用 redis锁  使用 dtm 来确保


bobby 2023-05-19 21:06:46

https://img1.sycdn.imooc.com//climg/6467744e095a75ff13330462.jpg 这个语句是原子性的,可以确保订单库存扣减不会超卖

  • 提问者 城中城 #1

    我知道为什么 我觉得有问题 但是老师又觉得没问题了

    是因为  update 这个语句 你更新的时候, 如果没有更新成功,它也不是 报错, 你需要在 三层代码结构的 server 层 需要额外判断  result.RowsAffected == 0 || result.Error != nil  的情况
    第一种是 库存不足  第二种是 mysql 有问题

    2023-05-22 14:57:06
  • bobby 回复 提问者 城中城 #2

    是的,如果不想超卖,那么上面的update语句会报告更新了多少行,没有更新成功就是库存不足,update语句已经兼具了检查库存是否充足以及扣减库存的逻辑 而且是原子性的

    2023-05-27 12:27:11
问题已解决,确定采纳
还有疑问,暂不采纳

恭喜解决一个难题,获得1积分~

来为老师/同学的回答评分吧

0 星
Go开发工程师全新版
  • 参与学习       489    人
  • 解答问题       559    个

风口上的技术,薪资水平遥遥领先,现在学习正值红利期! 未来3-5年,Go语言势必成为企业高性能项目中不可替代的语言 从基础到项目实战再到重构,对转行人员友好,真正从入门到精通!

了解课程
请稍等 ...
意见反馈 帮助中心 APP下载
官方微信

在线咨询

领取优惠

免费试听

领取大纲

扫描二维码,添加
你的专属老师