Redisson

文章目录


一、基于Redis实现分布式锁的问题

用 SET NX EX + Lua 脚本实现的分布式锁,能满足基本的互斥需求,但在生产环境中仍存在四个典型缺陷:

  • 不可重入:同一个线程无法多次获取同一把锁。
  • 不可重试:获取锁失败后,只能立刻返回 false,无法自旋等待。
  • 超时释放:业务执行时间如果超过锁的过期时间,锁会被自动释放,导致并发安全问题。
  • 主从一致性:如果 Redis 主节点宕机,锁数据尚未同步到从节点,其他线程可能重新获取到锁。

Redisson 作为成熟的 Redis 分布式锁框架,针对这些问题提供了完善的解决方案。

二、Redisson的使用

  1. 引入依赖:
html 复制代码
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.6</version>
</dependency>
  1. 配置Redisson客户端:
java 复制代码
@Configuration
public class RedisConfig {
    @Bean
    public RedissonClient redissonClient() {
        // 配置类
        Config config = new Config();
        // 添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址
        config.useSingleServer().setAddress("redis://192.168.150.101:6379").setPassword("123321");
        // 创建客户端
        return Redisson.create(config);
    }
}
  1. 使用 Redission 的分布式锁
java 复制代码
@Resource
private RedissonClient redissonClient;

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

三、Redisson可重入的原理

存储数据结构变为Hash,field存储线程唯一标识,value存储重入次数

Redisson 的可重入锁参考了 JUC 中 ReentrantLock 的设计,通过 state 计数器 记录锁的重入次数:state = 0 表示锁空闲,state ≥ 1 表示已被线程持有,state > 1 表示线程多次重入。

Redis 中使用 Hash 数据结构来存储锁信息;key 表示锁的名称;field 表示线程唯一标识(UUID + 线程ID);Value表示重入次数

  • 获取锁的流程:若锁不存在,则说明当前锁空闲,直接获取锁。锁已存在,则说明有线程正在持有锁,于是判断锁是否被自己持有,是自己的锁则重入,不是则获取锁失败。
  • 释放锁的流程:判断锁是否被自己持有,若是自己的锁,重入次数减 1,再判断重入次数是否为 0,若value> 0说明线程仍有未完成的重入层次;若 value== 0 说明所有重入层次都已执行完毕,可以完全释放锁,删除锁del key。

获取锁的 Lua 脚本

lua 复制代码
local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
-- 判断是否存在
if(redis.call('exists', key) == 0) then
    -- 不存在,获取锁
    redis.call('hset', key, threadId, '1');
    -- 设置有效期
    redis.call('expire', key, releaseTime);
    return 1; -- 返回结果
end;
-- 锁已经存在,判断threadId是否是自己
if(redis.call('hexists', key, threadId) == 1) then
    -- 不存在,获取锁,重入次数+1
    redis.call('hincrby', key, threadId, '1');
    -- 设置有效期
    redis.call('expire', key, releaseTime);
    return 1; -- 返回结果
end;
return 0; -- 代码走到这里,说明获取锁的不是自己,获取锁失败

释放锁的 Lua 脚本

lua 复制代码
local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
-- 判断当前锁是否还是被自己持有
if (redis.call('HEXISTS', key, threadId) == 0) then
    return nil; -- 如果已经不是自己,则直接返回
end
-- 是自己的锁,则重入次数-1
local count = redis.call('HINCRBY', key, threadId, -1);
-- 判断是否重入次数是否已经为0
if (count > 0) then
    -- 大于0说明不能释放锁,重置有效期然后返回
    redis.call('EXPIRE', key, releaseTime);
    return nil;
else  -- 等于0说明可以释放锁,直接删除
    redis.call('DEL', key);
    return nil;
end

四、Redisson的可重试原理(PubSub机制)

获取锁失败的时候会调用 subscribe 方法订阅频道,释放锁的时候会 publish

手动实现的分布式锁,在获取失败时直接返回 false,不具备自旋重试能力。Redisson 的 tryLock 方法支持设置最大等待时间,获取锁失败时不会立即返回,而是利用 Redis 的 Pub/Sub(发布订阅) 机制等待唤醒。

  • 线程 A 尝试获取锁失败后,会订阅一个与该锁相关的频道(channel),然后进入阻塞等待。
  • 当持有锁的线程 B 释放锁时,会向该频道发布一条消息,通知所有等待的线程锁已经被释放。
  • 线程 A 收到通知后,被唤醒,立即再次尝试获取锁;如果仍然失败,则继续等待,直到超时。

RedissonLock 的 tryAcquireAsync 返回 Future<Boolean>,如果获取失败,会调用 subscribe 方法订阅频道,并将当前线程挂起在 Semaphore 上。一旦收到释放锁的消息,信号量释放,线程被唤醒重试。

