【redisson】redisson分布式锁原理分析

为什么要有分布式锁 分布式锁是基于什么延伸出来的?这个可以在博主首页搜索:超卖, 里面通过超卖例子说明分布式锁和传统锁的区别,本文重点阐述的是redisson的api使用及其源码分析。

文章目录

springboot整合redisson

xml 复制代码
    <!-- redisson-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.16.5</version>
        </dependency>
java 复制代码
@Configuration
@EnableCaching
@AutoConfigureBefore(RedisAutoConfiguration.class)
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient() {
        // 创建 Redisson 配置
        Config config = new Config();
        SingleServerConfig singleServerConfig = config.useSingleServer();
        // 配置 Redis 连接地址(假设是单机版 Redis)
        singleServerConfig.setAddress("redis://xx.xx.xx.xxx:6379");
        singleServerConfig.setDatabase(1);
        singleServerConfig.setPassword("xxxxxxx");

        // 创建并返回 RedissonClient
        return Redisson.create(config);
    }


    /**
     * 哨兵模式配置
     */
//    @Bean
//    public RedissonClient createClient() {
//        // 创建配置对象
//        Config config = new Config();
//
//        // 使用 JSON 编解码器
//        config.setCodec(new JsonJacksonCodec());
//
//        // 配置哨兵模式
//        SentinelServersConfig sentinelServersConfig = config.useSentinelServers();
//        // 配置哨兵地址,多个哨兵地址之间用逗号分隔
//        sentinelServersConfig.setSentinelAddresses(Arrays.asList(
//                "redis://xx.xx.xx.xxx:26379", "redis://xx.xx.xx.xxx:26380","redis://xx.xx.xx.xxx:26381"
//        ));
//
//        // 配置主 Redis 实例名称
//        sentinelServersConfig.setMasterName("mymaster");
//
//        // 配置数据库索引(Redis 默认是 0)
//        sentinelServersConfig.setDatabase(0);
//
//        // 配置哨兵认证密码
//        sentinelServersConfig.setPassword("xxxxx");
//
//        /**
//         * 设置连接池的最大连接数、最小连接数等参数(可选)
//         */
//        // 设置连接超时为 10 秒
//        sentinelServersConfig.setConnectTimeout(10000);
//        // 设置操作超时为 3 秒
//        sentinelServersConfig.setTimeout(3000);
//        // 设置重试次数
//        sentinelServersConfig.setRetryAttempts(3);
//        // 设置重试间隔为 1.5 秒
//        sentinelServersConfig.setRetryInterval(1500);
//
//        // 返回 Redisson 客户端
//        return Redisson.create(config);
//    }

}

redisson三个重要参数

leaseTime

  1. leaseTime
    锁自动释放时间(或者理解为锁持有时间,持有30s和30s释放是一个意思),当出现异常 或者没手动释放锁,在到了自动释放时间时,会自动将锁释放

waitTime

  1. waitTime

    等待重试时间,有点像java原生定时器的延迟执行时间,当前线程发现锁已经被其它线程持有了(当前线程获取锁失败,通俗点解释就是 代码已经被上锁了 资源被占用了), 是应该直接重试呢 还是等待一段时间重试呢? 这种选择就是通过waitTime来控制的,当waitTime = 0时,那当前线程会马上重新尝试获取锁;如果waitTime为不为0 例如为30s, 那么当前线程会在30s再重试 尝试获取锁,如果获取成功则继续执行。

    由此也会延伸出一个疑问:如果waitTime时间设置过长或过短 会对锁造成什么影响吗?

    答案是否定的,不会造成死锁,在一般的项目中waitTime只要不是太不合理都问题不大;

    那我们严格分析一下,在高并发的项目中会发生什么:

    如果waitTime过短,那它会多次尝试 造成不必要的资源损耗 ;

    那如果waitTime > leaseTime (假设实际释放时间 = leaseTime )呢? 那可能等着等着已经被其它线程占用了

    (虽然是lua脚本,但是只是脚本执行的时候是原子性的!比如一个脚本设置过期时间为1h 不是这1h原子性操作 而是执行脚本一瞬间是原子操作)

