场景:高并发下商品库存扣减不正确的问题
目录
一、通过go语言自带的锁(sync.Mutex)解决并发问题
一、通过go语言自带的锁(sync.Mutex)解决并发问题
// 库存扣减 本地事务(购物车)
// 数据库基本的一个应用场景:数据库事务(数据的一致性)
// 并发情况下,可能会出现超卖,出现数据不一致的问题--》解决方法:加锁,分布式锁
var m sync.Mutex
func (*InventoryServer) Sell(ctx context.Context, req *proto.SellInfo) (*emptypb.Empty, error) {
tx := global.DB.Begin() //开启事务
m.Lock() //获取锁 这把锁有问题吗? 假设有10w的并发,这里并不是请求的同件商品 ;这个锁就没有问题了吗?
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, "该商品库存信息不存在")
}
if inv.Stocks < goodInfo.Num {
tx.Rollback() //回滚到之前的操作
return nil, status.Errorf(codes.ResourceExhausted, "该商品库存不足")
}
//扣减
inv.Stocks -= goodInfo.Num
tx.Save(&inv)
}
tx.Commit() //手动提交事务
m.Unlock() //释放锁
return &emptypb.Empty{}, nil
}
func TestSell(wg *sync.WaitGroup) {
defer wg.Done()
_, err := InvClient.Sell(context.Background(), &proto.SellInfo{
GoodsInfo: []*proto.GoodsInvInfo{
&proto.GoodsInvInfo{GoodsId: 421, Num: 1},
//&proto.GoodsInvInfo{GoodsId: 422, Num: 100},
}})
if err != nil {
panic(err)
}
fmt.Println("库存扣减成功")
}
func main() {
Init()
//TestBannerList()
//TestSetInv(421, 9)
//for i := 421; i < 841; i++ {
// TestSetInv(int32(i), 100)
//}
//TestInvDetail(422)
var wg sync.WaitGroup
wg.Add(80)
for i := 0; i < 80; i++ {
go TestSell(&wg)
}
wg.Wait()
conn.Close()
}
在扣减库存这个功能实现中,需要注意:
因在这个功能中使用到了数据库事务操作,所以在go并发协程获取数据库中同一件商品的库存数量时,需要 在事务从数据库获取到数据前,获取到锁(m.Lock),在事务提交到数据库之后再释放锁(tx.commit之后m.Unlock).
但在这个sell()功能中,仍然存在问题:多个不同的商品调用同一个sell方法,不同商品之间也会造成阻塞排队,这是有问题的。
二、mysql的for update语句实现悲观锁(行锁)
实现悲观锁属于数据库层面的操作
gorm实现for update 悲观锁
位置:gorm的高级查询
链接:高级查询 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.
说明:
悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据。
三、mysql的乐观锁实现
原理:
方法有2种:
1、使用数据版本(version)的方式
这是乐观锁最常用的一种实现 方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。
2、使用时间戳的方式
同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳 (timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。
这里使用数据版本的方式:
// 库存扣减 本地事务(购物车)
// 数据库基本的一个应用场景:数据库事务(数据的一致性)
// 并发情况下,可能会出现超卖,出现数据不一致的问题--》解决方法:加锁,分布式锁
//var m sync.Mutex
func (*InventoryServer) Sell(ctx context.Context, req *proto.SellInfo) (*emptypb.Empty, error) {
tx := global.DB.Begin() //开启事务
//m.Lock() //获取锁 这把锁有问题吗? 假设有10w的并发,这里并不是请求的同一件商品 ;这个锁就没有问题了吗?
for _, goodInfo := range req.GoodsInfo {
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.NotFound, "该商品库存信息不存在")
//}
for {
//乐观锁实现
if result := global.DB.Where(&model.Inventory{Goods: goodInfo.GoodsId}).First(&inv); result.RowsAffected == 0 {
tx.Rollback() //回滚到之前的操作
return nil, status.Errorf(codes.NotFound, "该商品库存信息不存在")
}
if inv.Stocks < goodInfo.Num {
tx.Rollback() //回滚到之前的操作
return nil, status.Errorf(codes.ResourceExhausted, "该商品库存不足")
}
//扣减
inv.Stocks -= goodInfo.Num
//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
}
乐观锁实现的关键语句为:
//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
}
四、基于redsync分布式锁实现
使用redis的redsync分布式锁实现,此处需要注意 setnx 和 redlock
package main
import (
"fmt"
"github.com/go-redsync/redsync/v4"
"github.com/go-redsync/redsync/v4/redis/goredis/v9"
goredislib "github.com/redis/go-redis/v9"
"sync"
"time"
)
func main() {
// Create a pool with go-redis (or redigo) which is the pool redisync will
// use while communicating with Redis. This can also be any pool that
// implements the `redis.Pool` interface.
client := goredislib.NewClient(&goredislib.Options{
Addr: "192.168.1.7:6379",
})
pool := goredis.NewPool(client) // or, pool := redigo.NewPool(...)
// Create an instance of redisync to be used to obtain a mutual exclusion
// lock.
rs := redsync.New(pool)
// Obtain a new mutex by using the same name for all instances wanting the
// same lock.
gNum := 2
mutexname := "421"
var wg sync.WaitGroup
wg.Add(gNum)
for i := 0; i < gNum; i++ {
go func() {
defer wg.Done()
mutex := rs.NewMutex(mutexname)
// Obtain a lock for our given mutex. After this is successful, no one else
// can obtain the same lock (the same mutex name) until we unlock it.
fmt.Println("开始获取锁")
if err := mutex.Lock(); err != nil {
panic(err)
}
fmt.Println("获取锁成功")
// Do your work that requires the lock.
time.Sleep(time.Second * 5)
fmt.Println("开始释放锁")
// Release the lock so other processes or threads can obtain a lock.
if ok, err := mutex.Unlock(); !ok || err != nil {
panic("unlock failed")
}
fmt.Println("释放锁成功")
}()
}
wg.Wait()
}
将redsync集成到库存服务中:
func (*InventoryServer) Sell(ctx context.Context, req *proto.SellInfo) (*emptypb.Empty, error) {
initialize.InitRedis()
tx := global.DB.Begin() //开启事务
//m.Lock() //获取锁 这把锁有问题吗? 假设有10w的并发,这里并不是请求的同一件商品 ;这个锁就没有问题了吗?
for _, goodInfo := range req.GoodsInfo {
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.NotFound, "该商品库存信息不存在")
//}
//for {
mutex := global.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.NotFound, "该商品库存信息不存在")
}
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
}
参考资料: