分布式线程池“打架”?Redisson 锁竞争闭环实战
前言
分布式部署后,单机线程池的状态隔离会变成集群级协调问题。多个节点同时执行有状态任务时,可能出现重复消费、资源抢占、拒绝策略频繁触发和 CPU 飙升等问题。
本文围绕分布式线程池的锁竞争闭环,分析如何使用 Redisson 分布式锁、WatchDog 续期和任务状态管理,保证集群任务执行的互斥性与可恢复性。
一、底层原理
1.1 核心机制
在单机环境下,ThreadPoolExecutor的状态是内存级的,大家互不干扰。
一旦变成微服务集群,三台机器各自维护各自的线程池。
任务分发过来,谁抢到谁执行。
这时候,如果任务本身有状态依赖(比如只能有一个节点在写库),那就麻烦了。
我们需要一个分布式的“交通指挥员”。
Redisson 就是那个拿着大喇叭的交警。
它基于 Redis 实现了分布式锁,核心是RLock。
看门狗(WatchDog)机制是它的灵魂。
业务逻辑还没跑完,锁的过期时间会自动续命。
这就避免了“锁还没释放,进程先挂了”的尴尬。
sequenceDiagram participant NodeA as 节点 A participant NodeB as 节点 B participant Redis as Redis 服务器 participant WatchDog as 看门狗线程 NodeA->>Redis: 尝试获取锁 (tryLock) Redis-->>NodeA: 获取成功 NodeA->>NodeA: 执行业务逻辑 NodeA->>WatchDog: 启动续期任务 WatchDog-->>Redis: 自动延长 TTL NodeA->>Redis: 释放锁 (unlock) Redis-->>NodeB: 通知锁可用 NodeB->>Redis: 尝试获取锁设计优势很明显。
自动续期解决了业务执行时间不可控的问题。
重入性保证了同一个线程可以多次获取同一把锁。
1.2 与同类方案的对比
咱们再横向对比一下,别盲目选技术。
| 方案 | 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| Redisson | Redis + Lua 脚本 | 自动续期,API 丰富,集成简单 | 依赖 Redis 高可用 | 绝大多数分布式锁场景 |
| Zookeeper | 临时顺序节点 | 强一致性,CP 模型 | 性能稍低,运维成本高 | 对一致性要求极高的金融场景 |
| 数据库锁 | 唯一索引/版本号 | 无需额外组件 | 性能差,易死锁 | 低频、非核心业务 |
对于咱们 Java 微服务来说,Redisson 是性价比之王。
除非你有特殊的强一致性需求,否则别折腾 ZK。
二、快速上手
别整那些复杂的配置,先来个 Hello World。
三分钟,让你体验一把“独占”的感觉。
// 引入 Redisson 依赖 (Maven) // <dependency> // <groupId>org.redisson</groupId> // <artifactId>redisson-spring-boot-starter</artifactId> // <version>3.23.0</version> // </dependency> public class LockDemo { // 注入 Redisson 客户端 // 假设你已经配置好了 Spring Boot Starter @Autowired private RedissonClient redissonClient; public void runTask() { // 定义锁的键名,最好带上业务前缀 String lockKey = "lock:task:exclusive"; // 获取分布式锁对象 RLock rLock = redissonClient.getLock(lockKey); // 尝试获取锁 // 参数 1:等待锁的最长时间(秒) // 参数 2:锁自动过期的时间(秒) boolean isLocked = false; try { // 如果 5 秒内没抢到,就放弃 isLocked = rLock.tryLock(5, 30, TimeUnit.SECONDS); if (isLocked) { System.out.println("节点 " + getNodeId() + " 抢到了锁,开始干活"); // 模拟业务逻辑 doBusinessLogic(); } else { System.out.println("节点 " + getNodeId() + " 没抢到,去喝杯咖啡"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); System.err.println("获取锁被中断了"); } finally { // 一定要在 finally 里释放锁 // 防止业务异常导致锁无法释放 if (isLocked && rLock.isHeldByCurrentThread()) { rLock.unlock(); System.out.println("锁已释放"); } } } private String getNodeId() { return "Node-" + System.currentTimeMillis() % 1000; } private void doBusinessLogic() { // 模拟耗时操作 try { Thread.sleep(2000); } catch (Exception e) {} } }看,就这么简单。
tryLock是核心,别用lock,那个会无限阻塞。
生产环境必须设超时时间。
三、核心 API / 深水区
3.1 核心方法速查
Redisson 的 API 设计得很人性化,像操作本地锁一样。
| 方法 | 说明 | 注意事项 |
|---|---|---|
tryLock(waitTime, leaseTime, unit) | 尝试加锁 | 必须设置 leaseTime,否则看门狗不生效 |
unlock() | 释放锁 | 只有持有锁的线程才能释放 |
isLocked() | 判断是否被锁 | 只能判断当前 Redis 状态,非强一致 |
isHeldByCurrentThread() | 判断当前线程是否持有 | 释放锁前务必检查,防止误删别人的锁 |
3.2 生产级配置
线上环境,配置得细致点。
特别是超时控制和异常处理。
@Configuration public class RedissonConfig { @Bean public RedissonClient redissonClient() { Config config = new Config(); // 使用单机模式,生产请用集群模式 config.useSingleServer() .setAddress("redis://127.0.0.1:6379") // 连接池配置 .setConnectionPoolSize(10) .setConnectionMinimumIdleSize(5); return Redisson.create(config); } }异常处理方面,一定要捕获InterruptedException。
不然线程池里的线程被中断了,状态就脏了。
3.3 高级定制
有些场景,需要红锁(RedLock)。
就是同时在多个 Redis 节点上加锁。
虽然 Redisson 支持,但我不建议轻易用。
除非你的业务真的不能容忍主从切换带来的锁丢失。
大多数时候,单机 Redis 的锁已经够用了。
四、实战演练
咱们来个真实的场景。
多节点抢占“每日报表生成”任务。
只能有一个节点在跑,跑了就更新状态,别重复生成。
@Component public class ReportScheduler { @Autowired private RedissonClient redissonClient; @Autowired private ReportService reportService; // 模拟线程池任务 @Scheduled(cron = "0 0 1 * * ?") public void generateDailyReport() { String lockKey = "lock:report:generate"; RLock lock = redissonClient.getLock(lockKey); boolean acquired = false; try { // 等待 3 秒,锁有效期 60 秒 acquired = lock.tryLock(3, 60, TimeUnit.SECONDS); if (acquired) { // 双重检查,防止并发下状态已更新 if (!reportService.isReportGeneratedToday()) { System.out.println("开始生成今日报表..."); reportService.createReport(); System.out.println("报表生成完毕"); } else { System.out.println("今日报表已存在,跳过"); } } } catch (Exception e) { System.err.println("任务执行异常:" + e.getMessage()); } finally { if (acquired && lock.isHeldByCurrentThread()) { lock.unlock(); } } } }结果分析:
三台服务器同时触发定时任务。
只有一台能拿到锁,进入if块。
其他两台拿到false,直接打印跳过。
资源竞争闭环完成。
五、避坑指南与最佳实践
踩过的坑,都是真金白银换来的。
💡技巧:锁的粒度要细。
别搞个lock:all把整个系统锁死。
按业务 ID 分片,lock:report:user:123。
⚠️警告:别在锁里做远程 RPC 调用。
万一对方超时,你的锁就延期了,甚至死锁。
✅推荐:锁过期时间要大于业务最大执行时间。
估算好最坏情况,留足余量。
⚠️警告:Redis 主从切换可能导致锁丢失。
这是 CAP 理论的妥协,业务上要设计幂等性。
六、综合实战演示
最后,给一套精简、闭环的综合实战代码。
这是一个通用的分布式任务执行器。
@Component public class DistributedTaskExecutor { @Autowired private RedissonClient redissonClient; /** * 执行带锁的分布式任务 * * @param taskName 任务名称,用于生成锁 Key * @param runnable 具体任务逻辑 * @param waitTime 等待锁时间(秒) * @param leaseTime 锁持有时间(秒) */ public void executeWithLock(String taskName, Runnable runnable, long waitTime, long leaseTime) { String key = "distributed:lock:" + taskName; RLock lock = redissonClient.getLock(key); boolean isLocked = false; try { // 尝试获取锁 isLocked = lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS); if (isLocked) { System.out.println("[任务 " + taskName + "] 获取锁成功,执行中..."); runnable.run(); } else { System.out.println("[任务 " + taskName + "] 获取锁失败,其他节点正在执行"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); System.err.println("[任务 " + taskName + "] 线程被中断"); } catch (Exception e) { System.err.println("[任务 " + taskName + "] 业务执行异常: " + e.getMessage()); } finally { // 安全释放锁 if (isLocked && lock.isHeldByCurrentThread()) { lock.unlock(); System.out.println("[任务 " + taskName + "] 锁已释放"); } } } }调用方式:
@Autowired private DistributedTaskExecutor executor; public void startJob() { executor.executeWithLock("order_sync", () -> { // 这里写具体的业务代码 System.out.println("正在同步订单数据..."); }, 5, 30); }七、总结
分布式锁不是银弹,但它是解决资源竞争的必要手段。
Redisson 把复杂的事情变简单了。
记住三点:
tryLock必须设超时。unlock必须在finally里。- 业务逻辑要幂等。
把锁用好,线程池不再“打架”,线上稳如老狗。
散会。