@SchedulerLock注解配置不当导致数据重复问题解决方案

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • 一、问题现象:锁了,但没完全锁?
  • 二、根因分析:你的锁可能是个“假锁”
    • 1.被忽视的lockAtMostFor属性
    • 2.锁的“租期”模型解析
  • 三、解决方案:四步彻底消灭重复数据
    • 1. 正确配置锁参数
    • 2. 添加任务幂等性校验
    • 3. 数据库唯一键兜底
    • 4. 监控与告警
  • 四、避坑总结:锁的黄金法则
  • 总结


前言

在使用Spring Boot的分布式定时任务框架(如ShedLock)时,你是否遇到过明明加了@SchedulerLock注解,却依然出现任务重复执行、数据重复生成的诡异问题?本文将从真实案例出发,深入剖析lockAtLeastFor与lockAtMostFor的配置陷阱,并提供一整套解决方案,彻底告别数据重复!


提示:以下是本篇文章正文内容,下面案例可供参考

一、问题现象:锁了,但没完全锁?

假设你的系统中有一个定时任务,每天凌晨自动统计订单数据并生成报表。为了防止多实例部署时重复执行,你使用了@SchedulerLock注解:


@Scheduled(cron = "0 0 0 * * ?")
@SchedulerLock(name = "order_report_task", lockAtLeastFor = "300000") // 锁至少5分钟
public void generateOrderReport() {
    // 生成报表逻辑(偶尔出现重复数据!)
}

诡异现象‌:

  1. 日志显示多个实例几乎同时抢到锁!
  2. 数据库中出现完全相同的两份报表数据!
  3. 问题随机发生,尤其在任务执行时间较长时!

二、根因分析:你的锁可能是个“假锁”

1.被忽视的lockAtMostFor属性

致命误区‌:只配置lockAtLeastFor,未设置lockAtMostFor!

  • ShedLock默认的lockAtMostFor值为30s!
  • 当任务执行时间 > lockAtMostFor时,锁自动失效‌,其他实例将重新获取锁执行任务
// 错误配置:lockAtMostFor使用默认30秒
@SchedulerLock(name = "order_report_task", lockAtLeastFor = "300000")

2.锁的“租期”模型解析

ShedLock的锁机制类似于租约:

  • lockAtLeastFor‌:最短租期,保证任务在此期间内不被其他节点抢占。
  • lockAtMostFor‌:最长租期,超时后强制释放锁(防止死锁)。

‌关键结论‌:
若任务实际执行时间超过lockAtMostFor,必然导致锁提前释放和任务重复!

三、解决方案:四步彻底消灭重复数据

1. 正确配置锁参数

公式‌:lockAtMostFor > 预估最大任务耗时 > lockAtLeastFor

@SchedulerLock(
    name = "order_report_task",
    lockAtLeastFor = "5m",  // 最短持有5分钟
    lockAtMostFor = "24h"   // 最大允许24小时(必须大于任务最长时间!)
)

配置建议‌

  • 监控历史任务执行时间,取最大值的‌2倍‌作为lockAtMostFor。
  • 生产环境建议lockAtMostFor不低于1小时。

2. 添加任务幂等性校验

即使锁配置正确,仍需防范极端情况(如Full GC导致执行超时)。在任务逻辑中增加‌幂等性检查‌:

public void generateOrderReport() {
    // 检查今天是否已生成报表
    LocalDate today = LocalDate.now();
    if (reportRepository.existsByReportDate(today)) {
        log.warn("今日报表已存在,跳过执行");
        return;
    }
    
    // 生成报表逻辑
}

3. 数据库唯一键兜底

在数据存储层添加‌唯一索引‌,彻底杜绝重复数据:

ALTER TABLE order_report 
ADD UNIQUE INDEX udx_report_date (report_date);

4. 监控与告警

配置任务执行时长监控,及时发现异常任务:

# Prometheus监控指标
shedlock_success_latest{name="order_report_task"} 
  - shedlock_success_latest{name="order_report_task"} offset 1h
  > 3600  # 执行时间超过1小时告警

四、避坑总结:锁的黄金法则

配置项 推荐值 错误示例
lockAtMostFor > 2倍最大任务耗时 默认30秒(导致重复)
lockAtLeastFor 预计任务耗时 + 缓冲时间(如5分钟) 不设置(可能过早释放)
幂等性检查 必须添加 依赖锁机制,无兜底
数据库唯一约束 强烈建议 无约束,依赖应用层校验

示例:

@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是防线,幂等性是底线,唯一约束是终极武器!用好这三板斧,从此告别重复数据!。

你可能感兴趣的:(开发问题解决方案,spring,boot)