Redis之分布式锁

# 基于 Redis 实现分布式锁的三种方案

  • Redis 实现分布式锁的正确姿势(实现一)
  • Redisson 实现分布式可重入锁(RedissonLock)(实现二)
  • Redisson 实现分布式锁(红锁 RedissonRedLock)(实现三)

# 分布式锁需满足四个条件

首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

  1. 互斥性。在任意时刻,只有一个客户端能持有锁。(也就是他的一个唯一性)
  2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  3. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了,即不能误解锁。
  4. 具有容错性。只要大多数Redis节点正常运行,客户端就能够获取和释放锁。

# 用 Redis 实现分布式锁的正确姿势(实现一)

主要思路

通过 set key value px milliseconds nx 命令实现加锁(这个命令的话,就是将两条执行语句合并成一条,实现了原子性)(判断这个锁是否存在,如果没有的话,才可以被抢到设置),对于未抢到锁的,则包在一个死循环中,一直进行一个抢锁的过程; 通过Lua脚本实现解锁。核心实现命令如下:

//获取锁(unique_value可以是UUID等)
SET resource_name unique_value NX PX  30000
 
//释放锁(lua脚本中,一定要比较value,防止误解锁)
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

数据操作过程:

         当主节点服务器出现故障,则需要手动的选取一个从节点,将该从节点设置为主节点,并且将其余的从节点原先主节点信息,设置为最新的主节点信息,这样就完成了主节点的设置

 

这种实现方式主要有以下几个要点:

  • set 命令要用 set key value px milliseconds nx,替代 setnx + expire 需要分两次执行命令的方式,保证了原子性,(抢锁)

  • value 要具有唯一性,可以使用UUID.randomUUID().toString()方法生成,用来标识这把锁是属于哪个请求加的,在解锁的时候就可以有依据;(也就是获取一个唯一标识,也可以认为是你的一个UID)

  • 释放锁时要验证 value 值,防止误解锁;(验证这个锁的UID是否与现有需要进行解锁的线程的UID一致,只有两个一致的情况下,才可以进行解锁;否则是不能被解锁)

  • 通过 Lua 脚本来避免 Check And Set 模型的并发问题,因为在释放锁的时候因为涉及到多个Redis操作 (利用了eval命令执行Lua脚本的原子性);避免两天的操作出现问题

缺点

1、锁不可重入(因为我们进入时,是会判断是否有存在锁,如果有就没有办法抢到锁,但是这个时候,如果是自己再进入,他就判断有存在了,就无法进入了,会一直是一个死循环的情况)

实现二中的tryLockInnerAsync方法,采用Hash数据结构,对锁的重入次数也进行了存储,解决了这个问题

2、如果业务处理时间大于锁的过期时间,锁会过期,其他线程有可能会获取到锁,想要解决这个问题,我们还需要额外的去维护这个过期时间(尽量讲这个过期时间设置长一些,但是有些还是无法判断这个过期时间的长短怎么设置)

实现二中的watch dog会在你获取锁之后,每隔10秒帮你把key的超时时间设为30s ,解决了这个问题

3、使用自旋方式重复进行 CAS 尝试获取锁,内存消耗较大

(因为在前面进行抢锁的过程,是放在一个死循环中,一直重复的去抢锁,这个过程内存消耗的比较大)

实现二中的采用的是订阅发布的模式,订阅锁释放事件,并通过await方法阻塞等待锁释放,有效的解决了无效的锁申请浪费资源的问题

4、因为Redis的主从同步是异步的,所以这种方案下,不论Redis的部署架构采用的是单机模式、主从模式、哨兵模式还是集群模式,都存在锁丢失的风险(当然,这是极端的情况)

实现二,仍未解决该问题

# Redisson 实现分布式可重入锁 (RedissonLock)(实现二)

什么是 Redisson

Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。【Redis官方推荐】

Redisson在基于NIONetty框架上,充分的利用了Redis键值数据库提供的一系列优势,在Java实用工具包中常用接口的基础上,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。

Redisson 分布式重入锁用法

