宁夏回族自治区网站建设_网站建设公司_在线商城_seo优化
2025/12/30 16:55:56 网站建设 项目流程

方案概述

本方案基于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
参数说明
参数类型说明示例值
waitTimelong等待时间:尝试获取锁的最大等待时间。如果在这段时间内无法获取锁,方法返回false15
leaseTimelong持有时间:锁的自动释放时间。超过这个时间,锁会自动释放,即使业务逻辑未完成30
timeUnitTimeUnit时间单位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的锁)

可能的问题

  1. 重复执行:多个线程可能同时执行相同的业务逻辑
  2. 数据不一致:并发修改可能导致数据冲突
  3. 资源浪费:重复调用外部接口,增加系统负载
  4. 锁释放异常:线程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 固定过期时间

特性看门狗机制固定过期时间
适用场景业务执行时间不确定业务执行时间可预估
自动续命✅ 是❌ 否
死锁风险低(异常退出时自动释放)低(固定时间后释放)
重复执行风险高(超时后可能重复执行)
性能开销略高(需要后台线程)
推荐使用✅ 业务时间不确定时✅ 业务时间确定且较短时

重试机制设计

设计原则

  1. 指数退避:重试间隔逐渐增加,避免系统过载
  2. 最大重试次数:防止无限重试
  3. 异常记录:记录最后一次异常,便于排查
  4. 中断处理:正确处理线程中断

实现示例

/** * 带重试机制的获取资源方法 * * @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 = fixed200ms, 200ms, 200ms系统负载稳定
线性递增sleepMs = base * retryCount200ms, 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

通用推荐值

业务类型waitTimeleaseTime是否看门狗说明
Token刷新15s30s通常很快完成
缓存预热10s20s数据加载较快
数据同步30s60s中等耗时操作
批量处理15s-1时间不确定
复杂计算15s-1时间不确定
外部API调用15s30s有超时控制

重试机制参数推荐

场景最大重试次数初始延迟退避策略说明
高可用要求5200ms线性递增提高成功率
快速失败2100ms固定间隔快速响应
网络不稳定3300ms指数退避适应网络波动
通用场景3200ms线性递增推荐配置

常见问题与解决方案

Q1: 锁获取失败后应该如何处理?

问题tryLock()返回false时,业务逻辑无法执行。

解决方案

  1. 降级策略:尝试从缓存读取(如示例代码)
  2. 重试机制:外层增加重试逻辑
  3. 快速失败:直接返回错误,由调用方处理

Q2: 如何避免死锁?

问题:业务异常导致锁未释放。

解决方案

  1. ✅ 使用try-finally确保锁释放
  2. ✅ 使用lock.isHeldByCurrentThread()检查
  3. ✅ 设置合理的leaseTime或使用看门狗
  4. ✅ 避免在锁内调用可能阻塞的方法

Q3: 锁被其他线程释放怎么办?

问题:线程A的锁被线程B释放。

解决方案

// ✅ 正确:检查锁的持有者if(lock.isHeldByCurrentThread()){lock.unlock();}// ❌ 错误:直接释放lock.unlock();// 可能释放其他线程的锁

Q4: 如何监控锁的使用情况?

解决方案

  1. 日志记录:记录锁获取/释放时间
  2. 指标监控:统计锁等待时间、持有时间
  3. 告警机制:锁等待时间过长时告警
StopWatchstopWatch=StopWatch.createStarted();booleanlocked=lock.tryLock(15,30,TimeUnit.SECONDS);longwaitTime=stopWatch.getTime();if(waitTime>5000){log.warn("锁等待时间过长: {}ms",waitTime);}

Q5: 分布式锁的性能影响?

优化建议

  1. 减少锁粒度:只锁必要的资源
  2. 缩短持有时间:尽快释放锁
  3. 使用本地锁:单机场景优先使用synchronized
  4. 缓存降级:锁获取失败时使用缓存

总结

核心要点

  1. 双重检查:减少锁竞争,提高性能
  2. 重试机制:提高系统容错能力
  3. 合理参数:根据业务场景选择waitTimeleaseTime
  4. 看门狗机制:业务时间不确定时使用
  5. 异常处理:确保锁正确释放

选择建议

  • 业务时间可预估→ 使用固定leaseTime
  • 业务时间不确定→ 使用看门狗机制
  • 高并发场景→ 缩短waitTime,快速失败
  • 高可用要求→ 增加重试次数,使用降级策略

参考资源

  • Redisson 官方文档
  • 分布式锁最佳实践

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询