[代码学习]Redis分布式锁Go语言实现

  • 加锁部分
  • 释放锁部分


之前使用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上下文信息,上下文信息中会定义链路运行的超时时间,超时时间肯定是比锁的过期时间还要短的,所以不存在线程在锁过期时间内执行不完的情况。所以用不上看门狗机制