redsync集成到库存服务中
// 库存扣减 本地事务(购物车)
// 数据库基本的一个应用场景:数据库事务(数据的一致性)
// 并发情况下,可能会出现超卖,出现数据不一致的问题--》解决方法:加锁,分布式锁
//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 {
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
}
NewMutex方法,对Mutex实例m进行初始化
// NewMutex returns a new distributed mutex with given name.
func (r *Redsync) NewMutex(name string, options ...Option) *Mutex {
m := &Mutex{
name: name,
expiry: 8 * time.Second,
tries: 32,
delayFunc: func(tries int) time.Duration {
return time.Duration(rand.Intn(maxRetryDelayMilliSec-minRetryDelayMilliSec)+minRetryDelayMilliSec) * time.Millisecond
},
genValueFunc: genValue,
driftFactor: 0.01,
timeoutFactor: 0.05,
quorum: len(r.pools)/2 + 1,
pools: r.pools,
}
for _, o := range options {
o.Apply(m)
}
if m.shuffle {
randomPools(m.pools)
}
return m
}
redis setnx在mutex.lock()方法中
// Lock locks m. In case it returns an error on failure, you may retry to acquire the lock by calling this method again.
func (m *Mutex) Lock() error {
return m.LockContext(context.Background())
}
// LockContext locks m. In case it returns an error on failure, you may retry to acquire the lock by calling this method again.
func (m *Mutex) LockContext(ctx context.Context) error {
return m.lockContext(ctx, m.tries)
}
// lockContext locks m. In case it returns an error on failure, you may retry to acquire the lock by calling this method again.
func (m *Mutex) lockContext(ctx context.Context, tries int) error {
if ctx == nil {
ctx = context.Background()
}
value, err := m.genValueFunc()
if err != nil {
return err
}
var timer *time.Timer
for i := 0; i < tries; i++ {
if i != 0 {
if timer == nil {
timer = time.NewTimer(m.delayFunc(i))
} else {
timer.Reset(m.delayFunc(i))
}
select {
case <-ctx.Done():
timer.Stop()
// Exit early if the context is done.
return ErrFailed
case <-timer.C:
// Fall-through when the delay timer completes.
}
}
start := time.Now()
n, err := func() (int, error) {
ctx, cancel := context.WithTimeout(ctx, time.Duration(int64(float64(m.expiry)*m.timeoutFactor)))
defer cancel()
return m.actOnPoolsAsync(func(pool redis.Pool) (bool, error) {
return m.acquire(ctx, pool, value)
})
}()
now := time.Now()
until := now.Add(m.expiry - now.Sub(start) - time.Duration(int64(float64(m.expiry)*m.driftFactor)))
if n >= m.quorum && now.Before(until) {
m.value = value
m.until = until
return nil
}
_, _ = func() (int, error) {
ctx, cancel := context.WithTimeout(ctx, time.Duration(int64(float64(m.expiry)*m.timeoutFactor)))
defer cancel()
return m.actOnPoolsAsync(func(pool redis.Pool) (bool, error) {
return m.release(ctx, pool, value)
})
}()
if i == m.tries-1 && err != nil {
return err
}
}
return ErrFailed
}
acquire方法
func (m *Mutex) acquire(ctx context.Context, pool redis.Pool, value string) (bool, error) {
conn, err := pool.Get(ctx)
if err != nil {
return false, err
}
defer conn.Close()
reply, err := conn.SetNX(m.name, value, m.expiry)
if err != nil {
return false, err
}
return reply, nil
}
setnx的来源:
redlock的原理与源码
原理:
Redis实现分布式锁--Redlock篇_redis redlock-CSDN博客
源码:
func (m *Mutex) actOnPoolsAsync(actFn func(redis.Pool) (bool, error)) (int, error) {
type result struct {
node int
statusOK bool
err error
}
ch := make(chan result, len(m.pools))
for node, pool := range m.pools {
go func(node int, pool redis.Pool) {
r := result{node: node}
r.statusOK, r.err = actFn(pool)
ch <- r
}(node, pool)
}
var (
n = 0
taken []int
err error
)
for range m.pools {
r := <-ch
if r.statusOK {
n++
} else if r.err == ErrLockAlreadyExpired {
err = multierror.Append(err, ErrLockAlreadyExpired)
} else if r.err != nil {
err = multierror.Append(err, &RedisError{Node: r.node, Err: r.err})
} else {
taken = append(taken, r.node)
err = multierror.Append(err, &ErrNodeTaken{Node: r.node})
}
if m.failFast {
// fast retrun
if n >= m.quorum {
return n, err
}
// fail fast
if len(taken) >= m.quorum {
return n, &ErrTaken{Nodes: taken}
}
}
}
if len(taken) >= m.quorum {
return n, &ErrTaken{Nodes: taken}
}
return n, err
}
如果你设置的过期时间比mutex设置的默认过期时间(8s)长,那么想要你设置的过期时间能生效的话,需要手动开启以下方法:
// Extend resets the mutex's expiry and returns the status of expiry extension.
func (m *Mutex) Extend() (bool, error) {
return m.ExtendContext(context.Background())
}
// ExtendContext resets the mutex's expiry and returns the status of expiry extension.
func (m *Mutex) ExtendContext(ctx context.Context) (bool, error) {
start := time.Now()
n, err := m.actOnPoolsAsync(func(pool redis.Pool) (bool, error) {
return m.touch(ctx, pool, m.value, int(m.expiry/time.Millisecond))
})
if n < m.quorum {
return false, err
}
now := time.Now()
until := now.Add(m.expiry - now.Sub(start) - time.Duration(int64(float64(m.expiry)*m.driftFactor)))
if now.Before(until) {
m.until = until
return true, nil
}
return false, ErrExtendFailed
}