前言
在使用Spring Boot的分布式定时任务框架(如ShedLock)时,你是否遇到过明明加了@SchedulerLock注解,却依然出现任务重复执行、数据重复生成的诡异问题?本文将从真实案例出发,深入剖析lockAtLeastFor与lockAtMostFor的配置陷阱,并提供一整套解决方案,彻底告别数据重复!
一、问题现象:锁了,但没完全锁?
假设你的系统中有一个定时任务,每天凌晨自动统计订单数据并生成报表。为了防止多实例部署时重复执行,你使用了@SchedulerLock注解:
java
@Scheduled(cron = "0 0 0 * * ?")
@SchedulerLock(name = "order_report_task", lockAtLeastFor = "300000") // 锁至少5分钟
public void generateOrderReport() {
// 生成报表逻辑(偶尔出现重复数据!)
}
诡异现象:
- 日志显示多个实例几乎同时抢到锁!
- 数据库中出现完全相同的两份报表数据!
- 问题随机发生,尤其在任务执行时间较长时!
二、根因分析:你的锁可能是个"假锁"
1.被忽视的lockAtMostFor属性
致命误区:只配置lockAtLeastFor,未设置lockAtMostFor!
- ShedLock默认的lockAtMostFor值为30s!
- 当任务执行时间 > lockAtMostFor时,锁自动失效,其他实例将重新获取锁执行任务
java
// 错误配置:lockAtMostFor使用默认30秒
@SchedulerLock(name = "order_report_task", lockAtLeastFor = "300000")
2.锁的"租期"模型解析
ShedLock的锁机制类似于租约:
- lockAtLeastFor:最短租期,保证任务在此期间内不被其他节点抢占。
- lockAtMostFor:最长租期,超时后强制释放锁(防止死锁)。
关键结论: 若任务实际执行时间超过lockAtMostFor,必然导致锁提前释放和任务重复!
三、解决方案:四步彻底消灭重复数据
1. 正确配置锁参数
公式:lockAtMostFor > 预估最大任务耗时 > lockAtLeastFor
java
@SchedulerLock(
name = "order_report_task",
lockAtLeastFor = "5m", // 最短持有5分钟
lockAtMostFor = "24h" // 最大允许24小时(必须大于任务最长时间!)
)
配置建议:
- 监控历史任务执行时间,取最大值的2倍作为lockAtMostFor。
- 生产环境建议lockAtMostFor不低于1小时。
2. 添加任务幂等性校验
即使锁配置正确,仍需防范极端情况(如Full GC导致执行超时)。在任务逻辑中增加幂等性检查:
java
public void generateOrderReport() {
// 检查今天是否已生成报表
LocalDate today = LocalDate.now();
if (reportRepository.existsByReportDate(today)) {
log.warn("今日报表已存在,跳过执行");
return;
}
// 生成报表逻辑
}
3. 数据库唯一键兜底
在数据存储层添加唯一索引,彻底杜绝重复数据:
java
ALTER TABLE order_report
ADD UNIQUE INDEX udx_report_date (report_date);
4. 监控与告警
配置任务执行时长监控,及时发现异常任务:
java
# Prometheus监控指标
shedlock_success_latest{name="order_report_task"}
- shedlock_success_latest{name="order_report_task"} offset 1h
> 3600 # 执行时间超过1小时告警
四、避坑总结:锁的黄金法则
配置项 | 推荐值 | 错误示例 |
---|---|---|
lockAtMostFor | > 2倍最大任务耗时 | 默认30秒(导致重复) |
lockAtLeastFor | 预计任务耗时 + 缓冲时间(如5分钟) | 不设置(可能过早释放) |
幂等性检查 | 必须添加 | 依赖锁机制,无兜底 |
数据库唯一约束 | 强烈建议 | 无约束,依赖应用层校验 |
示例:
java
@Scheduled(cron = "0 0 0 * * ?")
@SchedulerLock(
name = "order_report_task",
lockAtLeastFor = "5m", // 保证短任务不被重复
lockAtMostFor = "24h" // 覆盖任务最大执行时间
)
public void generateOrderReport() {
// 1. 幂等性检查
if (checkReportExists()) return;
// 2. 执行核心逻辑
doGenerateReport();
// 3. 记录执行成功(ShedLock自动更新)
}
总结
分布式定时任务的锁配置绝非简单的"加锁"即可,需要结合业务场景合理设计。记住:lockAtMostFor是防线,幂等性是底线,唯一约束是终极武器!用好这三板斧,从此告别重复数据!。