✪ 4.1.5 重入锁的逻辑
存在对应的锁,就对对应的hash结构的value直接+1,和Java重入锁的逻辑是一致的。
4.2 RedLock解决非单体项目的Redis主从架构的锁失效
查看Redis官方文档,对于单节点的Redis ,使用setnx和lua del删除分布式锁是足够的,但是主从架构的场景下:锁先加在一个master节点上,默认是异步同步到从节点,此时master挂了会选择slave为master,此时又可以加锁,就会导致超卖。但是如果使用zookeeper来实现的话,由于zk是CP的,所以CP不存在这样的问题。
Redis文档中给出了RedLock的解决办法,使用redLock真的可以解决吗?
✪ 4.2.1 RedLock 原理
基于客户端的实现,是基于多个独立的Redis Master节点的一种实现(一般为5)。client依次向各个节点申请锁,若能从多数个节点中申请锁成功并满足一些条件限制,那么client就能获取锁成功。它通过独立的N个Master节点,避免了使用主备异步复制协议的缺陷,只要多数Redis节点正常就能正常工作,显著提升了分布式锁的安全性、可用性。
注意图中所有的节点都是master节点。加锁超过半数成功,就认为是成功。具体流程:
- 获取锁
- 获取当前时间T1,作为后续的计时依据;
- 按顺序地,依次向5个独立的节点来尝试获取锁 SET resource_name my_random_value NX PX 30000;
- 计算获取锁总共花了多少时间,判断获取锁成功与否;
- 时间:T2-T1;
- 多数节点的锁(N/2+1);
- 当获取锁成功后的有效时间,要从初始的时间减去第三步算出来的消耗时间;
- 如果没能获取锁成功,尽快释放掉锁。
- 释放锁
- 向所有节点发起释放锁的操作,不管这些节点有没有成功设置过。
public String redlock() { String lockKey = "product_001"; //这里需要自己实例化不同redis实例的redisson客户端连接,这里只是伪代码用一个redisson客户端简化了 RLock lock1 = redisson.getLock(lockKey); RLock lock2 = redisson.getLock(lockKey); RLock lock3 = redisson.getLock(lockKey); /** * 根据多个 RLock 对象构建 RedissonRedLock (最核心的差别就在这里) */ RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3); try { /** * waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败 * leaseTime 锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完) */ boolean res = redLock.tryLock(10, 30, TimeUnit.SECONDS); if (res) { //成功获得锁,在这里处理业务 } } catch (Exception e) { throw new RuntimeException("lock fail"); } finally { //无论如何, 最后都要解锁 redLock.unlock(); } return "end"; }
但是,它的实现建立在一个不安全的系统模型上的,它依赖系统时间,当时钟发生跳跃时,也可能会出现安全性问题。分布式存储专家Martin对RedLock的分析文章,Redis作者的也专门写了一篇文章进行了反驳。
Martin Kleppmann:How to do distributed locking
Antirez:Is Redlock safe?
✪ 4.2.2 RedLock 问题一:持久化机制导致重复加锁
如果是上面的架构图,一般生产都不会配置AOF的每一条命令都落磁盘,一般会设置一些间隔时间,比如1s,如果ABC节点加锁成功,有一个节点C恰好是在1s内加锁,还没有落盘,此时挂了,就会导致其他客户端通过CDE又会加锁成功。
✪ 4.2.3 RedLock 问题二:主从下重复加锁
除非多部署一些节点,但是这样会导致加锁时间变长,这样比较下来效果就不如zk了。
✪ 4.2.4 RedLock 问题三:时钟跳跃导致重复加锁
C节点发生了时钟跳跃,导致加上的锁没有到达实际的超时时间,就被误以为超时而释放,此时其他客户端就可以重复加锁了。
4.3 Curator
✪ InterProcessMutex 可重入锁的分析