1. 为什么要讲 Redisson
上一篇手写了 Redis 分布式锁,但生产项目里不建议每个团队都维护一套锁实现。分布式锁实现涉及多种工程边界问题,关键复杂度来自锁语义与异常边界处理,比如可重入、续期、释放校验、异常恢复、等待队列、主从切换。
Redisson 是 Java 生态里常见的 Redis 客户端,它对锁、集合、队列、信号量等分布式对象做了封装。最常用的是 RLock。
RLock lock = redissonClient.getLock("lock:order:" + orderId);
lock.lock();
try {
createOrder(orderId);
} finally {
lock.unlock();
}
这段代码比手写版本短,但底层仍然绕不开 Redis 锁的那些边界。
需要先明确两个概念:Redisson RLock 通常是基于单个 Redis 逻辑锁模型的工程封装;RedLock 是多个独立 Redis master 上的 quorum 锁算法。它们解决的问题相关,但不是同一个层次。
2. Redisson 基础配置
单机 Redis 示例:
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setDatabase(0);
return Redisson.create(config);
}
如果项目同时使用 Spring Data Redis,也常见到类似连接配置:
spring:
redis:
host: 127.0.0.1
port: 6379
这类配置不等同于所有 Redisson starter 的专属配置。实际项目要以当前 Spring Boot、Redisson starter 和 Redis 客户端版本为准,Spring Boot 3 项目还要注意 spring.data.redis 这类配置前缀差异,不要只复制网上老版本配置。
3. RLock 的常用写法
阻塞加锁:
RLock lock = redissonClient.getLock("lock:stock:" + skuId);
lock.lock();
try {
deductStock(skuId);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
lock.lock() 没有显式传入 leaseTime 时,会启用 watchdog 自动续期。isHeldByCurrentThread() 的意义是防止 stale unlock:当前线程已经不再持有锁时,不应该再执行释放。
尝试加锁:
RLock lock = redissonClient.getLock("lock:stock:" + skuId);
boolean locked = lock.tryLock(3, 10, TimeUnit.SECONDS);
if (!locked) {
throw new IllegalStateException("system busy");
}
try {
deductStock(skuId);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
tryLock 这个重载会抛 InterruptedException,示例里省略了方法签名。真实代码要么在方法上继续抛出,要么捕获后恢复中断标记。
tryLock(waitTime, leaseTime, unit) 里:
| 参数 | 含义 |
|---|---|
| waitTime | 最多等待多久获取锁 |
| leaseTime | 获取锁后多久自动释放 |
| unit | 时间单位 |
如果传了明确 leaseTime,锁到期会释放。业务执行时间必须小于这个时间,否则有锁提前释放的风险。
waitTime 和 leaseTime 同时影响锁语义:waitTime 决定最多排队多久,leaseTime 决定拿到锁后多久自动释放。排队时间太长会拖慢调用方,租约太短会增加临界区执行过程中锁过期的风险。
4. 看门狗机制
Redisson 的 watchdog 用来解决业务执行时间不确定的问题。
如果调用:
lock.lock();
没有传 leaseTime,Redisson 会给锁设置默认过期时间,并通过内部定时任务在当前客户端仍持有锁时自动续期。默认锁看门狗超时时间可以通过 lockWatchdogTimeout 配置,常见默认值是 30 秒。
如果调用:
lock.lock(10, TimeUnit.SECONDS);
传了明确租约时间,Redisson 不会按 watchdog 无限续期,锁会在租约时间后自动释放。
这点很容易说错:不是所有 Redisson 加锁都会自动续期,要看调用方式。
watchdog 也不是绝对安全网。如果 JVM 发生长时间 GC pause、机器暂停、网络抖动或 Redis 短暂不可用,续期任务可能延迟,锁仍然可能过期。锁过期后旧持有者继续执行业务,就会进入并发执行窗口。
5. Redisson 锁底层思路
Redisson 的可重入锁会使用 Hash 结构记录持有线程和重入次数,逻辑和上一篇手写版本类似:
lock:order:1
field = clientId:threadId
value = count
加锁时:
- 如果锁不存在,创建 Hash,设置重入次数为 1,并设置过期时间。
- 如果锁存在且 field 是当前线程,重入次数加 1,刷新过期时间。
- 如果锁被其他线程持有,返回剩余 TTL,并等待通知或重试。
释放时:
- 校验当前线程是否持有锁。
- 重入次数减 1。
- 如果减到 0,删除锁并发布通知。
这里的 clientId:threadId 是唯一 owner 标识。每次重入都会刷新 TTL,避免嵌套调用期间锁提前过期。释放锁时 Redisson 底层通过 Lua 保证“校验持有者”和“递减 / 删除”这组动作原子执行,避免 stale client 误删新锁。
6. 主从切换带来的问题
单 Redis 实例加锁有一个典型风险:
客户端 A 在 master 获取锁
master 还没同步给 replica 就宕机
replica 被提升为新 master
客户端 B 在新 master 又获取同一把锁
这会导致同一时刻 A 和 B 都认为自己持有锁。
Redis 主从复制默认是异步复制,所以普通手写 Redis 锁不能承诺在主从切换时仍然绝对互斥。Redisson 在部分部署模式和配置下可以等待从节点同步来缩小窗口,但这仍然不是强一致锁;如果锁保护的是外部系统写入,仍然要考虑 fencing token 或业务幂等。
这不是某个客户端独有的问题,而是 Redis 锁模型和部署拓扑共同决定的边界。
更直白地说,replication lag 会带来双持锁窗口。Redis 锁更接近 best-effort mutual exclusion,不是线性一致锁。如果下游资源支持版本校验,fencing token 是更可靠的保护:旧锁持有者即使恢复执行,也会因为 token 过旧而被拒绝写入。
7. RedLock 是什么
RedLock 是 Redis 作者提出的一种分布式锁算法,核心思想是使用多个相互独立的 Redis master,而不是一个 master 加多个 replica。
假设有 5 个独立 Redis 节点:
redis-1
redis-2
redis-3
redis-4
redis-5
客户端获取锁时:
- 记录开始时间。
- 依次向多个 Redis 节点使用相同 key、相同 value、相同 TTL 加锁。
- 只有拿到多数派,比如 5 个节点里至少 3 个成功,才算成功。
- 总耗时必须小于锁有效期,并给时钟漂移和网络延迟留余量。
- 如果失败,要向所有节点尝试释放锁,包括已经获取成功的节点。

RedLock 通常使用奇数个节点,是为了更容易形成多数派。比如 N = 5 时,至少 3 个节点加锁成功才算成功,也就是 P > N / 2;如果允许失败 X 个节点,常见规模可以按 N = 2X + 1 估算。这里的重点不是“奇数节点一定强一致”,而是 RedLock 的判断条件依赖多数派和剩余有效时间。
剩余有效时间还要扣除加锁总耗时和 time drift。网络抖动时,timeout 不一定等价于加锁失败;客户端可能不知道某个节点上的写入到底是否已经成功,所以失败释放必须尽力清理所有节点。
8. RedLock 的使用边界
RedLock 有几个关键前提:
| 前提 | 说明 |
|---|---|
| 节点独立 | 多个 Redis master 不能是同一套主从里的副本 |
| 多数派成功 | 必须超过半数节点加锁成功 |
| 时间有效 | 获取锁耗时要小于 TTL,且要考虑时钟和网络延迟 |
| 失败释放 | 获取失败时清理已加锁节点 |
RedLock 在业界有争议。争议点主要在网络分区、时钟、暂停时间和锁语义到底能否满足严格正确性。从 CAP 视角看,网络分区发生时,如果系统仍然希望保持可用,就很难同时提供严格线性一致的锁语义。
我的工程建议是:
- 普通业务互斥,用 Redisson RLock,并接受 Redis 锁边界。
- 对正确性要求极高的场景,用数据库事务、唯一约束、乐观锁、ZooKeeper、etcd 等更合适的机制。
- 账务、支付、资金流水这类系统,不建议把 Redis 锁作为最终正确性基础。
- 不要因为听起来高级,就默认上 RedLock。它需要多套独立 Redis master,运维复杂度、监控成本和故障排查难度都会显著增加。
9. Redisson 和 RedLock 怎么选
| 场景 | 建议 |
|---|---|
| 缓存重建互斥 | Redisson RLock |
| 防重复提交 | Redisson RLock 或 token 机制 |
| 定时任务单实例 | Redisson RLock,也可用调度平台能力 |
| 秒杀库存扣减 | Redis 预扣可以做,但最终正确性要落 DB 或库存服务 |
| 金额、账务、支付 | 不建议只靠 Redis 锁 |
| 多机房强一致锁 | 优先看 etcd、ZooKeeper 或专门一致性系统 |
锁只是并发控制的一层。真正的数据正确性通常还需要数据库唯一索引、版本号、事务和幂等表一起兜底。
如果锁保护的是外部存储写入,并且外部存储能校验单调递增 token,可以优先评估 Redisson 的 RFencedLock。它比“我以为我还持有锁”更可靠,因为下游资源可以拒绝旧 token 的写入。
也可以把锁和 CAS / versioning 结合起来:Redis 锁只负责降低并发概率,数据库版本号或 fencing token 负责最终拒绝旧请求。这样比单独依赖 Redis 锁更稳。
10. 常见错误写法
| 错误 | 问题 |
|---|---|
| lock.lock() 后没有 finally unlock | 异常时锁只能等过期 |
| 释放前不判断当前线程 | 可能抛异常或误释放 |
| 临界区里做长 RPC | 锁时间不可控 |
| leaseTime 设置太短 | 业务没执行完锁已释放 |
| 认为 Redisson 等于强一致 | Redis 异步复制边界仍然存在 |
| Redis 重启后仍假设锁存在 | 锁状态可能丢失,业务必须能容忍重试 |
推荐模板:
RLock lock = redissonClient.getLock(lockKey);
boolean locked = lock.tryLock(2, 15, TimeUnit.SECONDS);
if (!locked) {
return;
}
try {
doBusiness();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
这段代码同样需要处理 InterruptedException。
这个模板不能替你解决所有一致性问题,但至少把锁释放、租约时间和当前线程校验几个基本点守住了。
最后可以按两组结论记:
工程建议:
- 普通短临界区优先用 Redisson RLock。
- 明确 leaseTime 会禁用 watchdog。
- 解锁前保留 isHeldByCurrentThread() 判断。
- 高正确性场景必须用 DB 约束、版本号、幂等表或一致性系统兜底。
风险总结:
- Redis 锁不是强一致锁,只是 best-effort mutual exclusion。
- 主从切换、GC pause、网络分区、Redis 重启都可能破坏锁语义。
- RedLock 是 multi-master quorum model,但仍存在争议和运维复杂度。
- fencing token 是防旧持锁者写外部系统的重要手段。