分布式锁与重试机制标准化方案
·
方案概述
本方案基于 Redisson 实现分布式锁,结合重试机制和双重检查模式,确保在高并发场景下的数据一致性和系统稳定性。
核心特性
- ✅ 分布式锁:防止多实例/多线程并发执行
- ✅ 重试机制:提高系统容错能力
- ✅ 双重检查:减少不必要的锁竞争
- ✅ 缓存降级:锁获取失败时尝试从缓存读取
- ✅ 异常处理:完善的异常捕获和日志记录
核心设计模式
1. 双重检查锁定(Double-Check Locking)
┌─────────────────────────────────────────┐
│ 1. 检查缓存(无锁) │
│ ↓ 缓存未命中 │
│ 2. 获取分布式锁 │
│ ↓ 获取成功 │
│ 3. 再次检查缓存(双重检查) │
│ ↓ 缓存仍未命中 │
│ 4. 执行业务逻辑 │
│ 5. 更新缓存 │
│ 6. 释放锁 │
└─────────────────────────────────────────┘
优势:
- 减少锁竞争:大部分请求在第一次检查时就能从缓存获取数据
- 避免重复执行:获取锁后再次检查,确保不重复执行
2. 重试机制(Retry Pattern)
尝试次数: 1 ──失败──> 等待 200ms ──> 尝试次数: 2 ──失败──> 等待 400ms ──> 尝试次数: 3 ──失败──> 抛出异常
特点:
- 指数退避:每次重试间隔递增(200ms, 400ms, 600ms…)
- 最大重试次数:防止无限重试
- 异常记录:记录最后一次异常信息
Redisson 分布式锁详解
lock.tryLock(waitTime, leaseTime, timeUnit) 方法详解
方法签名
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException
参数说明
| 参数 | 类型 | 说明 | 示例值 |
|---|---|---|---|
waitTime |
long | 等待时间:尝试获取锁的最大等待时间。如果在这段时间内无法获取锁,方法返回 false |
15 |
leaseTime |
long | 持有时间:锁的自动释放时间。超过这个时间,锁会自动释放,即使业务逻辑未完成 | 30 |
timeUnit |
TimeUnit | 时间单位 | TimeUnit.SECONDS |
示例调用
RLock lock = redissonClient.getLock("lock:key");
boolean locked = lock.tryLock(15, 30, TimeUnit.SECONDS);
含义:
- 最多等待 15秒 尝试获取锁
- 如果获取成功,锁将在 30秒 后自动释放
- 时间单位:秒
关键问题解答
1. 是否会自动续命(看门狗机制)?
答案:❌ 不会自动续命
当使用 tryLock(waitTime, leaseTime, timeUnit) 方法并指定了 leaseTime 参数时,Redisson 的看门狗机制会被禁用。
原因:
- 看门狗机制只在未指定
leaseTime时生效 - 指定
leaseTime后,Redisson 认为你希望锁在固定时间后自动释放 - 这是为了避免死锁,但可能导致业务逻辑未完成时锁被释放
验证方法:
// 方式1:指定 leaseTime(无看门狗)
lock.tryLock(15, 30, TimeUnit.SECONDS); // ❌ 无看门狗
// 方式2:不指定 leaseTime(有看门狗)
lock.tryLock(15, -1, TimeUnit.SECONDS); // ✅ 有看门狗(-1 表示不设置过期时间)
// 或者
lock.lock(); // ✅ 有看门狗,默认30秒续命
2. 如果 30 秒内业务没做完,会出现什么情况?
场景分析:
时间线:
T=0s: 线程A获取锁,开始执行业务逻辑(预计需要45秒)
T=30s: 锁自动释放(leaseTime到期)
T=31s: 线程B获取锁,开始执行业务逻辑
T=45s: 线程A的业务逻辑完成,尝试释放锁(可能失败或释放了线程B的锁)
可能的问题:
- 重复执行:多个线程可能同时执行相同的业务逻辑
- 数据不一致:并发修改可能导致数据冲突
- 资源浪费:重复调用外部接口,增加系统负载
- 锁释放异常:线程A可能释放了线程B的锁
解决方案:
- ✅ 使用看门狗机制(见下文)
- ✅ 合理设置
leaseTime,确保大于业务执行时间 - ✅ 业务逻辑中增加幂等性检查
- ✅ 使用
lock.isHeldByCurrentThread()检查锁的持有者
看门狗机制详解
什么是看门狗(Watchdog)?
看门狗是 Redisson 提供的一种自动续命机制,用于防止业务逻辑执行时间超过锁的持有时间。
工作原理
┌─────────────────────────────────────────────────────┐
│ 1. 获取锁(不指定 leaseTime) │
│ 2. Redisson 启动看门狗线程 │
│ 3. 每 10 秒检查一次锁是否仍被当前线程持有 │
│ 4. 如果持有,自动续命 30 秒(默认值) │
│ 5. 业务逻辑完成后,释放锁,看门狗停止 │
│ 6. 如果线程异常退出,锁在 30 秒后自动释放 │
└─────────────────────────────────────────────────────┘
如何启用看门狗?
方式1:使用 lock() 方法(推荐)
RLock lock = redissonClient.getLock("lock:key");
try {
// 先尝试获取锁,最多等待15秒
if (lock.tryLock(15, -1, TimeUnit.SECONDS)) {
try {
// 业务逻辑
// 看门狗会自动续命
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("获取锁被中断", e);
}
注意:leaseTime 设置为 -1 表示不设置过期时间,启用看门狗。
方式2:使用 lock() 无参方法
RLock lock = redissonClient.getLock("lock:key");
try {
// 阻塞等待获取锁,启用看门狗
lock.lock();
try {
// 业务逻辑
// 看门狗会自动续命
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
} catch (Exception e) {
// 异常处理
}
注意:lock() 方法会阻塞等待,直到获取到锁。
方式3:使用 lock(long leaseTime, TimeUnit unit) 并手动续命
RLock lock = redissonClient.getLock("lock:key");
try {
if (lock.tryLock(15, 30, TimeUnit.SECONDS)) {
try {
// 业务逻辑
// 如果预计执行时间超过30秒,需要手动续命
if (需要续命) {
lock.expire(30, TimeUnit.SECONDS); // 手动续命30秒
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("获取锁被中断", e);
}
看门狗配置
Redisson 默认配置:
- 续命间隔:
lockWatchdogTimeout= 30秒(默认值) - 续命时长:每次续命 30秒
自定义配置:
Config config = new Config();
// 设置看门狗超时时间为60秒
config.setLockWatchdogTimeout(60000); // 单位:毫秒
RedissonClient redissonClient = Redisson.create(config);
看门狗 vs 固定过期时间
| 特性 | 看门狗机制 | 固定过期时间 |
|---|---|---|
| 适用场景 | 业务执行时间不确定 | 业务执行时间可预估 |
| 自动续命 | ✅ 是 | ❌ 否 |
| 死锁风险 | 低(异常退出时自动释放) | 低(固定时间后释放) |
| 重复执行风险 | 低 | 高(超时后可能重复执行) |
| 性能开销 | 略高(需要后台线程) | 低 |
| 推荐使用 | ✅ 业务时间不确定时 | ✅ 业务时间确定且较短时 |
重试机制设计
设计原则
- 指数退避:重试间隔逐渐增加,避免系统过载
- 最大重试次数:防止无限重试
- 异常记录:记录最后一次异常,便于排查
- 中断处理:正确处理线程中断
实现示例
/**
* 带重试机制的获取资源方法
*
* @param maxRetries 最大重试次数
* @return 资源对象
*/
private String getResourceWithRetry(int maxRetries) {
int retryCount = 0;
Exception lastException = null;
while (retryCount < maxRetries) {
try {
return tryGetResourceWithLock();
} catch (Exception e) {
lastException = e;
retryCount++;
if (retryCount < maxRetries) {
// 指数退避:200ms, 400ms, 600ms...
long sleepMs = 200L * retryCount;
log.warn("第{}次尝试失败,{}ms后重试,错误: {}",
retryCount, sleepMs, e.getMessage());
try {
Thread.sleep(sleepMs);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("获取资源被中断", ie);
}
}
}
}
log.error("重试{}次后仍然失败", maxRetries);
throw new RuntimeException("获取资源失败: " + (lastException != null ? lastException.getMessage() : "未知错误"));
}
重试策略对比
| 策略 | 公式 | 示例(3次重试) | 适用场景 |
|---|---|---|---|
| 固定间隔 | sleepMs = fixed |
200ms, 200ms, 200ms | 系统负载稳定 |
| 线性递增 | sleepMs = base * retryCount |
200ms, 400ms, 600ms | 通用场景(推荐) |
| 指数退避 | sleepMs = base * 2^(retryCount-1) |
200ms, 400ms, 800ms | 高并发场景 |
| 随机退避 | sleepMs = random(base, max) |
150-250ms, 300-500ms | 避免惊群效应 |
完整实现示例
标准模板代码
@Component
@Slf4j
public class ResourceManager {
private static final String RESOURCE_CACHE_KEY = "resource:cache:key";
private static final String RESOURCE_LOCK_KEY = "lock:resource:key";
private static final int MAX_RETRIES = 3;
@Resource
private RedissonClient redissonClient;
/**
* 获取资源(带缓存和重试机制)
*
* @param forceRefresh 是否强制刷新
* @return 资源对象
*/
public String getResource(boolean forceRefresh) {
// 1. 如果不是强制刷新,先从缓存读取
if (!forceRefresh) {
RBucket<String> bucket = redissonClient.getBucket(RESOURCE_CACHE_KEY);
String cachedResource = bucket.get();
if (StringUtils.isNotBlank(cachedResource)) {
log.debug("资源从缓存获取成功");
return cachedResource;
}
}
// 2. 使用分布式锁获取资源,增加重试机制
return getResourceWithRetry(forceRefresh, MAX_RETRIES);
}
/**
* 带重试机制的获取资源
*
* @param forceRefresh 是否强制刷新
* @param maxRetries 最大重试次数
* @return 资源对象
*/
private String getResourceWithRetry(boolean forceRefresh, int maxRetries) {
int retryCount = 0;
Exception lastException = null;
while (retryCount < maxRetries) {
try {
return tryGetResourceWithLock(forceRefresh);
} catch (Exception e) {
lastException = e;
retryCount++;
if (retryCount < maxRetries) {
long sleepMs = 200L * retryCount; // 递增延迟
log.warn("第{}次尝试失败,{}ms后重试,错误: {}",
retryCount, sleepMs, e.getMessage());
try {
Thread.sleep(sleepMs);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("获取资源被中断", ie);
}
}
}
}
log.error("重试{}次后仍然失败", maxRetries);
throw new RuntimeException("获取资源失败: " +
(lastException != null ? lastException.getMessage() : "未知错误"));
}
/**
* 使用分布式锁尝试获取资源
*
* @param forceRefresh 是否强制刷新
* @return 资源对象
*/
private String tryGetResourceWithLock(boolean forceRefresh) {
RLock lock = redissonClient.getLock(RESOURCE_LOCK_KEY);
try {
// 方案1:使用固定过期时间(适合业务时间可预估的场景)
boolean locked = lock.tryLock(15, 30, TimeUnit.SECONDS);
// 方案2:使用看门狗机制(适合业务时间不确定的场景)
// boolean locked = lock.tryLock(15, -1, TimeUnit.SECONDS);
if (!locked) {
log.warn("获取分布式锁超时");
// 锁获取失败时,再次尝试从缓存读取
if (!forceRefresh) {
RBucket<String> bucket = redissonClient.getBucket(RESOURCE_CACHE_KEY);
String cachedResource = bucket.get();
if (StringUtils.isNotBlank(cachedResource)) {
log.info("锁超时后从缓存获取到资源");
return cachedResource;
}
}
throw new RuntimeException("获取分布式锁超时,可能系统繁忙");
}
try {
// 3. 双重检查,避免重复获取
if (!forceRefresh) {
RBucket<String> bucket = redissonClient.getBucket(RESOURCE_CACHE_KEY);
String cachedResource = bucket.get();
if (StringUtils.isNotBlank(cachedResource)) {
log.info("获取锁后从缓存获取到资源(双重检查)");
return cachedResource;
}
}
// 4. 执行业务逻辑(获取资源)
String resource = fetchResourceFromSource();
// 5. 写入缓存
if (StringUtils.isNotBlank(resource)) {
RBucket<String> bucket = redissonClient.getBucket(RESOURCE_CACHE_KEY);
bucket.set(resource, 3600, TimeUnit.SECONDS); // 缓存1小时
log.info("资源已缓存");
}
return resource;
} finally {
// 6. 释放锁(确保只释放当前线程持有的锁)
if (lock.isHeldByCurrentThread()) {
lock.unlock();
log.debug("锁已释放");
}
}
} catch (InterruptedException e) {
log.error("获取锁时被中断", e);
Thread.currentThread().interrupt();
throw new RuntimeException("获取资源被中断", e);
}
}
/**
* 从数据源获取资源(业务逻辑)
*/
private String fetchResourceFromSource() {
// 实现具体的业务逻辑
// 例如:调用外部API、查询数据库等
return "resource_data";
}
}
最佳实践与参数推荐
lock.tryLock() 参数推荐
场景1:业务执行时间可预估(< 30秒)
// 推荐配置
boolean locked = lock.tryLock(15, 30, TimeUnit.SECONDS);
参数说明:
waitTime = 15秒:等待时间适中,避免长时间阻塞leaseTime = 30秒:根据业务最大执行时间设置,建议设置为业务最大执行时间 * 1.5- 适用场景:Token刷新、缓存预热、数据同步等
场景2:业务执行时间不确定
// 推荐配置:使用看门狗机制
boolean locked = lock.tryLock(15, -1, TimeUnit.SECONDS);
// 或者
lock.lock(); // 阻塞等待,启用看门狗
参数说明:
waitTime = 15秒:等待时间leaseTime = -1:不设置过期时间,启用看门狗- 适用场景:复杂计算、批量处理、长时间任务等
场景3:高并发场景
// 推荐配置:缩短等待时间,避免线程堆积
boolean locked = lock.tryLock(5, 20, TimeUnit.SECONDS);
参数说明:
waitTime = 5秒:快速失败,避免线程堆积leaseTime = 20秒:根据实际业务时间设置- 适用场景:秒杀、限流、高频接口等
参数选择决策树
业务执行时间是否可预估?
├─ 是
│ ├─ 执行时间 < 10秒 → waitTime=10s, leaseTime=15s
│ ├─ 执行时间 10-30秒 → waitTime=15s, leaseTime=30s
│ └─ 执行时间 > 30秒 → 考虑使用看门狗机制
│
└─ 否
└─ 使用看门狗机制 → waitTime=15s, leaseTime=-1
通用推荐值
| 业务类型 | waitTime | leaseTime | 是否看门狗 | 说明 |
|---|---|---|---|---|
| Token刷新 | 15s | 30s | ❌ | 通常很快完成 |
| 缓存预热 | 10s | 20s | ❌ | 数据加载较快 |
| 数据同步 | 30s | 60s | ❌ | 中等耗时操作 |
| 批量处理 | 15s | -1 | ✅ | 时间不确定 |
| 复杂计算 | 15s | -1 | ✅ | 时间不确定 |
| 外部API调用 | 15s | 30s | ❌ | 有超时控制 |
重试机制参数推荐
| 场景 | 最大重试次数 | 初始延迟 | 退避策略 | 说明 |
|---|---|---|---|---|
| 高可用要求 | 5 | 200ms | 线性递增 | 提高成功率 |
| 快速失败 | 2 | 100ms | 固定间隔 | 快速响应 |
| 网络不稳定 | 3 | 300ms | 指数退避 | 适应网络波动 |
| 通用场景 | 3 | 200ms | 线性递增 | 推荐配置 |
常见问题与解决方案
Q1: 锁获取失败后应该如何处理?
问题:tryLock() 返回 false 时,业务逻辑无法执行。
解决方案:
- 降级策略:尝试从缓存读取(如示例代码)
- 重试机制:外层增加重试逻辑
- 快速失败:直接返回错误,由调用方处理
Q2: 如何避免死锁?
问题:业务异常导致锁未释放。
解决方案:
- ✅ 使用
try-finally确保锁释放 - ✅ 使用
lock.isHeldByCurrentThread()检查 - ✅ 设置合理的
leaseTime或使用看门狗 - ✅ 避免在锁内调用可能阻塞的方法
Q3: 锁被其他线程释放怎么办?
问题:线程A的锁被线程B释放。
解决方案:
// ✅ 正确:检查锁的持有者
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
// ❌ 错误:直接释放
lock.unlock(); // 可能释放其他线程的锁
Q4: 如何监控锁的使用情况?
解决方案:
- 日志记录:记录锁获取/释放时间
- 指标监控:统计锁等待时间、持有时间
- 告警机制:锁等待时间过长时告警
StopWatch stopWatch = StopWatch.createStarted();
boolean locked = lock.tryLock(15, 30, TimeUnit.SECONDS);
long waitTime = stopWatch.getTime();
if (waitTime > 5000) {
log.warn("锁等待时间过长: {}ms", waitTime);
}
Q5: 分布式锁的性能影响?
优化建议:
- 减少锁粒度:只锁必要的资源
- 缩短持有时间:尽快释放锁
- 使用本地锁:单机场景优先使用
synchronized - 缓存降级:锁获取失败时使用缓存
总结
核心要点
- 双重检查:减少锁竞争,提高性能
- 重试机制:提高系统容错能力
- 合理参数:根据业务场景选择
waitTime和leaseTime - 看门狗机制:业务时间不确定时使用
- 异常处理:确保锁正确释放
选择建议
- ✅ 业务时间可预估 → 使用固定
leaseTime - ✅ 业务时间不确定 → 使用看门狗机制
- ✅ 高并发场景 → 缩短
waitTime,快速失败 - ✅ 高可用要求 → 增加重试次数,使用降级策略
参考资源
更多推荐


所有评论(0)