五、Redisson的超时续约原理(WatchDog机制)

未指定超时时间则会开启一个后台定时任务(看门狗),每隔 10 秒检查锁是否仍被当前线程持有,若是则自动将锁的过期时间重置为 30 秒,直到线程主动释放锁或进程终止

锁的超时时间设得太长,可能导致资源长时间被占用;设得太短,业务还没执行完锁就过期。Redisson 通过 Watch Dog(看门狗) 机制,自动为锁延长有效期。

  • 当线程成功获取锁后,如果未显式指定锁的释放时间(即 leaseTime = -1),Redisson 会启动一个定时任务(Watch Dog)。
  • 定时任务每隔 internalLockLeaseTime / 3 秒(默认 internalLockLeaseTime = 30 秒,所以每 10 秒)执行一次。
  • 它执行一段 Lua 脚本,检查当前线程是否仍然持有该锁,如果是,则将锁的过期时间重新设置为 30 秒。
  • 当线程主动释放锁或进程崩溃,Watch Dog 自动停止。

调用 unlock() 、客户端进程崩溃、与 Redis 断连,或线程被中断/终止都会导致Watch Dog 停止,锁会在 30 秒后自动释放
用户无需担心业务执行时间过长导致锁过期;同时,一旦进程意外退出,Watch Dog 停止续期,锁会在 30 秒后自动释放
只要业务线程未执行完毕且未主动释放锁,Watch Dog 每隔 10 秒会用 Lua 脚本检查锁是否属于自己的线程,然后重新设置过期时间为 30 秒,理论上可以无限续期

六、Redisson 的主从一致性原理(MultiLock)

在 Redis 主从架构中,如果主节点写入锁数据后未来得及同步到从节点就宕机,从节点升级为主后,锁数据会丢失,其他线程可能重新获取到锁,造成并发安全问题。

例如下面的例子,Java 应用向 Redis Master 成功写入了锁,此时 Master 突然宕机,而这条锁数据还未同步到 Slave,Sentinel(哨兵)会从 Slave 中选举一个新的 Master,但新的 Master 中并没有 lock 这条数据。另一个 线程向新 Master 申请同一把锁会成功,因为锁数据根本不存在。

Redisson 的 MultiLock 方案正是为了解决这一问题。它不依赖主从复制,而是要求客户端在多个完全独立的 Redis 节点上依次成功获取锁,才算真正获取到锁。

如下图所示,部署多个独立的 Redis 节点(通常为奇数,如 3 个或 5 个),没有主从关系(或者为主节点独立设置从节点以保证高可用)。加锁时,客户端按顺序向每个节点发送 SET NX EX 请求。当其中一个Redis宕机了,新线程请求锁,在第一个Redis Master上请求成功,但当在其他节点上获取失败。

七、Redisson分布式锁原理

  • 可重入:利用 Redis Hash 结构存储线程标识和重入次数,通过 Lua 脚本原子加减。
  • 可重试:利用 Redis Pub/Sub 发布订阅 + Java Semaphore 实现等待唤醒。
  • 超时续约:Watch Dog 机制,后台定时任务每隔 10 秒延长锁过期时间(默认总时长 30 秒)。
  • 主从一致性:MultiLock机制,在多个独立 Redis 节点上同时加锁,打包成一个"组合锁"。
相关推荐
网络工程小王1 小时前
【LCEL 链式调用详解】调用篇-2
java·服务器·前端·数据库·人工智能
道法自然,人法天2 小时前
PostgreSQL安装与初始化教程(二进制压缩包)
数据库·postgresql
yzs873 小时前
从Hydra到storage_engine:PostgreSQL列存引擎的性能跃迁与技术进化
数据库·postgresql
红云梦3 小时前
官方 Anthropic Postgres MCP Server 存在 SQL 注入漏洞 -- SafeDB 是如何做到 4 层防御的
数据库·sql
TDengine (老段)3 小时前
红有软件重构智能油田时序数据底座,支撑生产实时感知与设备预测性维护
大数据·数据库·人工智能·重构·时序数据库·tdengine
倒霉蛋小马3 小时前
【Redis】什么是缓存击穿?
数据库·redis·缓存
Jing_jing_X4 小时前
MCP (一)是什么?一文讲清 AI 如何连接现实世界
数据库·人工智能·oracle
阿凡观察站4 小时前
2026年工程项目管理软件推荐:这5款主流产品值得关注
大数据·数据库·低代码·finebi·简道云
逸Y 仙X4 小时前
文章二十一:ElasticSearch 词项查询与调度查询实战
java·大数据·数据库·elasticsearch·搜索引擎