概述

在集群环境下,单机的锁类似java的synchronize关键字,其他锁实现,如reentrantLock,以及其他的并发工具,比如cycleBarrier,countDownLatch,samphore等无法在保证并发的安全性,需要引入分布式环境下的锁,以保证集群多个服务实例能够正确同步,分布式锁目前我已知的是基于redis的和基于zk的以及基于数据库的。谷歌的分布式锁服务 Chubby 。这里介绍基于redis实现的。

单机情况下

实现

基于redis的命令setnx和expire,但是无法保证原子性,比如说当redis执行完setnx的时候,服务挂掉了,则会导致expire不执行而形成死锁。redis在2.8版本中加入了set指令的扩展参数(也可以基于lua脚本),可以使得setnx和expire命令一起执行。

  1. >set lockkey true ex 5 nx
  2. OK
  3. do something critical...
  4. >del lockkey

超时问题

redis的分布式锁不能解决超时问题,如果在加锁和解锁之间的逻辑执行太长,超过了锁的超时限制,就会出现问题,因为第一个获取锁的线程持有的锁已经过期了,但是临界区的逻辑没有执行完,但是此时第二个线程已经从新持有了锁,所以临界区的代码并不是严格的串行执行。
一种稍微安全的方法时在加锁是,set命令的value可以为一个随机数,这样可以保证只有持有锁的线程才能释放自己的锁。伪代码如下:

tag = random.nextint();
if redis.set(key, tag, nx = true, ex = 5);
    do_something();
  redis.delifequals(key, tag);

由于匹配value和删除key不是一个院子操作,所以需要借助lua脚本来处理,因为lua脚本可以保证连续多个指令的原子执行
#delifequals
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

需要注意的是,这不是一个完美的方案,只是相对第一种安全了,但是如果锁超时自动释放了而逻辑没有执行完,依然会出问题。

可重入性

redisson提供了具体实现,简易版本也可以通过threadlocal来实现,代码可以参看书本22页。

集群环境下

集群状态下,比如主从结构,一条线程在主节点加锁完成,之后,主节点挂掉了,从节点晋升为主节点,但是新的主节点内部没有这个锁,那么另外一条线程去新的主节点申请加锁就被批准了,这种显然是不允许的。基于这个问题,出现了redlock算法,也有具体的三方包实现Redisson。
redlock需要提供更多的redis实例来实现,这些实例之间相互独立,没有主从关系,redlock算法也是基于大多数机制。加锁时,它会向过半节点发送set(key, value, nx = true, ex = xxx)指令,只要过半节点set成功,就认为加锁成功,释放锁时,需要向全部节点发送del指令。同时使用时需要清楚地是由于需要考虑失败重试,时钟漂移等问题,redlock需要向多个节点读写,性能较单机时会下降。

redis锁的超时自动释放

首先思考一个场景,机器A设置了redis的分布式锁A,并设置过期时间为5s,正常情况下,业务逻辑在5s内是可以执行完的,但是这次由于一些原因,比如说网络分区或者业务数据量剧增,导致业务逻辑执行缓慢,5s内业务没有执行完成,那么,锁自动释放了,导致机器B获取到了锁,这样肯定会导致数据的错误,那么这个时间怎么动态的保证安全呢?
一种解决方案是通过后台线程不断轮询锁是否仍被持有,如果持有,那么就对失效时间进行延长,每次只延长一个轮询时间,这样就能保证锁的正确释放。具体的实现图例如下:
redis锁超时时间解决方案.jpg