分布式定时任务解决方案:ShedLock实现与优化
2026/7/23 14:30:58 网站建设 项目流程

1. 分布式定时任务的挑战与解决方案

在分布式系统中实现定时任务的精确控制一直是个经典难题。我最近在金融支付系统架构升级中就遇到了这个痛点:原本单机运行的日终对账任务,在迁移到Spring Cloud微服务集群后,出现了多个节点重复执行的问题。这直接导致账务数据被重复处理,产生了严重的业务逻辑错误。

1.1 问题本质分析

当我们在JAVA应用中通过@Scheduled注解创建定时任务时,默认情况下每个服务实例都会独立执行任务。这在分布式环境下会产生三个典型问题:

  1. 重复执行:所有节点同时触发任务,导致业务逻辑被多次执行
  2. 资源竞争:多个实例同时操作共享资源(如数据库行锁)
  3. 状态不一致:不同节点读取到中间状态数据

以我们系统的对账任务为例,伪代码如下:

@Scheduled(cron = "0 0 23 * * ?") public void reconcileAccounts() { // 读取交易记录 // 比对账户余额 // 生成差异报告 }

当这个服务以3个节点部署时,每天23点就会同时产生3份对账报告,完全违背了业务需求。

1.2 解决方案选型

目前主流解决方案可分为三类:

方案类型代表实现优点缺点
数据库锁ShedLock实现简单,依赖少性能较差,锁粒度粗
分布式协调ZooKeeper可靠性高引入额外组件
中间件特性Redis SETNX性能好需处理锁续期问题

经过压测对比,我们最终选择了ShedLock方案。主要基于以下考虑:

  1. 系统已使用MySQL,不希望引入新组件
  2. 对账任务执行频率低(每日一次)
  3. 需要最小化改造现有代码

2. ShedLock实现详解

2.1 核心机制解析

ShedLock通过创建锁表记录来实现分布式协调,其工作原理可分为四个阶段:

  1. 锁获取阶段:任务启动时尝试插入锁记录
  2. 执行判定阶段:检查插入结果判断是否获得锁
  3. 任务执行阶段:持有锁的节点执行业务逻辑
  4. 锁释放阶段:更新记录状态(或等待自动过期)

锁表结构示例:

CREATE TABLE shedlock( name VARCHAR(64) PRIMARY KEY, lock_until TIMESTAMP(3) NULL, locked_at TIMESTAMP(3) NULL, locked_by VARCHAR(255) );

2.2 具体实现步骤

2.2.1 环境配置
  1. 添加Maven依赖:
<dependency> <groupId>net.javacrumbs.shedlock</groupId> <artifactId>shedlock-spring</artifactId> <version>4.42.0</version> </dependency> <dependency> <groupId>net.javacrumbs.shedlock</groupId> <artifactId>shedlock-provider-jdbc-template</artifactId> <version>4.42.0</version> </dependency>
  1. 创建锁表(以MySQL为例):
CREATE TABLE shedlock ( name VARCHAR(64) NOT NULL, lock_until TIMESTAMP(3) NOT NULL, locked_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), locked_by VARCHAR(255) NOT NULL, PRIMARY KEY (name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2.2.2 Spring Boot集成

配置类示例:

@Configuration @EnableScheduling @EnableSchedulerLock(defaultLockAtMostFor = "10m") public class SchedulerConfig { @Bean public LockProvider lockProvider(DataSource dataSource) { return new JdbcTemplateLockProvider( JdbcTemplateLockProvider.Configuration.builder() .withJdbcTemplate(new JdbcTemplate(dataSource)) .usingDbTime() // 使用数据库时间避免时钟不同步 .build() ); } }
2.2.3 定时任务改造

原始任务改造后:

@Scheduled(cron = "0 0 23 * * ?") @SchedulerLock( name = "accountReconciliation", lockAtLeastFor = "5m", lockAtMostFor = "60m" ) public void reconcileAccounts() { // 业务逻辑保持不变 }

关键参数说明:

  • name:全局唯一的锁标识
  • lockAtLeastFor:最短持有时间(防止过早释放)
  • lockAtMostFor:最大持有时间(防止死锁)

3. 生产环境优化实践

3.1 性能调优技巧

  1. 锁超时设置:根据任务执行时间动态调整

    • 短任务(<1分钟):lockAtMostFor="2m"
    • 长任务(>10分钟):lockAtMostFor="任务时间×1.5"
  2. 数据库优化

    ALTER TABLE shedlock ADD INDEX idx_lock_until (lock_until);
  3. 监控配置

    @Bean public LockProvider lockProvider(DataSource dataSource) { return new JdbcTemplateLockProvider( Configuration.builder() .withJdbcTemplate(new JdbcTemplate(dataSource)) .withTableName("system_shedlock") // 自定义表名 .withColumnNames(new ColumnNames("lock_name", "lock_timeout", "lock_acquired", "lock_owner")) .withIsolationLevel(Connection.TRANSACTION_READ_COMMITTED) .build() ); }

3.2 高可用方案

对于关键任务建议采用双保险策略:

  1. 主方案:ShedLock保证分布式协调
  2. 备方案:任务表增加执行状态检查
    @Transactional @SchedulerLock(...) public void criticalTask() { TaskRecord record = taskRepository.findByTaskNameAndDate(); if (record != null && record.getStatus() == Status.COMPLETED) { return; } // 执行业务逻辑 }

4. 常见问题排查指南

4.1 锁失效场景

现象:多个节点同时执行任务
排查步骤

  1. 检查数据库时间是否同步
    SELECT NOW() FROM dual; -- 在所有节点执行
  2. 验证锁表记录
    SELECT * FROM shedlock WHERE name = 'taskName';
  3. 检查事务隔离级别(需要READ_COMMITTED以上)

4.2 性能瓶颈处理

现象:任务延迟执行
优化方案

  1. 将锁表单独放在高性能实例
  2. 调整锁超时时间
    @SchedulerLock(lockAtMostFor = "${reconcile.timeout:30m}")
  3. 考虑改用Redis实现
    @Bean public LockProvider lockProvider(RedisTemplate redisTemplate) { return new RedisLockProvider(redisTemplate.getConnectionFactory()); }

4.3 锁监控方案

建议通过Spring Actuator添加健康检查:

@Bean public HealthIndicator lockHealthIndicator(LockProvider lockProvider) { return () -> { try { lockProvider.lock(new LockConfiguration( Instant.now(), "health_check", Duration.ofSeconds(1), Duration.ZERO )).ifPresent(Lock::unlock); return Health.up().build(); } catch (Exception e) { return Health.down(e).build(); } }; }

5. 替代方案对比

5.1 XXL-JOB方案

适用于需要集中管理的场景:

@XxlJob("accountReconciliation") public void reconcileAccounts() { // 通过XXL-JOB控制台管理触发 }

优势:

  • 提供可视化任务管理
  • 支持故障转移
  • 完善的日志追踪

5.2 Redis分布式锁

适合高频短任务:

private final RedissonClient redisson; @Scheduled(...) public void task() { RLock lock = redisson.getLock("taskLock"); try { if (lock.tryLock(0, 30, TimeUnit.SECONDS)) { // 业务逻辑 } } finally { lock.unlock(); } }

5.3 Quartz集群方案

需要配置quartz.properties:

org.quartz.jobStore.isClustered=true org.quartz.jobStore.clusterCheckinInterval=20000

数据库需要创建11张Quartz专用表,适合重型调度系统。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询