Redisson分布式锁使用指南
Redisson 分布式锁使用指南
本文档介绍 Redisson 中最常用的两种分布式锁:可重入锁(Reentrant Lock) 和 读写锁(ReadWrite Lock),包含原理、使用方式、典型场景及最佳实践。
一、可重入锁(Reentrant Lock)
1.1 原理简介
可重入锁是 Redisson 默认提供的分布式锁,基于 Redis 实现,核心特性:
- 可重入:同一线程可多次获取同一把锁,不会自我阻塞(通过 Hash 结构记录线程 ID 和重入次数)
- 自动续期:内置看门狗(Watchdog)机制,默认每 10 秒续期一次,避免业务未执行完锁就过期
- 原子性:加锁、解锁均通过 Lua 脚本保证原子性
- 防误删:通过唯一线程标识,只有持有者才能释放锁
1.2 底层结构
Redis 中存储结构为 Hash:
Key: 锁名称(如 "lock:order:1001")
Field: 线程标识(UUID:threadId)
Value: 重入次数
加锁 Lua 脚本简化逻辑:
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hset', KEYS[1], ARGV[2], 1); -- 首次加锁
redis.call('pexpire', KEYS[1], ARGV[1]); -- 设置过期时间
return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[2], 1); -- 重入次数 +1
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
return redis.call('pttl', KEYS[1]); -- 锁已被其他线程持有,返回剩余过期时间
1.3 基本用法
方式一:阻塞式加锁(推荐)
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
@Autowired
private RedissonClient redissonClient;
public void createOrder(String userId) {
RLock lock = redissonClient.getLock("lock:order:" + userId);
lock.lock(); // 阻塞直到获取锁,默认 30 秒过期,看门狗自动续期
try {
// 业务逻辑:扣库存、生成订单、扣款
doCreateOrder(userId);
} finally {
// 注意:必须判断是否持有锁,避免解锁别人的锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
private void doCreateOrder(String userId) {
// 实际业务逻辑
}
}
方式二:带超时时间的加锁
public void createOrderWithWait(String userId) {
RLock lock = redissonClient.getLock("lock:order:" + userId);
try {
// tryLock(等待时间, 持有时间, 时间单位)
// 等待 3 秒获取锁,获取后锁持有 10 秒(不会自动续期)
boolean acquired = lock.tryLock(3, 10, TimeUnit.SECONDS);
if (acquired) {
try {
doCreateOrder(userId);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
} else {
// 获取锁失败的处理
throw new RuntimeException("系统繁忙,请稍后重试");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("加锁被中断", e);
}
}
方式三:可重入演示
public void reentrantDemo() {
RLock lock = redissonClient.getLock("lock:demo");
lock.lock();
try {
System.out.println("第一次加锁");
methodA(); // 内部再次加锁,不会阻塞(可重入)
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
private void methodA() {
RLock lock = redisson.getLock("lock:demo"); // 同一把锁
lock.lock();
try {
System.out.println("第二次加锁(重入)");
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
1.4 可重入的价值:为什么需要它
核心区别
可重入锁与普通锁的唯一核心区别:同一线程能否重复获取同一把锁。
| 对比项 | 普通锁(不可重入) | 可重入锁 |
|---|---|---|
| 同线程二次加锁 | ❌ 死锁 | ✅ 成功(计数 +1) |
| 多线程互斥 | ✅ | ✅ |
普通锁的死锁后果
如果用普通锁发生嵌套加锁,会引发连锁反应:
- 当前线程永久阻塞:卡在内部的
lock(),永远等不到锁释放 - 锁永不释放:外层
unlock()永远执行不到 - 其他线程全部阻塞:所有请求这把锁的线程排队等死
- 线程池耗尽:Tomcat 线程池被占满,新请求直接拒绝
- 服务整体不可用:整个接口/服务崩溃
本质:一个线程卡死,整条链路报废。
为什么会嵌套加锁
嵌套加锁不是刻意为之,而是每个层级各自保证安全,叠加后自然形成:
每个方法独立设计时都要保证线程安全
↓
方法 A 加锁 → 调用方法 B
方法 B 自己也加锁(因为 B 也可能被单独调用)
↓
自然形成嵌套加锁
典型嵌套场景:
| 场景 | 说明 |
|---|---|
| 方法复用 | updateProduct() 既能单独调用,也能被 batchUpdate() 批量调用 |
| AOP 切面叠加 | 日志切面、缓存切面、事务切面各自加锁,叠加后多层嵌套 |
| 框架拦截器 | Spring 事务拦截器 + 业务代码加锁 |
| 递归调用 | 树形结构递归处理,每层都加锁 |
可重入带来的好处
- 方法自由组合:不用关心外层是否已加锁,该加就加
- 代码结构清晰:每个方法职责单一,不用把所有逻辑塞一个方法里
- 与框架安全共存:和 Spring 事务、AOP 切面叠加不冲突
- 递归安全:递归调用不用额外处理
一句话总结:可重入锁的核心能力只有一个(同线程可重复加锁),但它解放了代码设计——写代码时不用时刻惦记"我现在有没有锁"。
1.5 看门狗机制说明
| 加锁方式 | 是否触发看门狗 | 过期时间 |
|---|---|---|
lock.lock() |
是 | 默认 30 秒,自动续期 |
lock.lock(10, SECONDS) |
否 | 固定 10 秒,不续期 |
tryLock(3, 10, SECONDS) |
否 | 固定 10 秒,不续期 |
tryLock(3, -1, SECONDS) |
是 | -1 表示触发看门狗 |
看门狗工作机制:
- 加锁成功后,启动定时任务(每隔
锁过期时间 / 3执行一次,默认 10 秒) - 检查当前线程是否仍持有锁
- 如果持有,重新设置过期时间为 30 秒
- 业务执行完成调用
unlock()后,定时任务自动停止
注意:只有在不指定 leaseTime 或 leaseTime = -1 时才会启用看门狗。
1.6 典型应用场景
| 场景 | 锁 Key 设计 | 说明 |
|---|---|---|
| 防止订单重复提交 | lock:submit:{userId} |
同一用户短时间内只能提交一次 |
| 扣减库存 | lock:stock:{skuId} |
防止超卖 |
| 账户余额变更 | lock:account:{accountId} |
防止并发修改余额 |
| 定时任务执行 | lock:job:{jobName} |
集群环境下保证任务只执行一次 |
| 缓存重建 | lock:cache:{cacheKey} |
防止缓存击穿,只允许一个线程重建 |
1.7 最佳实践
/**
* 可重入锁标准模板
*/
public <T> T executeWithLock(String lockKey, long waitTime, long leaseTime, Supplier<T> supplier) {
RLock lock = redissonClient.getLock(lockKey);
boolean acquired = false;
try {
acquired = lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
if (!acquired) {
throw new RuntimeException("获取锁失败: " + lockKey);
}
return supplier.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("加锁被中断", e);
} finally {
if (acquired && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
注意事项:
unlock()前必须判断isHeldByCurrentThread(),否则可能抛IllegalMonitorStateException- 业务执行时间不确定时,使用
lock.lock()触发看门狗自动续期 - 业务执行时间可控时,使用
tryLock(wait, lease, unit)指定固定过期时间,避免看门狗开销 - 锁的粒度要细,例如按
userId、orderId加锁,避免锁住所有用户
二、读写锁(ReadWrite Lock)
2.1 原理简介
读写锁将锁分为读锁(共享锁)和写锁(排他锁):
- 读锁:多个线程可同时持有,互不阻塞
- 写锁:独占锁,只有一个线程能持有
- 互斥规则:
- 读-读:不互斥 ✅
- 读-写:互斥 ❌
- 写-写:互斥 ❌
适用场景:读多写少,通过读锁共享提升并发性能。
2.2 底层结构
读写锁在 Redis 中使用两个 Key:
Key1: 锁名称(存储写锁信息,Hash 结构)
Key2: 锁名称 + ":rw_lock:"(存储读锁计数)
读锁加锁简化逻辑:
-- 如果存在写锁,则获取失败
if (redis.call('exists', KEYS[1]) == 1) then
return redis.call('pttl', KEYS[1]);
end;
-- 读锁计数 +1
redis.call('hincrby', KEYS[2], ARGV[2], 1);
redis.call('pexpire', KEYS[2], ARGV[1]);
return nil;
2.3 基本用法
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class ProductService {
@Autowired
private RedissonClient redissonClient;
/**
* 读商品(加读锁,允许多线程并发读)
*/
public Product getProduct(String productId) {
RReadWriteLock rwLock = redissonClient.getReadWriteLock("rwlock:product:" + productId);
RLock readLock = rwLock.readLock();
readLock.lock();
try {
// 先查缓存
Product product = getFromCache(productId);
if (product != null) {
return product;
}
// 缓存未命中查 DB(此时其他线程可能也在查,可接受)
product = getFromDB(productId);
saveToCache(productId, product);
return product;
} finally {
if (readLock.isHeldByCurrentThread()) {
readLock.unlock();
}
}
}
/**
* 更新商品(加写锁,阻塞所有读和写)
*/
public void updateProduct(String productId, Product newProduct) {
RReadWriteLock rwLock = redissonClient.getReadWriteLock("rwlock:product:" + productId);
RLock writeLock = rwLock.writeLock();
writeLock.lock();
try {
// 先更新 DB
updateDB(productId, newProduct);
// 再更新缓存
saveToCache(productId, newProduct);
} finally {
if (writeLock.isHeldByCurrentThread()) {
writeLock.unlock();
}
}
}
// 省略 getFromCache / getFromDB / updateDB / saveToCache 方法
private Product getFromCache(String id) { return null; }
private Product getFromDB(String id) { return new Product(); }
private void saveToCache(String id, Product p) { }
private void updateDB(String id, Product p) { }
}
class Product { }
2.4 带超时的读写锁
public Product getProductWithWait(String productId) {
RReadWriteLock rwLock = redissonClient.getReadWriteLock("rwlock:product:" + productId);
RLock readLock = rwLock.readLock();
try {
// 最多等待 3 秒获取读锁
boolean acquired = readLock.tryLock(3, TimeUnit.SECONDS);
if (acquired) {
try {
return getFromDB(productId);
} finally {
if (readLock.isHeldByCurrentThread()) {
readLock.unlock();
}
}
}
throw new RuntimeException("获取读锁超时");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("加锁被中断", e);
}
}
2.5 读写锁的互斥矩阵
| 读锁请求 | 写锁请求 | |
|---|---|---|
| 当前持有读锁 | ✅ 成功(共享) | ❌ 阻塞(需等所有读锁释放) |
| 当前持有写锁 | ❌ 阻塞 | ❌ 阻塞 |
| 无锁 | ✅ 成功 | ✅ 成功 |
2.6 典型应用场景
| 场景 | 读锁 | 写锁 | 说明 |
|---|---|---|---|
| 缓存重建 | 查询时加读锁 | 重建缓存时加写锁 | 防止缓存击穿 |
| 配置中心 | 读取配置时加读锁 | 更新配置时加写锁 | 配置热更新时阻塞读 |
| 报表生成 | 查询报表时加读锁 | 生成报表时加写锁 | 生成过程中阻塞查询 |
| 商品详情 | 查看商品加读锁 | 修改商品加写锁 | 提升查询并发度 |
| 数据字典 | 查询字典加读锁 | 刷新字典加写锁 | 字典更新时阻塞读 |
2.7 最佳实践
- 读多写少才用:如果读写频率接近,读写锁比可重入锁更慢(维护读写状态有开销)
- 写锁要及时释放:写锁会阻塞所有读操作,持有时间应尽量短
- 避免锁升级:不要在读锁中尝试获取写锁,会导致死锁
- 读锁内不要修改数据:读锁是共享的,多个线程同时执行,修改数据会有并发问题
三、两种锁对比
| 对比项 | 可重入锁 | 读写锁 |
|---|---|---|
| 互斥性 | 完全互斥 | 读-读不互斥 |
| 并发性能 | 低 | 高(读场景) |
| 使用复杂度 | 简单 | 略复杂 |
| 重入支持 | 支持 | 支持 |
| 看门狗 | 支持 | 支持 |
| 适用场景 | 写多读少 / 通用互斥 | 读多写少 |
| Redis 开销 | 一个 Key | 两个 Key |
四、选型建议
业务场景
├── 读多写少(查询多、更新少)→ 读写锁 ✅
├── 写多读少 / 读写均衡 → 可重入锁 ✅
├── 通用互斥场景 → 可重入锁 ✅
├── 防止超卖 / 防重复提交 → 可重入锁 ✅
├── 缓存击穿防护 → 读写锁 ✅
└── 配置热更新 → 读写锁 ✅
核心原则:
- 默认用可重入锁,满足 80% 场景
- 读多写少时用读写锁,提升并发性能
- 不要过度设计,简单场景不要用复杂锁
五、是否需要读写互斥的强约束
5.1 判断流程图
业务场景
├── 数据一致性要求高?
│ ├── 是 → 使用写锁(读写互斥)✅
│ └── 否 → 使用读锁(读-读共享)或不加锁
│
├── 写操作期间允许读?
│ ├── 否 → 使用写锁 ✅
│ └── 是 → 使用读锁或不加锁
│
├── 读操作依赖最新数据?
│ ├── 是 → 使用写锁 ✅
│ └── 否 → 使用读锁或不加锁
│
└── 允许脏读?
├── 否 → 使用写锁 ✅
└── 是 → 使用读锁或不加锁
5.2 核心判断标准
- 数据一致性要求:金融交易、库存扣减等必须强一致
- 业务正确性:不允许基于过期数据做决策
- 用户体验:不允许用户看到中间状态
满足以上任意一条,就需要读写互斥的强约束。
六、分布式环境下的常见坑与避坑指南
6.1 共同的坑(两种锁都有)
坑 1:锁过期了业务还没执行完
lock.tryLock(3, 10, TimeUnit.SECONDS); // 锁只持 10 秒
try {
Thread.sleep(15000); // 业务执行了 15 秒,锁已经过期
} finally {
lock.unlock(); // 解锁的是别人刚加的锁!
}
后果:锁自动释放后其他线程也能获取锁,引发并发问题;当前线程 unlock() 可能误删别人的锁。
避坑:
- 业务时间不确定 → 用
lock.lock()触发看门狗自动续期 - 业务时间可控 → 才用固定 leaseTime
坑 2:释放了别人的锁
场景:锁过期后当前线程才执行完,unlock() 删掉了别人刚加上的锁。
避坑:解锁前必须判断:
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
坑 3:finally 里没解锁
lock.lock();
doSomething(); // 抛异常,unlock() 走不到
lock.unlock();
后果:锁只能等过期自动释放,期间所有线程阻塞。
避坑:标准模板:
lock.lock();
try {
doSomething();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
坑 4:Redis 主从切换导致锁丢失
Master(锁在这里)
↓ 主从同步有延迟
Slave
Master 挂了 → Slave 提升为 Master
→ Slave 上可能还没同步到锁
→ 新 Master 上锁不存在了!
后果:两个节点都认为自己拿到了锁。
避坑:
- 对一致性要求极高 → RedLock / Zookeeper 锁
- 普通业务 → 可接受极低概率的锁丢失
坑 5:锁粒度过大
// 全商品共用一把锁 → 所有操作串行
RLock lock = redisson.getLock("lock:product");
避坑:按业务维度细分:
// 只锁单个商品
RLock lock = redisson.getLock("lock:product:" + productId);
坑 6:多锁死锁
线程 A:先锁账户 1 → 再锁账户 2
线程 B:先锁账户 2 → 再锁账户 1
→ 循环等待,死锁
避坑:
- 按固定顺序加锁(如按 ID 排序,小的先加)
- 用
tryLock加超时,获取失败就释放已持有的锁 - 用 Redisson 的
MultiLock
坑 7:业务异常回滚了,锁正常释放
锁只保证"同一时间只有一个线程执行",不保证"执行结果一定成功"。数据一致性靠数据库事务,锁只解决并发问题。
坑 8:网络延迟导致锁竞争不均
网络好的节点更容易抢到锁,造成不公平。对公平性有要求用 getFairLock() 公平锁,但性能有损失。
6.2 读写锁的特有坑
坑 1:写锁饥饿
读锁-读锁-读锁-... 源源不断的读
↓
写锁永远等不到
后果:写操作一直阻塞,请求超时堆积。
避坑:
- 控制读锁持有时间,尽量短
- 写操作优先级高的场景,考虑用可重入锁替代
坑 2:读锁里修改数据
读锁只保证写锁不进来,但多个读锁是同时存在的。在读锁里修改数据等于没有保护。
避坑:读锁里只读,写操作必须用写锁。
坑 3:锁升级(读锁升级为写锁)
readLock.lock();
try {
writeLock.lock(); // ❌ 死锁!持有读锁又请求写锁
} finally {
readLock.unlock();
}
后果:自己持有读锁,又请求写锁 → 写锁要等所有读锁释放 → 自己等自己 → 死锁。
避坑:Redisson 不支持锁升级。先释放读锁,再获取写锁。
坑 4:写操作很慢,读请求洪峰涌入
写锁期间读全部阻塞,写完后读请求同时涌入打满系统。
避坑:
- 优化写操作,缩短持有时间
- 考虑双缓存策略
坑 5:读写频率差不多,用读写锁反而更慢
读写比至少 5:1 以上,读写锁的收益才大于开销。读写均衡时直接用可重入锁。
6.3 避坑速查表
| 坑 | 严重程度 | 避坑方法 |
|---|---|---|
| 锁过期业务没跑完 | ⭐⭐⭐⭐⭐ | 用看门狗(lock.lock() 或 leaseTime=-1) |
| 释放了别人的锁 | ⭐⭐⭐⭐⭐ | unlock() 前判断 isHeldByCurrentThread() |
| finally 没解锁 | ⭐⭐⭐⭐⭐ | 标准模板:lock → try → finally unlock |
| 主从切换锁丢失 | ⭐⭐⭐⭐ | 高一致用 RedLock / ZK,普通业务可接受 |
| 锁粒度过大 | ⭐⭐⭐ | 按业务 ID 细分锁 |
| 多锁死锁 | ⭐⭐⭐ | 固定顺序加锁 / tryLock 超时 / MultiLock |
| 写锁饥饿 | ⭐⭐⭐⭐ | 缩短读锁时间 / 考虑用可重入锁 |
| 锁升级死锁 | ⭐⭐⭐⭐ | 不支持升级,先释放读锁再获取写锁 |
| 读锁里写数据 | ⭐⭐⭐⭐ | 读锁只读,写操作必须用写锁 |
| 读写差不多还用读写锁 | ⭐⭐ | 读写比 5:1 以上再考虑 |

浙公网安备 33010602011771号