之前使用Java写Redis一些操作的时候,有比较成熟的框架可以直接调用,比如Redisson等,把一些加Redis分布式锁等一些比较高级的操作都做了封装,使得Java语言使用Redis都非常的方便。但是好像Go语言中使用Redis还是比较原始的,自己搭轮子的状态。
今天阅读项目源码的时候,看到了一段Redis分布式锁Go语言的实现。感觉有一定的学习价值,所以摘抄至此。
这里实现Redis分布式锁的思想是比较常见的,主要为:
加锁部分
1)首先是使用SetNx(SET if Not Exists)的指令,顾名思义就是如果redis中若不存在这个key,就把他加入到redis
2)主要传入的三个参数:key
作为分布式锁的名称或称为标识;expireSec
是设置这把锁的过期时间,主要是防止死锁的发生,当过期之后,就删除这把锁,防止锁删除失败的情况,导致资源一直被锁住;maxWait
是设置等待锁释放,正在自旋的线程的最大等待时间,如果超过这个时间也没有成功获得锁的话,直接退出锁的争抢。
func Lock(ctx context.Context, key string, requestID string, expireSec uint64, maxWait time.Duration) (bool, error)
3)其中如下这段代码,是未获得锁的线程等待20ms之后再尝试枪锁
time.Sleep(20 * time.Millisecond)
释放锁部分
释放锁的话这里实现就比较简单,是直接使用Del指令删除redis中key为指定字符串的那个锁,就结束了。这里即使删除锁失败,之前加锁时候设置了过期时间,再到达过期时间之后,锁也应该可以成功释放吧
代码汇总如下:
- 使用部分
// AddCount ...
func (o *RImpl) AddCount (ctx context.Context,
uId, cId uint64, rule *db.TRule) error {
err := addCountParamCheck(ctx, rule)
if err != nil {
return err
}
//1.锁uid+ruleId
lockKey := getLockKey(userId, rule.RuleId)
ok, err := redis.Lock(ctx,
lockKey, ctx.requestID, conf.LockExpireSec, conf.LockMaxWaitTime)
if err != nil {
return err
}
if !ok {
return xerr.New(404,
"AddCount failed, get Lock timeout")
}
defer redis.Unlock(ctx, lockKey, ctx.requestID)
.....
}
它这里的key是lockKey := getLockKey(userId, rule.RuleId)
,userId和ruleId拼起来。在我看来,是相当于只锁住那些要对userId和RuleId这部分进行修改的线程,不是锁住整个Redis
- 加锁,释放锁部分
// Lock ...
func Lock(ctx context.Context, key string, requestID string, expireSec uint64, maxWait time.Duration) (bool, error) {
for startTime := time.Now(); time.Since(startTime) < maxWait; {
ok, err := SetNx(ctx, proxy, key, requestID, expireSec)
if err != nil {
log.WarnContext("Lock failed", "key", key)
return false, err
}
if ok {
return ok, nil
}
time.Sleep(20 * time.Millisecond)
}
log.WarnContext(ctx, "redis get Lock timeout", "key", key)
return false, nil
}
// Unlock ...
func Unlock(ctx context.Context, key string, requestID string) error {
_, err := Del(ctx, proxy, key, requestID)
if err != nil {
log.WarnContext(ctx, "Unlock failed", "key", key)
return err
}
return nil
}
// SetNx ...
func SetNx(ctx context.Context, proxy redis.Client, key string, value string, seconds uint64) (bool, error) {
rsp, err := redis.String(proxy.Do(ctx, "SET", key, value, "EX", seconds, "NX"))
if err != nil && err != redis.ErrNil {
log.ErrorContext(ctx, "SetNx err", "err", err.Error())
return false, "SetNx err")
}
log.DebugContext(ctx, "redis SetNx", "key", key, "seconds", seconds, "rsp", rsp)
if rsp != "OK" {
log.DebugContext(ctx, "SetNx fail", "key", key)
return false, nil
}
return true, nil
}
不足之处:
1)可重入锁
可重入,就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁
这里实现的Redis锁,只是在加锁和解锁的过程中增删一个key值,并不能实现可重入锁机制
例如Redisson中的方法,一般可重入锁的实现,主要是设置一个key-value键值对,key还是原来的锁的标识,value表示加锁的次数,如果某个线程想要多次获得这把锁,可以是value值+1,同时在释放一次琐时-1。当value值为0时,就表示这把锁的完全释放,可由别的线程来获取
所以这里想要实现可重入锁修改还是比较方便的
2)看门狗机制
如果没有看门狗机制,redis分布式锁就无法自动续期。比如,一个锁设置了1分钟超时释放,如果拿到这个锁的线程在一分钟内没有执行完毕,那么这个锁就会被其他线程拿到,这种情况可能会导致一些问题的发生
Redisson框架中应用的看门狗机制,就是提供分布式锁的自动续期。如果线程在一定时间内仍旧没有执行完,那么redisson会自动给redis中的目标key延长超时时间
但是,
经过询问得,其实在业务代码中不需要看门狗机制。因为在方法调用过程中会携带context
上下文信息,上下文信息中会定义链路运行的超时时间,超时时间肯定是比锁的过期时间还要短的,所以不存在线程在锁过期时间内执行不完的情况。所以用不上看门狗机制