likes
comments
collection
share

图解redsync开源包,告诉你分布式锁为什么不仅仅是setnx

作者站长头像
站长
· 阅读数 26

大家好,我是渔夫子。

今天跟大家聊聊redis实现的分布式锁时需要注意的问题。 之前给大家推荐过一个golang版本的redis的分布式锁是redsync,该包也是redis官网推荐使用的。

有读者给我留言说 为什么不能直接使用redis的setnx命令就行,非要用这么一个包呢? 今天我们就深入剖析一下redsync包的实现,看看除了setnx命令外,还做了哪些必要的工作。

redsync是redis官网上的golang版本的分布式锁的实现,权威性自然不用说。下面是根据我自己的理解画的一张redsync设计的简图,

图解redsync开源包,告诉你分布式锁为什么不仅仅是setnx

首先,该包对外暴露了两个接口:LockUnlock。这也是锁最基本的两个操作原语。Lock接口的底层实现是代码中的acquire函数;Unlock接口的底层实现是代码中的release函数

基于redis的setnx,实现分布式锁的互斥性。

通过源代码看到acquire的实现本质上就是setnx的使用。如下:

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
}

图解redsync开源包,告诉你分布式锁为什么不仅仅是setnx

设置过期时间,防死锁

setnx的时候,我们看到还设置了过期时间。过期时间的设置是为了防止死锁的产生图解redsync开源包,告诉你分布式锁为什么不仅仅是setnx

在没有给锁设置过期时间的情况下,死锁的产生一般是因为当一个进程A持有锁后,在执行业务逻辑期间,突然崩溃了,那么该进程锁持有的锁就永远无法释放了。

这时,另外一个进程B再获取锁时,因为进程A没有释放锁,所以一直获取不到。那么这个锁就成了死锁或叫做长生锁。

图解redsync开源包,告诉你分布式锁为什么不仅仅是setnx

设置过期时间还需要注意的一点就是需要保证setnx+expire是原子操作。因为在redis 2.8版本之前,setnx+expire是两个操作;从redis 2.8版本开始,setnx才支持同时设置expire图解redsync开源包,告诉你分布式锁为什么不仅仅是setnx

这个和上面未设置过期时间的场景下产生死锁的原理相似。只不过是在执行了setnx之后,还没来的及执行expire操作,进程就崩溃了。也同样会导致死锁的产生。

value值的随机性+唯一性验证,防误删

我们再来看加锁时setnxvalue值的设置。该value值的产生是通过genValueFunc函数产生的。genValueFunc函数又是在初始化Mutex对象时指定的在genValue函数中产生的,默认是genValue函数。genValue函数的功能是随机生成了一个16字节的序列,然后通过base64进行编码成字符串。如下: 图解redsync开源包,告诉你分布式锁为什么不仅仅是setnx

所以,value是一个随机值。因为随机性也就产生了唯一性或者在一定时间范围内是唯一的其作用就是为了防止被别的进程误删。

图解redsync开源包,告诉你分布式锁为什么不仅仅是setnx

被误删的一个前提是锁的有效期到了,锁被自动释放了。以下是一个产生锁被误删的情景。 假设线程a先获取了锁。当线程a执行完业务要去释放锁的时候,正巧赶上锁的过期时间也到了,这时锁自动被释放。同时,线程b获取了锁。然后线程a又做了释放锁的操作。这时如果是直接删除锁的话,就把线程b的锁给删除掉了。如下: 图解redsync开源包,告诉你分布式锁为什么不仅仅是setnx

所以,在释放锁时,不是简单的对rediskey的删除。而是增加了对value值的校验判断。如下: 图解redsync开源包,告诉你分布式锁为什么不仅仅是setnx

redsync中代码的实现如下: 图解redsync开源包,告诉你分布式锁为什么不仅仅是setnx

预估业务可执行时间,防获取无效锁

redsync的获取锁的代码中,当执行完acuquire函数后,判断是否成功获取锁还有一个时间比较的条件。如下: 图解redsync开源包,告诉你分布式锁为什么不仅仅是setnx

在锁的生命周期内其实是有 获取锁的时间+漂移时间+业务执行时间三部分组成的。 图解redsync开源包,告诉你分布式锁为什么不仅仅是setnx

那么留给业务的执行时间就是:过期时间 - 获取锁的时间 - redis服务器漂移时间 再用 当前时间 + 留给业务的时间 就能推导出业务执行的截止时间。 如果当前时间已经超过了业务运行的截止时间,那么就说明锁已经过期了(比如获取锁的时间过长),就需要释放锁,并返回加锁失败。

重试机制,提高获取锁的效率

redsync包中,还增加了获取锁的重试机制。代码如下: 图解redsync开源包,告诉你分布式锁为什么不仅仅是setnx

那为什么需要重试机制呢? 首先重试增加获取锁的稳定性。在分布式系统中,由于网络延迟等原因,获取锁的操作可能会失败。等待一段时间后再进行重试可以增加系统的稳定性,从而降低系统崩溃的概率。

其次,要防止频繁重试。如果在获取锁时发生错误,立即进行重试可能导致系统频繁重试,从而导致性能下降。因此,在等待一段时间后再进行重试可以减少这种情况的发生。 图解redsync开源包,告诉你分布式锁为什么不仅仅是setnx

多redis节点支持,保证高可用性

redsync包为了保证获取锁的高可用性,还支持了多redis节点。如下代码: 图解redsync开源包,告诉你分布式锁为什么不仅仅是setnx

m.pools是一个redis实例的数组。在实例化Mutex的时候传入的。在获取锁时,依次向所有的redis节点发送加锁请求,当获取锁的redis节点数量超过预先设定的quorum值时(一般为redis总节点的1/2)才算加锁成功。

图解redsync开源包,告诉你分布式锁为什么不仅仅是setnx

总结

通过分析源码,我们了解到redsync包实现了分布式锁的互斥性锁超时释放防误删高可用和高性能的特点。但分布式锁的可重入性并没有实现。但在大多数的场景下也足够用了。

特别说明: 你的关注,是我写下去的最大动力。关注Go学堂 送《100个go常见的错误》pdf文档、经典go学习资料。

转载自:https://juejin.cn/post/7241400783676424247
评论
请登录