
1. 分布式定时任务的挑战与解决方案在分布式系统中实现定时任务的精确控制一直是个经典难题。我最近在金融支付系统架构升级中就遇到了这个痛点原本单机运行的日终对账任务在迁移到Spring Cloud微服务集群后出现了多个节点重复执行的问题。这直接导致账务数据被重复处理产生了严重的业务逻辑错误。1.1 问题本质分析当我们在JAVA应用中通过Scheduled注解创建定时任务时默认情况下每个服务实例都会独立执行任务。这在分布式环境下会产生三个典型问题重复执行所有节点同时触发任务导致业务逻辑被多次执行资源竞争多个实例同时操作共享资源如数据库行锁状态不一致不同节点读取到中间状态数据以我们系统的对账任务为例伪代码如下Scheduled(cron 0 0 23 * * ?) public void reconcileAccounts() { // 读取交易记录 // 比对账户余额 // 生成差异报告 }当这个服务以3个节点部署时每天23点就会同时产生3份对账报告完全违背了业务需求。1.2 解决方案选型目前主流解决方案可分为三类方案类型代表实现优点缺点数据库锁ShedLock实现简单依赖少性能较差锁粒度粗分布式协调ZooKeeper可靠性高引入额外组件中间件特性Redis SETNX性能好需处理锁续期问题经过压测对比我们最终选择了ShedLock方案。主要基于以下考虑系统已使用MySQL不希望引入新组件对账任务执行频率低每日一次需要最小化改造现有代码2. ShedLock实现详解2.1 核心机制解析ShedLock通过创建锁表记录来实现分布式协调其工作原理可分为四个阶段锁获取阶段任务启动时尝试插入锁记录执行判定阶段检查插入结果判断是否获得锁任务执行阶段持有锁的节点执行业务逻辑锁释放阶段更新记录状态或等待自动过期锁表结构示例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 环境配置添加Maven依赖dependency groupIdnet.javacrumbs.shedlock/groupId artifactIdshedlock-spring/artifactId version4.42.0/version /dependency dependency groupIdnet.javacrumbs.shedlock/groupId artifactIdshedlock-provider-jdbc-template/artifactId version4.42.0/version /dependency创建锁表以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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;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分钟lockAtMostFor2m长任务10分钟lockAtMostFor任务时间×1.5数据库优化ALTER TABLE shedlock ADD INDEX idx_lock_until (lock_until);监控配置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 高可用方案对于关键任务建议采用双保险策略主方案ShedLock保证分布式协调备方案任务表增加执行状态检查Transactional SchedulerLock(...) public void criticalTask() { TaskRecord record taskRepository.findByTaskNameAndDate(); if (record ! null record.getStatus() Status.COMPLETED) { return; } // 执行业务逻辑 }4. 常见问题排查指南4.1 锁失效场景现象多个节点同时执行任务排查步骤检查数据库时间是否同步SELECT NOW() FROM dual; -- 在所有节点执行验证锁表记录SELECT * FROM shedlock WHERE name taskName;检查事务隔离级别需要READ_COMMITTED以上4.2 性能瓶颈处理现象任务延迟执行优化方案将锁表单独放在高性能实例调整锁超时时间SchedulerLock(lockAtMostFor ${reconcile.timeout:30m})考虑改用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.propertiesorg.quartz.jobStore.isClusteredtrue org.quartz.jobStore.clusterCheckinInterval20000数据库需要创建11张Quartz专用表适合重型调度系统。