lockWatchdogTimeout

  1. watchDog看门狗,注意是和leaseTime互斥的!可以简单理解为:如果都强制说明了锁释放时间,那么也就不需要锁续命机制了,watchDog的默认时间是30秒,可以在配置Redisson的时候 Config里面自行修改;

    看门狗主要解决的是 我们可能没法很好的评估线程执行完的时间,声明时间很可能导致锁提前释放;redisson的watchDog机制 发现线程没结束 会锁续命,保证线程完全处于加锁状态中。

    这里博主曾陷入一个误区,如果业务始终不完成 例如死循环 锁岂不是一直不释放 就成了死锁?

    答案虽然是肯定的,但是这不是我们该考虑的范畴,业务死循环 有没有锁 都会OOM的 这不应该是我们写出来的业务代码,应该从源头去避免这个问题。

redisson常用api

  1. lock()

    lock是阻塞锁,没获取到锁会一直阻塞等待:

     线程1加锁 ->
     线程2阻塞等待 ->
     线程1释放锁 ->
     线程2加锁 ->
     线程2释放锁
    
  2. tryLock()

    tryLock是异步锁,如果发现获取锁失败 会直接返回false,我们可以手动判断加锁结果 做不同的业务 (比如false中直接return表示不等待 视为失败)

    以上两个api默认都是有watchDog机制的 ,因为没有显式的指定leaseTime。

  3. tryLock(long waitTime, long leaseTime, TimeUnit unit)

    有严谨的同学,希望完全自己掌控时间(我命由我 不由框架),会选择自己定义锁释放的时间,但是定义了leaseTime,watchDog机制就失效了。

    缺点:可能会和延迟双删一样的问题了,锁可能会提前释放,也可能预估不准确。

    优点:保证锁在一定时间内会释放 不会阻塞。

api源码简析

leaseTime和watchDog不能共存的原因

不管是lock还是tryLock,我们不显式声明leaseTime的时候,它内部都是声明-1,

主要是在tryAcquire方法里面判断:

当配置了leaseTime,则以leaseTime为准,如果没配置 ,则会使用watchDog的时间,且会不断续命(并不是只用一次watchDog的时间就释放锁)

我们也可以用demo来验证一下:

锁没有提前释放例子

声明了leaseTime ,watchDog失效的例子

当我们声明leaseTime = 10s , 在10s就会自动将锁释放,等到我们业务代码执行30s后,再去手动释放锁会提示unlock失败 并报错: attempt to unlock lock, not locked by current thread by node id xxx

所以需要在实际业务中,我们手动释放锁要加一个判断
if (lock.isHeldByCurrentThread())

否则会抛异常

(为什么有了自动释放 还需要手动释放呢? 还是那句话 我命由我,自动的时间可能会受一些意想不到的因素影响,例如网络、jvm的gc导致的stw)

lock同步阻塞的原因

lock源码 朴实无华的同步方法

我们可以对比一下tryLock是怎么做的:

只看方法名可能不太够,我们稍微追踪一下源码(过程比较简单 就不一一贴出来了) 追踪几步后可以看到异步执行的代码:

相关推荐
m0_7482571817 分钟前
【Spring】Spring实现加法计算器和用户登录
java·后端·spring
爱是小小的癌17 分钟前
Java-数据结构-链表-高频面试题(1)
java·数据结构·算法·链表
Atlim30 分钟前
maven多模块项目编译一直报Failure to find com.xxx.xxx:xxx-xxx-xxx:pom:1.0-SNAPSHOT in问题
java·开发语言·maven
敲代码养活全家30 分钟前
基于Elasticsearch8的向量检索实现相似图形搜索
java·elasticsearch
程序猿阿伟36 分钟前
《深度学习模型在鸿蒙分布式框架下的跨设备高效之旅》
分布式·深度学习·harmonyos
咔咔库奇37 分钟前
HarmonyOS开发:传参方式
java·华为·harmonyos
ss27341 分钟前
基于SpringBoot实现的保障性住房管理系统
java·spring boot·后端
ccmjga43 分钟前
升级 Spring Boot 3 配置讲解 — JDK 23 会给 SpringBoot 带来什么特性?
java·spring boot·后端·spring·gradle·spring security
福大大架构师每日一题1 小时前
42.2 告警触发trigger模块单点问题和高可用解决方案
java·linux·服务器·prometheus
sg_knight1 小时前
RabbitMQ如何实现队列持久化
分布式·消息队列·rabbitmq·springcloud·持久化