目录
[2.1、Redis 分布式锁](#2.1、Redis 分布式锁)
[2.2、Quartz 集群模式](#2.2、Quartz 集群模式)
[2.3、Kubernetes CronJob](#2.3、Kubernetes CronJob)
前言:
为什么分布式定时任务成为云原生时代的刚需?
在单体架构时代,一个 @Scheduled 注解就能搞定所有定时任务。但随着微服务、容器化、Kubernetes 的普及,应用部署从"单机"走向"多副本集群",定时任务的重复执行问题日益凸显。

根据 CNCF 2023 年报告,83% 的企业已在生产环境使用多副本部署 ,而其中 67% 曾因定时任务重复执行导致数据异常(如重复发券、重复扣款)。
💡 核心矛盾 :Spring 的 @Scheduled是 JVM 级别的,而现代应用是集群级别的。
1、问题背景
为什么 @Scheduled在集群中会重复执行?
1.1、@Scheduled
1.开启定时任务
java
@SpringBootApplication
@EnableScheduling // ← 关键注解!
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2.编写定时任务
java
@Component
public class MyTask {
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void dailyReport() {
log.info("生成日报...");
// 业务逻辑
}
@Scheduled(fixedRate = 30000) // 每30秒执行一次
public void healthCheck() {
log.info("健康检查...");
}
}
优点:简单、轻量、无需额外依赖;
致命缺陷 :每个节点都会执行!
1.2、技术原理
Spring 的定时任务基于 TaskScheduler接口 ,默认实现是 ConcurrentTaskScheduler,其底层依赖 JDK 的ScheduledExecutorService。
代码如下所示:
java
// Spring 内部创建调度器
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(1); // 默认单线程
return scheduler;
}
- 每个 Spring Boot 应用启动时,都会创建独立的线程池和调度器
- 调度信息仅存在于本 JVM 内存中,不与其他节点共享
- 当集群有 N 个节点时,任务会被执行 N 次
🖼️ 架构示意:
节点 A: [JVM 内存] → 调度器 → 执行任务
节点 B: [JVM 内存] → 调度器 → 执行任务 ← 无感知对方存在!
节点 C: [JVM 内存] → 调度器 → 执行任务
为什么不能简单用****synchronized ?
- synchronized 是 JVM 级别锁,无法跨进程
- 集群中每个节点是独立 JVM,锁互不影响
- 分布式系统必须用分布式协调机制
1.3、故障场景
- 财务对账:每天凌晨 2 点跑批,结果对了 3 次账
- 优惠券发放:用户收到 3 张相同券
- 日志清理:多个节点同时删文件,导致 I/O 飙升
2、解决方案
为了解决上述分布式系统的定时任务重复问题,可以从以下方向进行治理。
2.1、Redis 分布式锁
1.核心挑战与设计原则
| 挑战 | 解决方案 |
|---|---|
| 死锁 | 设置合理过期时间(TTL) |
| 误删锁 | 锁值唯一(UUID),释放时校验 |
| 锁续期 | 任务执行时间 > TTL 时,需自动续期 |
| 原子性 | 获取/释放锁必须原子操作 |
如下所示:

2.适用场景
- 已有 Redis 集群
- 任务执行时间 < 锁过期时间
- 中小型项目,追求简单高效
3.实现步骤
1. 添加依赖
XML
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2. 编写分布式任务类
步骤 1:定义锁工具类
java
@Component
@Slf4j
public class RedisDistributedLock {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
/**
* 尝试获取锁
* @param lockKey 锁名称
* @param requestId 请求ID(唯一标识)
* @param expireTime 过期时间(毫秒)
* @return 是否获取成功
*/
public boolean tryLock(String lockKey, String requestId, long expireTime) {
try {
// SET key value NX PX milliseconds
Boolean result = redisTemplate.execute(
(RedisCallback<Boolean>) connection -> {
Object nativeConnection = connection.getNativeConnection();
if (nativeConnection instanceof Jedis) {
return ((Jedis) nativeConnection)
.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
} else if (nativeConnection instanceof LettuceConnection) {
// Lettuce 实现略(生产建议统一用 Jedis 或 Lettuce)
throw new UnsupportedOperationException("Lettuce not supported");
}
return false;
}
);
return LOCK_SUCCESS.equals(result);
} catch (Exception e) {
log.error("获取Redis锁异常", e);
return false;
}
}
/**
* 释放锁(Lua脚本保证原子性)
*/
public boolean releaseLock(String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
try {
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey),
requestId
);
return result != null && result == 1L;
} catch (Exception e) {
log.error("释放Redis锁异常", e);
return false;
}
}
}
步骤 2:实现分布式任务
java
@Component
@Slf4j
public class DailyReportTask {
@Autowired
private RedisDistributedLock redisLock;
private static final String LOCK_KEY = "lock:daily_report";
private static final long LOCK_EXPIRE = 5 * 60 * 1000; // 5分钟
@Scheduled(cron = "0 0 2 * * ?")
public void execute() {
String requestId = UUID.randomUUID().toString();
if (redisLock.tryLock(LOCK_KEY, requestId, LOCK_EXPIRE)) {
try {
log.info("✅ 获得分布式锁,开始执行日报任务 | requestId={}", requestId);
generateReport();
} catch (Exception e) {
log.error("❌ 任务执行异常", e);
// 可选:发送告警
} finally {
// 释放锁
if (redisLock.releaseLock(LOCK_KEY, requestId)) {
log.info("🔓 成功释放锁 | requestId={}", requestId);
} else {
log.warn("⚠️ 释放锁失败(可能已过期)| requestId={}", requestId);
}
}
} else {
log.info("🔒 未获得锁,跳过本次执行");
}
}
private void generateReport() {
// 模拟耗时业务
try {
Thread.sleep(30_000); // 30秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.info("📊 日报生成完成");
}
}
3.高级优化:Redlock 算法(防 Redis 单点故障)
当 Redis 是单机或主从架构时,主节点宕机可能导致锁丢失, Redis 官方推荐 Redlock 算法(需 5 个独立 Redis 实例)。
如下所示:

代码如下所示:
java
// 使用 Redisson 实现 Redlock
RLock lock1 = redissonClient1.getLock("myLock");
RLock lock2 = redissonClient2.getLock("myLock");
RLock lock3 = redissonClient3.getLock("myLock");
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
boolean isLocked = redLock.tryLock(500, 30000, TimeUnit.MILLISECONDS);
if (isLocked) {
// 执行业务
}
} finally {
redLock.unlock();
}
⚠️ 注意 :Redlock 性能较低,仅在极高可靠性要求场景使用。
4.关键注意事项
- 锁过期时间 > 任务最大执行时间(防死锁)
- 锁值唯一 :建议用
UUID避免误删(本例简化) - 异常处理:任务失败也要释放锁
2.2、Quartz 集群模式
如下所示:

