redis 分布式锁扣减库存不正确

redis 分布式锁扣减库存不正确

按老师的代码运行下来的结果,100个库存,50个goroutine,预期扣减后的结果剩余50个库存。但我运行后的结果是不正确的,库存剩余 55。前面已经有同学提过该问题了,但没有解决,我这里将上下文贴详细一些。

相关截图:

图片描述

相关代码:

Sell 方法

func (*InventoryServer) Sell(ctx context.Context, req *proto.SellInfo) (*emptypb.Empty, error) {
	//扣减库存, 本地事务 [1:10,  2:5, 3: 20]
	//数据库基本的一个应用场景:数据库事务
	//并发情况之下 可能会出现超卖 1
	client := goredislib.NewClient(&goredislib.Options{
		Addr: "127.0.0.1:6379",
	})
	pool := goredis.NewPool(client) // or, pool := redigo.NewPool(...)
	rs := redsync.New(pool)

	tx := global.DB.Begin()
	//m.Lock() //获取锁 这把锁有问题吗?  假设有10w的并发, 这里并不是请求的同一件商品  这个锁就没有问题了吗?

	//并发时候会有漏洞, 同一个时刻发送了重复了多次, 使用锁,分布式锁
	var details []model.GoodsDetail
	for _, goodInfo := range req.GoodsInfo {
		details = append(details, model.GoodsDetail{
			Goods: goodInfo.GoodsId,
			Num:   goodInfo.Num,
		})

		var inv model.Inventory
		//if result := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where(&model.Inventory{Goods:goodInfo.GoodsId}).First(&inv); result.RowsAffected == 0 {
		//	tx.Rollback() //回滚之前的操作
		//	return nil, status.Errorf(codes.InvalidArgument, "没有库存信息")
		//}

		//for {
		mutex := rs.NewMutex(fmt.Sprintf("goods_%d", goodInfo.GoodsId))
		if err := mutex.Lock(); err != nil {
			return nil, status.Errorf(codes.Internal, "获取redis分布式锁异常")
		}

		if result := global.DB.Where(&model.Inventory{Goods: goodInfo.GoodsId}).First(&inv); result.RowsAffected == 0 {
			tx.Rollback() //回滚之前的操作
			return nil, status.Errorf(codes.InvalidArgument, "没有库存信息")
		}
		//判断库存是否充足
		if inv.Stocks < goodInfo.Num {
			tx.Rollback() //回滚之前的操作
			return nil, status.Errorf(codes.ResourceExhausted, "库存不足")
		}
		//扣减, 会出现数据不一致的问题 - 锁,分布式锁
		inv.Stocks -= goodInfo.Num
		tx.Save(&inv)

		if ok, err := mutex.Unlock(); !ok || err != nil {
			return nil, status.Errorf(codes.Internal, "释放redis分布式锁异常")
		}
		//update inventory set stocks = stocks-1, version=version+1 where goods=goods and version=version
		//这种写法有瑕疵,为什么?
		//零值 对于int类型来说 默认值是0 这种会被gorm给忽略掉
		//if result := tx.Model(&model.Inventory{}).Select("Stocks", "Version").Where("goods = ? and version= ?", goodInfo.GoodsId, inv.Version).Updates(model.Inventory{Stocks: inv.Stocks, Version: inv.Version+1}); result.RowsAffected == 0 {
		//	zap.S().Info("库存扣减失败")
		//}else{
		//	break
		//}
		//}
		//tx.Save(&inv)
	}
	tx.Commit() // 需要自己手动提交操作
	//m.Unlock() //释放锁
	return &emptypb.Empty{}, nil
}

测试方法

func main() {
	Init()
	//并发情况之下 库存无法正确的扣减
	var wg sync.WaitGroup
	wg.Add(50)
	for i := 0; i < 50; i++ {
		go TestSell(&wg)
	}

	wg.Wait()
	conn.Close()
}

func TestSell(wg *sync.WaitGroup) {
	/*
		1. 第一件扣减成功: 第二件: 1. 没有库存信息 2. 库存不足
		2. 两件都扣减成功
	*/
	defer wg.Done()
	_, err := invClient.Sell(context.Background(), &proto.SellInfo{
		GoodsInfo: []*proto.GoodsInvInfo{
			{GoodsId: 421, Num: 1},
			//{GoodsId: 422, Num: 30},
		},
	})
	if err != nil {
		panic(err)
	}
	fmt.Println("库存扣减成功")
}

正在回答 回答被采纳积分+1

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

2回答
慕斯卡8520410 2023-02-25 21:23:03

这个问题,我也遇到过,主要的原因在于,你应该把数据库操作口库存的操作包在redis分布式锁的加锁和释放锁代码中间----特别的是commit才算真正的数据库操作完成, 现在你的代码是把commit放在for循环语句外面了,这是不行的,我自己测了一遍是没问题的。100个协程并发处理,100个库存,全部扣完到0

https://img1.sycdn.imooc.com//climg/63fa09b40976994316901536.jpg

https://img1.sycdn.imooc.com//climg/63fa09b4093036dd17401364.jpg

https://img1.sycdn.imooc.com//climg/63fa09bd09f422b500000000.jpg


  • ZYCGary #1

    这个代码也是有问题的,将 DB transaction 放到 for 循环之外的目的是保证如果有一个 goods 的数据库操作失败,可以保证当前请求里的所有 goods 数据操作回滚。如果把 transaction 放到 for 循环里,确实可以保证并发结果的正确,但是无法保证回滚后数据正确。

    2024-05-01 10:20:16
bobby 2023-01-09 17:07:11

你发一下前面的其他问题的url 我看看

问题已解决,确定采纳
还有疑问,暂不采纳

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

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

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

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

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

在线咨询

领取优惠

免费试听

领取大纲

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