方案概述
本方案基于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)方法详解
方法签名
booleantryLock(longwaitTime,longleaseTime,TimeUnitunit)throwsInterruptedException参数说明
| 参数 | 类型 | 说明 | 示例值 |
|---|---|---|---|
waitTime | long | 等待时间:尝试获取锁的最大等待时间。如果在这段时间内无法获取锁,方法返回false | 15 |
leaseTime | long | 持有时间:锁的自动释放时间。超过这个时间,锁会自动释放,即使业务逻辑未完成 | 30 |
timeUnit | TimeUnit | 时间单位 | TimeUnit.SECONDS |
示例调用
RLocklock=redissonClient.getLock("lock:key");booleanlocked=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()方法(推荐)
RLocklock=redissonClient.getLock("lock:key");try{// 先尝试获取锁,最多等待15秒if(lock.tryLock(15,-1,TimeUnit.SECONDS)){try{// 业务逻辑// 看门狗会自动续命}finally{if(lock.isHeldByCurrentThread()){lock.unlock();}}}}catch(InterruptedExceptione){Thread.currentThread().interrupt();thrownewRuntimeException("获取锁被中断",e);}注意:leaseTime设置为-1表示不设置过期时间,启用看门狗。
方式2:使用lock()无参方法
RLocklock=redissonClient.getLock("lock:key");try{// 阻塞等待获取锁,启用看门狗lock.lock();try{// 业务逻辑// 看门狗会自动续命}finally{if(lock.isHeldByCurrentThread()){lock.unlock();}}}catch(Exceptione){// 异常处理}注意:lock()方法会阻塞等待,直到获取到锁。
方式3:使用lock(long leaseTime, TimeUnit unit)并手动续命
RLocklock=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(InterruptedExceptione){Thread.currentThread().interrupt();thrownewRuntimeException("获取锁被中断",e);}看门狗配置
Redisson 默认配置:
- 续命间隔:
lockWatchdogTimeout= 30秒(默认值) - 续命时长:每次续命 30秒
自定义配置:
Configconfig=newConfig();// 设置看门狗超时时间为60秒config.setLockWatchdogTimeout(60000);// 单位:毫秒RedissonClientredissonClient=Redisson.create(config);看门狗 vs 固定过期时间
| 特性 | 看门狗机制 | 固定过期时间 |
|---|---|---|
| 适用场景 | 业务执行时间不确定 | 业务执行时间可预估 |
| 自动续命 | ✅ 是 | ❌ 否 |
| 死锁风险 | 低(异常退出时自动释放) | 低(固定时间后释放) |
| 重复执行风险 | 低 | 高(超时后可能重复执行) |
| 性能开销 | 略高(需要后台线程) | 低 |
| 推荐使用 | ✅ 业务时间不确定时 | ✅ 业务时间确定且较短时 |
重试机制设计
设计原则
- 指数退避:重试间隔逐渐增加,避免系统过载
- 最大重试次数:防止无限重试
- 异常记录:记录最后一次异常,便于排查
- 中断处理:正确处理线程中断
实现示例
/** * 带重试机制的获取资源方法 * * @param maxRetries 最大重试次数 * @return 资源对象 */privateStringgetResourceWithRetry(intmaxRetries){intretryCount=0;ExceptionlastException=null;while(retryCount<maxRetries){try{returntryGetResourceWithLock();}catch(Exceptione){lastException=e;retryCount++;if(retryCount<maxRetries){// 指数退避:200ms, 400ms, 600ms...longsleepMs=200L*retryCount;log.warn("第{}次尝试失败,{}ms后重试,错误: {}",retryCount,sleepMs,e.getMessage());try{Thread.sleep(sleepMs);}catch(InterruptedExceptionie){Thread.currentThread().interrupt();thrownewRuntimeException("获取资源被中断",ie);}}}}log.error("重试{}次后仍然失败",maxRetries);thrownewRuntimeException("获取资源失败: "+(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@Slf4jpublicclassResourceManager{privatestaticfinalStringRESOURCE_CACHE_KEY="resource:cache:key";privatestaticfinalStringRESOURCE_LOCK_KEY="lock:resource:key";privatestaticfinalintMAX_RETRIES=3;@ResourceprivateRedissonClientredissonClient;/** * 获取资源(带缓存和重试机制) * * @param forceRefresh 是否强制刷新 * @return 资源对象 */publicStringgetResource(booleanforceRefresh){// 1. 如果不是强制刷新,先从缓存读取if(!forceRefresh){RBucket<String>bucket=redissonClient.getBucket(RESOURCE_CACHE_KEY);StringcachedResource=bucket.get();if(StringUtils.isNotBlank(cachedResource)){log.debug("资源从缓存获取成功");returncachedResource;}}// 2. 使用分布式锁获取资源,增加重试机制returngetResourceWithRetry(forceRefresh,MAX_RETRIES);}/** * 带重试机制的获取资源 * * @param forceRefresh 是否强制刷新 * @param maxRetries 最大重试次数 * @return 资源对象 */privateStringgetResourceWithRetry(booleanforceRefresh,intmaxRetries){intretryCount=0;ExceptionlastException=null;while(retryCount<maxRetries){try{returntryGetResourceWithLock(forceRefresh);}catch(Exceptione){lastException=e;retryCount++;if(retryCount<maxRetries){longsleepMs=200L*retryCount;// 递增延迟log.warn("第{}次尝试失败,{}ms后重试,错误: {}",retryCount,sleepMs,e.getMessage());try{Thread.sleep(sleepMs);}catch(InterruptedExceptionie){Thread.currentThread().interrupt();thrownewRuntimeException("获取资源被中断",ie);}}}}log.error("重试{}次后仍然失败",maxRetries);thrownewRuntimeException("获取资源失败: "+(lastException!=null?lastException.getMessage():"未知错误"));}/** * 使用分布式锁尝试获取资源 * * @param forceRefresh 是否强制刷新 * @return 资源对象 */privateStringtryGetResourceWithLock(booleanforceRefresh){RLocklock=redissonClient.getLock(RESOURCE_LOCK_KEY);try{// 方案1:使用固定过期时间(适合业务时间可预估的场景)booleanlocked=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);StringcachedResource=bucket.get();if(StringUtils.isNotBlank(cachedResource)){log.info("锁超时后从缓存获取到资源");returncachedResource;}}thrownewRuntimeException("获取分布式锁超时,可能系统繁忙");}try{// 3. 双重检查,避免重复获取if(!forceRefresh){RBucket<String>bucket=redissonClient.getBucket(RESOURCE_CACHE_KEY);StringcachedResource=bucket.get();if(StringUtils.isNotBlank(cachedResource)){log.info("获取锁后从缓存获取到资源(双重检查)");returncachedResource;}}// 4. 执行业务逻辑(获取资源)Stringresource=fetchResourceFromSource();// 5. 写入缓存if(StringUtils.isNotBlank(resource)){RBucket<String>bucket=redissonClient.getBucket(RESOURCE_CACHE_KEY);bucket.set(resource,3600,TimeUnit.SECONDS);// 缓存1小时log.info("资源已缓存");}returnresource;}finally{// 6. 释放锁(确保只释放当前线程持有的锁)if(lock.isHeldByCurrentThread()){lock.unlock();log.debug("锁已释放");}}}catch(InterruptedExceptione){log.error("获取锁时被中断",e);Thread.currentThread().interrupt();thrownewRuntimeException("获取资源被中断",e);}}/** * 从数据源获取资源(业务逻辑) */privateStringfetchResourceFromSource(){// 实现具体的业务逻辑// 例如:调用外部API、查询数据库等return"resource_data";}}最佳实践与参数推荐
lock.tryLock()参数推荐
场景1:业务执行时间可预估(< 30秒)
// 推荐配置booleanlocked=lock.tryLock(15,30,TimeUnit.SECONDS);参数说明:
waitTime = 15秒:等待时间适中,避免长时间阻塞leaseTime = 30秒:根据业务最大执行时间设置,建议设置为业务最大执行时间 * 1.5- 适用场景:Token刷新、缓存预热、数据同步等
场景2:业务执行时间不确定
// 推荐配置:使用看门狗机制booleanlocked=lock.tryLock(15,-1,TimeUnit.SECONDS);// 或者lock.lock();// 阻塞等待,启用看门狗参数说明:
waitTime = 15秒:等待时间leaseTime = -1:不设置过期时间,启用看门狗- 适用场景:复杂计算、批量处理、长时间任务等
场景3:高并发场景
// 推荐配置:缩短等待时间,避免线程堆积booleanlocked=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: 如何监控锁的使用情况?
解决方案:
- 日志记录:记录锁获取/释放时间
- 指标监控:统计锁等待时间、持有时间
- 告警机制:锁等待时间过长时告警
StopWatchstopWatch=StopWatch.createStarted();booleanlocked=lock.tryLock(15,30,TimeUnit.SECONDS);longwaitTime=stopWatch.getTime();if(waitTime>5000){log.warn("锁等待时间过长: {}ms",waitTime);}Q5: 分布式锁的性能影响?
优化建议:
- 减少锁粒度:只锁必要的资源
- 缩短持有时间:尽快释放锁
- 使用本地锁:单机场景优先使用
synchronized - 缓存降级:锁获取失败时使用缓存
总结
核心要点
- 双重检查:减少锁竞争,提高性能
- 重试机制:提高系统容错能力
- 合理参数:根据业务场景选择
waitTime和leaseTime - 看门狗机制:业务时间不确定时使用
- 异常处理:确保锁正确释放
选择建议
- ✅业务时间可预估→ 使用固定
leaseTime - ✅业务时间不确定→ 使用看门狗机制
- ✅高并发场景→ 缩短
waitTime,快速失败 - ✅高可用要求→ 增加重试次数,使用降级策略
参考资源
- Redisson 官方文档
- 分布式锁最佳实践