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("库存扣减成功")
}
64
收起
正在回答 回答被采纳积分+1
2回答
慕斯卡8520410
2023-02-25 21:23:03
这个问题,我也遇到过,主要的原因在于,你应该把数据库操作口库存的操作包在redis分布式锁的加锁和释放锁代码中间----特别的是commit才算真正的数据库操作完成, 现在你的代码是把commit放在for循环语句外面了,这是不行的,我自己测了一遍是没问题的。100个协程并发处理,100个库存,全部扣完到0
Go开发工程师全新版
- 参与学习 489 人
- 解答问题 559 个
风口上的技术,薪资水平遥遥领先,现在学习正值红利期! 未来3-5年,Go语言势必成为企业高性能项目中不可替代的语言 从基础到项目实战再到重构,对转行人员友好,真正从入门到精通!
了解课程
恭喜解决一个难题,获得1积分~
来为老师/同学的回答评分吧
0 星