Spring定时任务设计

2 阅读4分钟

在 Spring Cloud 微服务架构中,多个实例部署同一个微服务时,若需要执行定时任务(如每天凌晨清理日志、同步数据等),必须确保同一时间只有一个实例执行该任务,否则会造成重复执行、数据不一致等问题。为实现这一点,可以借助 Redis 分布式锁 来协调多个实例。 下面将详细介绍如何在 Spring Cloud 微服务中使用 Redis 实现分布式定时任务的方案。

一、整体思路

  • 使用 @Scheduled 注解定义定时任务。

  • 在定时任务执行前尝试获取 Redis 分布式锁。

  • 若成功获取锁,则执行业务逻辑;否则跳过本次执行。

  • 任务执行完成后释放锁。

  • 锁需设置合理的过期时间(防止死锁)。

  • 使用 Redis 的 SET key value NX PX 命令原子性地加锁。

二、核心组件设计

1. Redis 分布式锁工具类

查看项目,我的在 common 包中已经存在了可以使用的 redis 组件

建议去git仓库找开源的代码或者直接让ai写个工具类

其中存在 set() 方法可以很方便设置 redis 分布式事务锁

/**
 * 普通缓存放入并设置时间
 *
 * @param key   键
 * @param value 值
 * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
 * @return true成功 false 失败
 * @author c02822
 */
public boolean set(String key, Object value, long time) {
    try {
        if (time > 0) {
            redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
        } else {
            set(key, value);
        }
        return true;
    } catch (Exception e) {
        log.error("except.", e);
        return false;
    }
}

2. 定时任务服务类

需要的定时任务:每天在固定的某一个时间将所有产品,所有环境的所有产品的工作负载状态存储到一个数据库表中。

以上面这个需求为例,首先需要明确的是用于存储数据的数据表。为此你新建两个数据库,建表语句如下:

-- 1. 工作负载每日快照表
CREATE TABLE workload_daily_snapshot (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    snapshot_date DATE NOT NULL, -- 快照日期
    namespace VARCHAR(100) NOT NULL,
    workload_name VARCHAR(255) NOT NULL,
    workload_type VARCHAR(50) NOT NULL, -- 'Deployment'/'StatefulSet'
    product VARCHAR(100), -- 产品线
    environment VARCHAR(50), -- 环境
    desired_replicas INT NOT NULL DEFAULT 0,
    available_replicas INT NOT NULL DEFAULT 0,
    image VARCHAR(500),
    cpu_request_m DECIMAL(10,2), -- CPU请求(毫核)
    memory_request_mi INT, -- 内存请求(MiB)
    cpu_limit_m DECIMAL(10,2), -- CPU限制(毫核)
    memory_limit_mi INT, -- 内存限制(MiB)
    created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_date_namespace (snapshot_date, namespace),
    INDEX idx_product_env (product, environment),
    UNIQUE KEY uk_daily_workload (snapshot_date, namespace, workload_name, workload_type)
);

-- 2. Pod每日状态表
CREATE TABLE pod_daily_status (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    snapshot_date DATE NOT NULL,
    namespace VARCHAR(100) NOT NULL,
    pod_name VARCHAR(255) NOT NULL,
    workload_name VARCHAR(255), -- 所属工作负载
    workload_type VARCHAR(50), -- 所属工作负载类型
    status VARCHAR(50) NOT NULL, -- Running/Pending/Failed等
    node_name VARCHAR(255),
    restart_count INT DEFAULT 0,
    product VARCHAR(100),
    environment VARCHAR(50),
    created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_date_status (snapshot_date, status),
    INDEX idx_workload (namespace, workload_name),
    INDEX idx_node (node_name)
);

此外我写了个定时任务的模板


// ScheduledTaskService.java
@Slf4j

@Service

public class ScheduledTaskService {


    /**
    * redis工具包
    */
    @Autowired
    @Qualifier("redisUtil")
    private RedisUtil redisUtil;


    // 每天凌晨 2 点执行

    @Scheduled(cron = "0 0 2 * * ?")

    public void dailyCleanupTask() {

        String lockKey = "lock:daily_cleanup_task";

        long expireTime = 60 * 10; // 10 分钟,根据任务最大执行时间设定

        if (!redisUtil.hasKey(lockKey)) {
           redisUtil.set(lockKey, 1 , expireTime);
           // 业务逻辑
           。。。
        }
}

3. 配置类(启用定时任务 & Redis)



// AppConfig.java

package com.example.config;



import org.springframework.context.annotation.Configuration;

import org.springframework.scheduling.annotation.EnableScheduling;



@Configuration

@EnableScheduling

public class AppConfig {

    // 启用 @Scheduled

}

三、注意事项

锁的过期时间:必须大于任务最大可能执行时间,避免任务未完成锁就过期,导致其他节点抢占锁造成并发执行。

锁的 key 命名规范:如 lock:service_name:task_name,便于管理和监控。

异常处理:务必在 finally 块中释放锁,防止因异常导致锁未释放。

避免主从切换下的锁失效:Redis 主从异步复制可能导致锁在主节点加锁后,主挂掉,从节点未同步锁信息就被提升为主,此时另一个客户端可能再次加锁。若对一致性要求极高,应使用 RedLock 算法或改用 ZooKeeper / etcd。但在大多数业务场景中,单 Redis 实例 + 合理过期时间已足够。

四、总结

通过 Redis 分布式锁,我们可以在 Spring Cloud 多实例环境下安全地执行定时任务,确保幂等性和唯一性。本方案轻量、易理解,适用于大多数业务场景。