Redis学习之Redisson实现分布式锁

发布时间 2023-10-12 17:13:42作者: 万事胜意k

Redisson实现分布式锁

Redisson 是 Java 的 Redis 高级客户端,提供了各种现成的分布式工具类便于我们使用 Redis。

官网:https://github.com/redisson/redisson

中文文档:https://github.com/redisson/redisson/wiki/

使用方式:

1)引入独立的Redisson包:

不建议引入 springboot-starter,因为可能会和 springboot 内置的 redis 整合冲突

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.23.1</version>
        </dependency>

2)创建一个Redisson客户端,代码如下:

@Configuration
public class RedissonConfig {
​
    @Bean
    public RedissonClient redissonClient(){
        // 配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379")
            .setPassword("123321");
        // 创建RedissonClient对象
        return Redisson.create(config);
    }
}
​

3)使用Redisson的Lock,代码如下:

@Resource
private RedissionClient redissonClient;
​
@Test
void testRedisson() throws Exception{
    //获取锁(可重入),指定锁的名称
    RLock lock = redissonClient.getLock("anyLock");
    //尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
    boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
    //判断获取锁成功
    if(isLock){
        try{
            System.out.println("执行业务");          
        }finally{
            //释放锁
            lock.unlock();
        }
        
    }  
}

如何实现可重入锁

目的:保证同一个线程可以多次获取同一把锁

解决思路:在锁的 value 中额外保存当前线程获取锁的次数,每次获取锁 +1、释放锁 -1,当次数为 0 时才真正删除 key。

采用hash结构来存储锁信息,如图:

image-20231011212907569

流程如下:

image-20231011212925904

注意:

  1. 所有的判断和操作都需要使用Lua脚本来保证原子性

  2. 每次获取和释放锁时要重置锁的有效期。

获取锁的Lua脚本:

image-20231011213352946

释放锁的Lua脚本:

image-20231011213408607

如何重试获取锁

基于 Redis Pub / Sub 发布订阅机制。如果获取锁失败,则阻塞订阅释放锁的消息;当锁被释放时,会触发推送(告诉其他线程我释放锁啦),然后其他线程再重试获取;如此往复,直到超时。

如何防止锁提前超时释放

基于看门狗机制

总的来说就是默认锁过期时间是30s,而自动续期机制在源码当中就是开启了定时任务,定时间隔是看门狗时间的三分之一,也就是10s,所以就是在业务没有处理完的情况下锁默认每隔10s续期到30s;

需要思考两个问题:

  1. 如何保证同一个锁只注册一个定时任务?

  2. 如何防止无限续期?

要解决这些问题,使用全局 ConcurrentHashMap 来管理锁 => 任务信息,key 为锁的 id,从而保证唯一。当某个锁释放时,从全局 ConcurrentHashMap 中取出定时任务并取消掉,然后把锁的信息从 Map 中删掉即可。

最终,完整的分布式锁流程如下:

image-20231012165830290

如何解决主从一致性问题

为了提高redis的可用性,我们会搭建集群或者主从,现在以主从为例

此时我们去写命令,写在主机上, 主机会将数据同步给从机,但是假设在主机还没有来得及把数据写入到从机去的时候,此时主机宕机,哨兵会发现主机宕机,并且选举一个slave变成master,而此时新的master中实际上并没有锁信息,此时锁信息就已经丢掉了。

image-20231012165932042

可以使用Redisson的MultiLock(联锁)来解决,和核心思想是开启多个Redis 主节点,设置锁时必须在所有主节点都写入成功,才算设置成功。如果出现有一个节点拿不到,都不能算是加锁成功,就保证了加锁的可靠性。

image-20231012170207816

MutiLock 加锁原理

当我们去设置了多个锁时,redission会将多个锁添加到一个集合中,然后用while循环去不停去尝试拿锁,但是会有一个总共的加锁时间,这个时间是用需要加锁的个数 * 1500ms ,假设有3个锁,那么时间就是4500ms,假设在这4500ms内,所有的锁都加锁成功, 那么此时才算是加锁成功,如果在4500ms有线程加锁失败,则会再次去进行重试.

image-20231012170345846

实现 MultiLock 的几个关键:

  1. 遍历所有节点,依次设置锁,并使用列表来记录所有主节点的锁是否设置成功。

  2. 只要有一个节点设置不成功,就要释放所有的锁,从头来过。

  3. 因为不同节点设置锁成功的时间不同,所以在所有锁设置成功后,要统一设置过期时间(但如果 leaseTime = -1 就不用了,因为开启了看门狗机制会自动续期)

  4. 锁释放时间(leaseTime)必须要大于抢锁最大等待时间(waitTime),否则可能出现第一个节点抢到锁,最后一个节点还没抢到锁,之前的锁就已经超时释放了。所以如果指定了 waitTime 和 leaseTime,默认 leaseTime = waitTime * 2。

MultiLock最安全,但成本也是最高的。