1.适用场景
- 企业级应用
- 需要复杂调度(如动态修改 cron)
- 要求高可靠、持久化
2.实现步骤
1. 添加依赖
XML
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
2. 配置 application.yml
bash
spring:
quartz:
job-store-type: jdbc # 使用数据库存储
properties:
org:
quartz:
scheduler:
instanceName: MyClusteredScheduler
instanceId: AUTO
jobStore:
class: org.quartz.impl.jdbcjobstore.JobStoreTX
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
tablePrefix: QRTZ_
isClustered: true # ← 关键!开启集群模式
3. 定义 Job
java
public class DailyReportJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) {
log.info("🚀 Quartz 执行日报任务");
// 业务逻辑
}
}
4.注册 Job 和 Trigger
java
@Configuration
public class QuartzConfig {
@Bean
public JobDetail reportJobDetail() {
return JobBuilder.newJob(DailyReportJob.class)
.withIdentity("dailyReportJob")
.storeDurably()
.build();
}
@Bean
public Trigger reportTrigger() {
return TriggerBuilder.newTrigger()
.forJob(reportJobDetail())
.withIdentity("dailyReportTrigger")
.withSchedule(CronScheduleBuilder.cronSchedule("0 0 2 * * ?"))
.build();
}
}
2.3、Kubernetes CronJob
整体结构如下所示:

1.适用场景
- 应用已容器化,运行在 K8s 集群
- 任务独立,无需与主应用共享状态
2.YAML 示例
bash
apiVersion: batch/v1
kind: CronJob
metadata:
name: daily-report
spec:
schedule: "0 2 * * *" # UTC 时间
concurrencyPolicy: Forbid # 禁止并发执行
jobTemplate:
spec:
template:
spec:
containers:
- name: report
image: your-registry/your-app:latest
args: ["--task=daily-report"]
restartPolicy: OnFailure
3.优势
- 天然单实例执行
- 与 K8s 生态无缝集成
- 资源隔离,不影响主应用
2.4、数据库唯一约束
1.适用场景
- 无 Redis,只有数据库
- 任务按天/小时执行,可构造唯一键
2.表结构
sql
CREATE TABLE task_execution (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
task_name VARCHAR(100) NOT NULL,
exec_day DATE NOT NULL, -- 按天去重
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE uk_task_day (task_name, exec_day)
);
3.代码
java
@Scheduled(cron = "0 0 2 * * ?")
public void execute() {
try {
taskExecutionMapper.insert("daily_report", LocalDate.now());
// 成功插入 → 本节点执行
doBusiness();
} catch (DuplicateKeyException e) {
log.info("任务已被执行,跳过");
}
}
小结:

终极建议:
- 优先用 Redis 分布式锁 ------ 简单可靠,覆盖大多数场景
- 任务必须幂等 ------ 即使重复执行也不产生副作用
- 监控 + 告警 ------ 任务失败/未执行要及时通知
- 避免在定时任务中做耗时操作 ------ 考虑异步化
记住 :"分布式系统的复杂性,不在于技术多高级,而在于如何用最简单的方式解决问题。"
参考文章: