Golang微服务(三)
分布式锁
gRPC service层demo router handler如下:
/*
>>>model
type Inventory struct {
BaseModel
Goods int32 `gorm:"type:int;index"`
Stocks int32 `gorm:"type:int"`
Version int32 `gorm:"type:int"`
}
>>>proto源
syntax = "proto3";
import "google/protobuf/empty.proto";
option go_package = ".;proto";
service Inventory {
...
rpc Sell(SellInfo) returns (google.protobuf.Empty);
...
}
message GoodsInv {
int32 goodsID = 1;
int32 num = 2;
}
message SellInfo {
repeated GoodsInv goodsInfo = 1;
}
>>>proto生成的结构
type SellInfo struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
GoodsInfo []*GoodsInv `protobuf:"bytes,1,rep,name=goodsInfo,proto3" json:"goodsInfo,omitempty"`
}
type GoodsInv struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
GoodsID int32 `protobuf:"varint,1,opt,name=goodsID,proto3" json:"goodsID,omitempty"`
Num int32 `protobuf:"varint,2,opt,name=num,proto3" json:"num,omitempty"`
}
*/
func (is *InventoryServer) Sell(ctx context.Context, req *proto.SellInfo) (*emptypb.Empty, error) {
tx := global.DB.Begin()
for _, good := range req.GoodsInfo {
var inv model.Inventory
if result := global.DB.Where("goods=?", good.GoodsID).First(&inv); result.RowsAffected < 1 {
tx.Rollback()
return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("参数错误商品:%d不存在", good.GoodsID))
}
if inv.Stocks < good.Num {
tx.Rollback()
return nil, status.Errorf(codes.ResourceExhausted, fmt.Sprintf("商品:%d库存不足", good.GoodsID))
}
inv.Stocks -= good.Num
tx.Save(&inv)
}
tx.Commit()
return &emptypb.Empty{}, nil
}
互斥锁
当并发请求Sell时,可能会出现库存扣减总量与请求扣减总量不一致的情况,这种并发请求带来的问题无法通过数据库事务来解决,而需要靠锁,将读写过程串行:
var m sync.Mutex
func (is *InventoryServer) Sell(ctx context.Context, req *proto.SellInfo) (*emptypb.Empty, error) {
tx := global.DB.Begin()
m.Lock()
...
m.Lock()
return &emptypb.Empty{}, nil
}
但这样有个问题:此接口在按照订单处理预扣库存,即多种商品读写在没必要得全部串行(只将同商品的数据修改串行即可确保数据安全)
悲观锁
mysql的for update语句会有一些特性:在索引列for update会给满足条件的记录做行锁,在非索引列or update时会升级为表锁,但是只针对更新语句,如果没有符合条件的语句,则不会锁表。commit后释放锁。
所以可以修改为如下悲观锁:
func (is *InventoryServer) Sell(ctx context.Context, req *proto.SellInfo) (*emptypb.Empty, error) {
tx := global.DB.Begin()
for _, good := range req.GoodsInfo {
var inv model.Inventory
// 只需在此处使用gorm的Clauses
if result := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("goods=?", good.GoodsID).First(&inv); result.RowsAffected < 1 {
tx.Rollback()
return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("参数错误商品:%d不存在", good.GoodsID))
}
if inv.Stocks < good.Num {
tx.Rollback()
return nil, status.Errorf(codes.ResourceExhausted, fmt.Sprintf("商品:%d库存不足", good.GoodsID))
}
inv.Stocks -= good.Num
tx.Save(&inv)
}
tx.Commit()
return &emptypb.Empty{}, nil
}
悲观锁并不完全反对并发,很多情况下只是行锁,对于正常的select语句也不会造成影响。但悲观锁的性能确实不尽如人意。
乐观锁
乐观锁本质上是保证数据一致性的一种解决方案,优点是在没有让数据库加锁的前提下避免了数据不一致的问题:通过在记录中增加版本号字段,在并发读取了同一条原纪录且尝试同时将新的数据保存至原纪录时确保只有一次保存成功并保证其他保存动作全部失败。
将以上代码修改为乐观锁:
func (is *InventoryServer) Sell(ctx context.Context, req *proto.SellInfo) (*emptypb.Empty, error) {
tx := global.DB.Begin()
for _, good := range req.GoodsInfo {
var inv model.Inventory
for {
if result := global.DB.Where("goods=?", good.GoodsID).First(&inv); result.RowsAffected < 1 {
tx.Rollback()
return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("参数错误商品:%d不存在", good.GoodsID))
}
if inv.Stocks < good.Num {
tx.Rollback()
return nil, status.Errorf(codes.ResourceExhausted, fmt.Sprintf("商品:%d库存不足", good.GoodsID))
}
if result := tx.Model(&model.Inventory{}).Select("Stocks", "Version").Where("goods = ? AND version = ?", inv.Goods, inv.Version).Updates(model.Inventory{
Stocks: inv.Stocks - good.Num,
Version: inv.Version + 1,
}); result.RowsAffected > 0 {
break
}
}
}
tx.Commit()
return &emptypb.Empty{}, nil
}
基于Redis的分布式锁
通过对redis指定key的查询,不同服务可以共享同一把锁,另外redis可以提供包括setnx在内的一些命令来实现指定key的get&set的原子操作,用来完成锁的查询、获取、释放等。
保证互斥性:原子操作
防死锁常用操作逻辑链:防死锁-->设置超时-->防止超时影响正常业务逻辑完整执行-->设置延时-->防止某种服务卡住导致无限申请延时
安全性:value值与goroutine绑定(genValueFunc: genValue),只有持有锁的goroutine可以删除key-value
集群问题:redlock,m.actOnPoolsAsync,不分主从的redis集群,通过获取过半redis实例的锁来确定当前goroutine在所有redis实例上的持有,未能获取过半实例的goroutine释放已经获取的实例上的锁并进入轮询拿锁。
相对于乐观锁,分布式锁工作的条件更苛刻。
func (is *InventoryServer) Sell(ctx context.Context, req *proto.SellInfo) (*emptypb.Empty, error) {
rs := redsync.New(global.Pool)
tx := global.DB.Begin()
for _, good := range req.GoodsInfo {
var inv model.Inventory
mutex := rs.NewMutex(fmt.Sprintf("goods_%d", good.GoodsID), redsync.WithExpiry(6*time.Second))
if err := mutex.Lock(); err != nil {
return nil, status.Errorf(codes.Internal, "获取分布式锁异常")
}
if result := global.DB.Where(&model.Inventory{Goods: good.GoodsID}).First(&inv); result.RowsAffected < 1 {
tx.Rollback()
return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("参数错误商品:%d不存在", good.GoodsID))
}
if inv.Stocks < good.Num {
tx.Rollback()
return nil, status.Errorf(codes.ResourceExhausted, fmt.Sprintf("商品:%d库存不足", good.GoodsID))
}
inv.Stocks -= good.Num
tx.Save(&inv)
if ok, err := mutex.Unlock(); !ok || err != nil {
return nil, status.Errorf(codes.Internal, "释放分布式锁异常")
}
}
tx.Commit()
return &emptypb.Empty{}, nil
}