setnx
redis 分布式锁使用非常广泛的,来实现对一些共享资源进行互斥访问。
一般使用setnx(set if not exists) 来抢占,del 来释放。
setnx lock 1
... do something ...
del lock
但是这个流程有问题,如果del 调用失败或者异常导致del 没有调用,就会陷入死锁,导致锁永远不能释放。可以想到的一个解决方案就是我们给锁加一个过期时间,如下:
setnx lock 1
expire lock 5
... do something ...
del lock
但是上述流程还是有问题,如果expire 调用失败,还是会陷入死锁。于是我们想到下面代码程序校验的方案来解决可能的死锁问题:
current_ts = time()
lock_ts = redis->get('lock')
if (!lock_ts || current_ts - lock_ts > 5) {
redis->set(lock, current_ts)
redis->expire(lock, 5)
... do something ...
del lock
} else {
return false
}
但是上述方案还是有问题的,原因就在于抢占资源不是原子操作的,当我们的程序访问并发比较高的时候,会有多个访问同时抢占到锁,不能保证互斥性。
那怎么解决呢? 好在Redis 2.8 版本之后,redis 的set 执行增加了扩展参数,使setnx 和expire 可以一起执行,从而解决了上述难题。
SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX]
set lock 1 ex 5 nx
... do something ...
del lock
del误删
上述方案还是会存在误删问题:假如线程A获得锁并且设置超时时间为30秒,某种原因导致线程A执行很慢,超过30秒后线程A锁自动过期,释放了锁,线程B获了锁。随后线程A执行完成,del删除锁,但线程B还未执行完成,实际上线程A删除的是线程B的锁。
解决方案就是通过lua脚本实现一个乐观锁:
线程在加锁的时候,可以给锁加一个版本号或者随机数来区分。
SET lock rand_value EX 300 NX
del锁之前先做判断,通过lua脚本来保证判断和del 两个操作的原子性。
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
其他问题
-
前面这个算法中出现的锁的有效时间(lock validity time),设置成多少合适呢?如果设置太短的话,锁就有可能在客户端完成对于共享资源的访问之前过期,从而失去保护;如果设置太长的话,一旦某个持有锁的客户端释放锁失败,那么就会导致所有其它客户端都无法获取锁,从而长时间内无法正常工作。看来真是个两难的问题。
-
假如Redis节点宕机了,那么所有客户端就都无法获得锁了,服务变得不可用。为了提高可用性,我们可以给这个Redis节点挂一个Slave,当Master节点不可用的时候,系统自动切到Slave上(failover)。但由于Redis的主从复制(replication)是异步的,这可能导致在failover过程中丧失锁的安全性。这是基于单Redis节点的分布式锁无法解决的,而Redlock算法就是为了解决这个问题提出的,是基于多个Redis节点(都是Master)的一种实现。详情可以看下官网介绍:https://redis.io/topics/distlock ,也可以参考一下这篇blog的介绍:http://zhangtielei.com/posts/blog-redlock-reasoning.html
网友评论