大家好,我是渔夫子。
今天跟大家聊聊redis实现的分布式锁时需要注意的问题。之前给大家推荐过一个golang版本的redis的分布式锁是redsync
,该包也是redis官网推荐使用的。
有读者给我留言说 为什么不能直接使用redis的setnx
命令就行,非要用这么一个包呢?今天我们就深入剖析一下redsync
包的实现,看看除了setnx
命令外,还做了哪些必要的工作。
redsync是redis官网上的golang版本的分布式锁的实现,权威性自然不用说。下面是根据我自己的理解画的一张redsync设计的简图,
image.png
首先,该包对外暴露了两个接口:Lock
和Unlock
。这也是锁最基本的两个操作原语。Lock接口的底层实现是代码中的acquire函数
;Unlock接口的底层实现是代码中的release函数
。
通过源代码看到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
}
image.png
在setnx
的时候,我们看到还设置了过期时间。过期时间的设置是为了防止死锁的产生。
在没有给锁设置过期时间的情况下,死锁的产生一般是因为当一个进程A持有锁后,在执行业务逻辑期间,突然崩溃了,那么该进程锁持有的锁就永远无法释放了。
这时,另外一个进程B再获取锁时,因为进程A没有释放锁,所以一直获取不到。那么这个锁就成了死锁或叫做长生锁。
image.png
设置过期时间还需要注意的一点就是需要保证setnx+expire是原子操作。因为在redis 2.8版本之前,setnx+expire是两个操作;从redis 2.8版本开始,setnx
才支持同时设置expire
。
这个和上面未设置过期时间的场景下产生死锁的原理相似。只不过是在执行了setnx
之后,还没来的及执行expire
操作,进程就崩溃了。也同样会导致死锁的产生。
我们再来看加锁时setnx
的value
值的设置。该value
值的产生是通过genValueFunc
函数产生的。genValueFunc
函数又是在初始化Mutex
对象时指定的在genValue
函数中产生的,默认是genValue
函数。genValue
函数的功能是随机生成了一个16字节的序列,然后通过base64进行编码成字符串。如下:
所以,value是一个随机值。因为随机性也就产生了唯一性,或者在一定时间范围内是唯一的。其作用就是为了防止被别的进程误删。
image.png
被误删的一个前提是锁的有效期到了,锁被自动释放了。以下是一个产生锁被误删的情景。假设线程a
先获取了锁。当线程a
执行完业务要去释放锁的时候,正巧赶上锁的过期时间也到了,这时锁自动被释放。同时,线程b
获取了锁。然后线程a
又做了释放锁的操作。这时如果是直接删除锁的话,就把线程b的锁给删除掉了。如下:
所以,在释放锁时,不是简单的对redis
的key
的删除。而是增加了对value
值的校验判断。如下:
redsync中代码的实现如下:
在redsync
的获取锁的代码中,当执行完acuquire
函数后,判断是否成功获取锁还有一个时间比较的条件。如下:
在锁的生命周期内其实是有 获取锁的时间+漂移时间+业务执行时间三部分组成的。
那么留给业务的执行时间就是:过期时间 - 获取锁的时间 - redis服务器漂移时间再用 当前时间 + 留给业务的时间 就能推导出业务执行的截止时间。如果当前时间已经超过了业务运行的截止时间,那么就说明锁已经过期了(比如获取锁的时间过长),就需要释放锁,并返回加锁失败。
在redsync
包中,还增加了获取锁的重试机制。代码如下:
那为什么需要重试机制呢?首先重试增加获取锁的稳定性。在分布式系统中,由于网络延迟等原因,获取锁的操作可能会失败。等待一段时间后再进行重试可以增加系统的稳定性,从而降低系统崩溃的概率。
其次,要防止频繁重试。如果在获取锁时发生错误,立即进行重试可能导致系统频繁重试,从而导致性能下降。因此,在等待一段时间后再进行重试可以减少这种情况的发生。
redsync
包为了保证获取锁的高可用性,还支持了多redis
节点。如下代码:
m.pools
是一个redis
实例的数组。在实例化Mutex
的时候传入的。在获取锁时,依次向所有的redis节点发送加锁请求,当获取锁的redis节点数量超过预先设定的quorum�
值时(一般为redis总节点的1/2)才算加锁成功。
image.png
通过分析源码,我们了解到redsync包实现了分布式锁的互斥性、锁超时释放、防误删、高可用和高性能的特点。但分布式锁的可重入性并没有实现。但在大多数的场景下也足够用了。
特别说明:你的关注,是我写下去的最大动力。点击下方公众号卡片,直接关注。关注送《100个go常见的错误》pdf文档、经典go学习资料。