Redisson 支持单点模式、主从模式、哨兵模式、集群模式,这里以单点模式为例:

        // 1.构造redisson实现分布式锁必要的Config
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:5379").setPassword("123456").setDatabase(0);
        // 2.构造RedissonClient
        RedissonClient redissonClient = Redisson.create(config);
        // 3.获取锁对象实例(无法保证是按线程的顺序获取到)
        RLock rLock = redissonClient.getLock(lockKey);
        try {
            /**
             * 4.尝试获取锁
             * waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败
             * leaseTime   锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)
             */
            boolean res = rLock.tryLock((long)waitTimeout, (long)leaseTime, TimeUnit.SECONDS);
            if (res) {
                //成功获得锁,在这里处理业务
            }
        } catch (Exception e) {
            throw new RuntimeException("aquire lock fail");
        }finally{
            //无论如何, 最后都要解锁
            rLock.unlock();
        }

我们只需要通过它的api中的lock和unlock即可完成分布式锁,他帮我们考虑了很多细节:

  • redisson所有指令都通过lua脚本执行,redis支持lua脚本原子性执行
  • redisson会自动将过期时间进行延后:redisson设置一个key的默认过期时间为30s,如果某个客户端持有一个锁超过了30s怎么办? redisson中有一个watchdog的概念,翻译过来就是看门狗,它会在你获取锁之后,每隔10秒帮你把key的超时时间设为30s 这样的话,就算一直持有锁也不会出现key过期了,导致其他线程获取到锁的问题了
  • redisson的“看门狗”逻辑保证了没有死锁发生。 (如果机器宕机了,看门狗也就没了。此时就不会延长key的过期时间,到了30s之后就会自动过期了,其他线程可以获取到锁)

数据处理过程:

        哨兵进行监控,如果出现主节点服务器损坏无法使用,则直接从从节点中进行筛选,(一般来说从节点都是单数)满足过半条件后,就直接将过半节点设置为主节点,修改各个节点中的配置

         并且过期时间将自动进行推后,比如任务需执行5s,但是过期时间设置为3s;在执行时会进行判断,如果没有执行完成,会自动往后再推3s,保证了任务在执行时,不会因为过期时间而丢锁;

 

对比

通过 Redisson 实现分布式可重入锁(实现二),比纯自己通过set key value px milliseconds nx +lua 实现(实现一)的效果更好些,虽然基本原理都一样,但是它帮我们屏蔽了内部的执行细节,不需要我们再去手写获取锁和释放锁的逻辑,直接调用它的lock和unlock方法即可,而且通过分析源码可知,RedissonLock是可重入的,并且考虑了失败重试,可以设置锁的最大等待时间, 在实现上也做了一些优化,减少了无效的锁申请,提升了资源的利用率。

缺点

RedissonLock 同样没有解决 节点挂掉的时候,存在丢失锁的风险的问题。而现实情况是有一些场景无法容忍的,所以 Redisson 提供了实现了redlock算法的 RedissonRedLock,RedissonRedLock 真正解决了单点失败的问题,代价是需要额外的为 RedissonRedLock 搭建Redis环境。所以,如果业务场景可以容忍这种小概率的错误,则推荐使用 RedissonLock, 如果无法容忍,则推荐使用 RedissonRedLock。

# redlock算法

在Redis的分布式环境中,我们假设有N个Redis master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制我们确保将在N个实例上使用与在Redis单实例下相同方法获取和释放锁。现在我们假设有5个Redis master节点,同时我们需要在5台服务器上面运行这些Redis实例,这样保证他们不会同时都宕掉。

# 用 Redisson 实现分布式锁(红锁 RedissonRedLock)及源码分析(实现三)

这里以三个单机模式为例,需要特别注意的是他们完全互相独立,不存在主从复制或者其他集群协调机制。

        也就是从之前的单一集群,变为多个集群,并且多个集群是相互独立,但是里面的数据是一样的;并且进行加锁时,只要超过半数就可直接进行;这样就避免了第二种方式出现的数据丢失的问题,这样能够更大程度的减少了数据丢失的可能性

最核心的变化就是需要构建多个 RLock ,然后根据多个 RLock 构建成一个 RedissonRedLock,因为 redLock 算法是建立在多个互相独立的 Redis 环境之上的(为了区分可以叫为 Redission node),Redission node 节点既可以是单机模式(single),也可以是主从模式(master/salve),哨兵模式(sentinal),或者集群模式(cluster)。这就意味着,不能跟以往一样只搭建 1个 cluster、或 1个 sentinel 集群,或是1套主从架构就了事了,需要为 RedissonRedLock 额外搭建多几套独立的 Redission 节点。 比如可以搭建3个 或者5个 Redission节点,具体可看视资源及业务情况而定。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值