一, 前言
在上一篇文章中, 已经介绍了基于 Redis 实现分布式锁的正确姿势, 但是上篇文章存在一定的缺陷 -- 它加锁只作用在一个 Redis 节点上, 如果通过 sentinel 保证高可用, 如果 master 节点由于某些原因发生了主从切换, 那么就会出现锁丢失的情况:
客户端 1 在 Redis 的 master 节点上拿到了锁
Master 宕机了, 存储锁的 key 还没有来得及同步到 Slave 上
master 故障, 发生故障转移, slave 节点升级为 master 节点
客户端 2 从新的 Master 获取到了对应同一个资源的锁
于是, 客户端 1 和客户端 2 同时持有了同一个资源的锁. 锁的安全性被打破了. 针对这个问题. Redis 作者 antirez 提出了 RedLock 算法来解决这个问题
二, RedLock 算法的实现思路
antirez 提出的 redlock 算法实现思路大概是这样的.
客户端按照下面的步骤来获取锁:
获取当前时间的毫秒数 T1.
按顺序依次向 N 个 Redis 节点执行获取锁的操作. 这个获取锁的操作和上一篇中基于单 Redis 节点获取锁的过程相同. 包括唯一 UUID 作为 Value 以及锁的过期时间(expireTime). 为了保证在某个在某个 Redis 节点不可用的时候算法能够继续运行, 这个获取锁的操作还需要一个超时时间. 它应该远小于锁的过期时间. 客户端向某个 Redis 节点获取锁失败后, 应立即尝试下一个 Redis 节点. 这里失败包括 Redis 节点不可用或者该 Redis 节点上的锁已经被其他客户端持有.
计算整个获取锁过程的总耗时. 即当前时间减去第一步记录的时间. 计算公司为 T2=now()- T1. 如果客户端从大多数 Redis 节点 (>N/2 +1) 成功获取到锁. 并且获取锁总共消耗的时间小于锁的过期时间(即 T2<expireTime). 则认为客户端获取锁成功, 否则, 认为获取锁失败
如果获取锁成功, 需要重新计算锁的过期时间. 它等于最初锁的有效时间减去第三步计算出来获取锁消耗的时间, 即 expireTime - T2
如果最终获取锁失败, 那么客户端立即向所有 Redis 系欸但发起释放锁的操作.(和上一篇释放锁的逻辑一样)
虽然说 RedLock 算法可以解决单点 Redis 分布式锁的安全性问题, 但如果集群中有节点发生崩溃重启, 还是会锁的安全性有影响的. 具体出现问题的场景如下:
假设一共有 5 个 Redis 节点: A, B, C, D, E. 设想发生了如下的事件序列:
客户端 1 成功锁住了 A, B, C, 获取锁成功(但 D 和 E 没有锁住)
节点 C 崩溃重启了, 但客户端 1 在 C 上加的锁没有持久化下来, 丢失了
节点 C 重启后, 客户端 2 锁住了 C, D, E, 获取锁成功
这样, 客户端 1 和客户端 2 同时获得了锁(针对同一资源). 针对这样场景, 解决方式也很简单, 也就是让 Redis 崩溃后延迟重启, 并且这个延迟时间大于锁的过期时间就好. 这样等节点重启后, 所有节点上的锁都已经失效了. 也不存在以上出现 2 个客户端获取同一个资源的情况了.
相比之下, RedLock 安全性和稳定性都比前一篇文章中介绍的实现要好很多, 但要说完全没有问题不是. 例如, 如果客户端获取锁成功后, 如果访问共享资源操作执行时间过长, 导致锁过期了, 后续客户端获取锁成功了, 这样在同一个时刻又出现了 2 个客户端获得了锁的情况. 所以针对分布式锁的应用的时候需要多测试. 服务器台数越多, 出现不可预期的情况也越多. 如果客户端获取锁之后, 在上面第三步发生了 GC 得情况导致 GC 完成后, 锁失效了, 这样同时也使得同一时间有 2 个客户端获得了锁. 如果系统对共享资源有非常严格要求得情况下, 还是建议需要做数据库锁得得方案来补充. 如飞机票或火车票座位得情况. 对于一些抢购获取, 针对偶尔出现超卖, 后续可以人为沟通置换得方式采用分布式锁得方式没什么问题. 因为可以绝大部分保证分布式锁的安全性.
三, 分布式场景下基于 Redis 实现分布式锁的正确姿势
目前 redisson 包已经有对 redlock 算法封装, 接下来就具体看看使用 redisson 包来实现分布式锁的正确姿势.
具体实现代码如下代码所示:
- public interface DistributedLock {
- /**
- * 获取锁
- * @author zhi.li
- * @return 锁标识
- */
- String acquire();
- /**
- * 释放锁
- * @author zhi.li
- * @param indentifier
- * @return
- */
- boolean release(String indentifier);
- }
- public class RedisDistributedRedLock implements DistributedLock {
- /**
- * Redis 客户端
- */
- private RedissonClient redissonClient;
- /**
- * 分布式锁的键值
- */
- private String lockKey;
- private RLock redLock;
- /**
- * 锁的有效时间 10s
- */
- int expireTime = 10 * 1000;
- /**
- * 获取锁的超时时间
- */
- int acquireTimeout = 500;
- public RedisDistributedRedLock(RedissonClient redissonClient, String lockKey) {
- this.redissonClient = redissonClient;
- this.lockKey = lockKey;
- }
- @Override
- public String acquire() {
- redLock = redissonClient.getLock(lockKey);
- boolean isLock;
- try{
- isLock = redLock.tryLock(acquireTimeout, expireTime, TimeUnit.MILLISECONDS);
- if(isLock){
- System.out.println(Thread.currentThread().getName() + "" + lockKey +" 获得了锁 ");
- return null;
- }
- }catch (Exception e){
- e.printStackTrace();
- }
- return null;
- }
- @Override
- public boolean release(String indentifier) {
- if(null != redLock){
- redLock.unlock();
- return true;
- }
- return false;
- }
- }
由于 RedLock 是针对主从和集群场景准备. 上面代码采用哨兵模式. 所以要让上面代码运行起来, 需要先本地搭建 Redis 哨兵模式. 本人的环境是 Windows, 具体 Windows 哨兵环境搭建参考文章: Redis sentinel 部署(Windows 下实现).
具体测试代码如下所示:
- public class RedisDistributedRedLockTest {
- static int n = 5;
- public static void secskill() {
- if(n <= 0) {
- System.out.println("抢购完成");
- return;
- }
- System.out.println(--n);
- }
- public static void main(String[] args) {
- Config config = new Config();
- // 支持单机, 主从, 哨兵, 集群等模式
- // 此为哨兵模式
- config.useSentinelServers()
- .setMasterName("mymaster")
- .addSentinelAddress("127.0.0.1:26369","127.0.0.1:26379","127.0.0.1:26389")
- .setDatabase(0);
- Runnable runnable = () -> {
- RedisDistributedRedLock redisDistributedRedLock = null;
- RedissonClient redissonClient = null;
- try {
- redissonClient = Redisson.create(config);
- redisDistributedRedLock = new RedisDistributedRedLock(redissonClient, "stock_lock");
- redisDistributedRedLock.acquire();
- secskill();
- System.out.println(Thread.currentThread().getName() + "正在运行");
- } finally {
- if (redisDistributedRedLock != null) {
- redisDistributedRedLock.release(null);
- }
- redissonClient.shutdown();
- }
- };
- for (int i = 0; i < 10; i++) {
- Thread t = new Thread(runnable);
- t.start();
- }
- }
具体的运行结果, 如下图所示:
四, 总结
到此, 基于 Redis 实现分布式锁的就告一段落了, 由于分布式锁的实现方式主要有: 数据库锁的方式, 基于 Redis 实现和基于 Zookeeper 实现. 接下来的一篇文章将介绍基于 Zookeeper 分布式锁的正确姿势.
本文所有代码地址: https://github.com/learninghard-lizhi/common-util https://github.com/learninghard-lizhi/common-util
来源: https://www.cnblogs.com/zhili/p/redLock_DistributedLock.html