Spring Boot分布式定时任务防重复执行方案对比
发布时间:2026/9/18 6:03:07 锦皓数字建站

1. 分布式定时任务重复执行问题解析在Spring Boot应用中Scheduled注解是开发者常用的定时任务实现方式。当应用以单实例运行时定时任务会按照预设的cron表达式或固定间隔稳定执行。然而在分布式部署场景下如果同一个服务部署了多个实例每个实例都会独立执行相同的定时任务这就导致了任务重复执行的问题。举个例子假设我们有一个每天凌晨统计昨日订单量的定时任务。在单机环境下这个任务会准时运行一次。但如果部署了三个实例三个实例会同时执行这个统计任务不仅造成资源浪费更可能导致数据统计结果异常如重复计算。这种问题在以下场景尤为突出电商平台的库存同步任务金融系统的日终对账作业日志分析系统的数据聚合任务2. 解决方案技术选型分析2.1 方案对比方案实现复杂度可靠性功能完整性适用场景Spring Redis Lock中等高需要手动续期已有Redis环境的中型系统Redisson低很高自动续期对可靠性要求高的生产环境ShedLock很低高自动管理轻量级定时任务场景2.2 选型建议对于大多数Java项目我的实践经验是如果是新项目且对Redis有依赖优先考虑Redisson方案如果项目已经使用Spring Integration可以沿用其LockRegistry对于简单的定时任务管控ShedLock是最轻量级的选择3. Spring Redis Lock方案实现3.1 环境准备首先需要添加Spring Integration Redis依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-integration/artifactId /dependency dependency groupIdorg.springframework.integration/groupId artifactIdspring-integration-redis/artifactId /dependency3.2 配置Redis锁注册中心Configuration public class RedisLockConfig { Bean public RedisLockRegistry redisLockRegistry(RedisConnectionFactory connectionFactory) { // 锁有效期默认60秒 return new RedisLockRegistry(connectionFactory, scheduler-lock, 60000); } }3.3 定时任务实现Service public class ScheduledService { private final RedisLockRegistry lockRegistry; private final ScheduledExecutorService scheduler Executors.newScheduledThreadPool(1, r - { Thread t new Thread(r, lock-renewer); t.setDaemon(true); return t; }); Scheduled(fixedDelay 5000) public void distributedTask() throws Exception { Lock lock lockRegistry.obtain(inventory-sync); if (!lock.tryLock()) { log.info(未获取到锁跳过执行); return; } // 启动锁续期任务 ScheduledFuture? renewTask scheduler.scheduleAtFixedRate(() - { try { lockRegistry.renewLock(inventory-sync); } catch (Exception e) { log.error(锁续期失败, e); } }, 2000, 2000, TimeUnit.MILLISECONDS); try { log.info(开始执行定时任务...); // 模拟业务处理 Thread.sleep(8000); } finally { renewTask.cancel(true); lock.unlock(); log.info(任务执行完成释放锁); } } }3.4 关键点解析锁续期机制由于Redis锁有过期时间长时间任务需要定期续期守护线程使用守护线程执行续期任务避免应用关闭时线程无法退出异常处理务必在finally块中释放锁避免死锁实际项目中我曾遇到过一个坑当任务执行时间超过锁有效期且未正确续期时多个节点可能同时获取锁。因此必须确保续期间隔小于锁过期时间。4. Redisson方案实现4.1 依赖配置dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version3.23.4/version /dependency4.2 定时任务实现Service RequiredArgsConstructor public class OrderStatsService { private final RedissonClient redissonClient; Scheduled(cron 0 0 2 * * ?) public void dailyOrderStats() { RLock lock redissonClient.getLock(order-stats-lock); try { // 尝试获取锁等待最多1秒锁有效期30秒 if (lock.tryLock(1, 30, TimeUnit.SECONDS)) { try { log.info(开始执行订单统计...); // 统计逻辑 Thread.sleep(15000); } finally { lock.unlock(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }4.3 方案优势自动续期Redisson内部通过看门狗机制自动续期可重入锁支持同一线程多次获取锁丰富的API提供tryLock、lockInterruptibly等多种加锁方式生产环境建议配置lockWatchdogTimeout默认30秒这个参数决定了锁自动续期的间隔时间。5. ShedLock方案实现5.1 基础配置dependency groupIdnet.javacrumbs.shedlock/groupId artifactIdshedlock-spring/artifactId version5.6.0/version /dependency dependency groupIdnet.javacrumbs.shedlock/groupId artifactIdshedlock-provider-jdbc-template/artifactId version5.6.0/version /dependency5.2 数据库表准备CREATE TABLE shedlock ( name VARCHAR(64) PRIMARY KEY, lock_until TIMESTAMP(3) NULL, locked_at TIMESTAMP(3) NULL, locked_by VARCHAR(255) );5.3 Spring配置Configuration EnableScheduling EnableSchedulerLock(defaultLockAtMostFor 30s) public class SchedulerConfig { Bean public LockProvider lockProvider(DataSource dataSource) { return new JdbcTemplateLockProvider( JdbcTemplateLockProvider.Configuration.builder() .withJdbcTemplate(new JdbcTemplate(dataSource)) .usingDbTime() .build() ); } }5.4 定时任务示例Service public class LogCleanupService { Scheduled(cron 0 0 3 * * ?) SchedulerLock( name logCleanupTask, lockAtLeastFor 10s, lockAtMostFor 30s ) public void cleanUpLogs() { log.info(开始清理日志文件...); // 清理逻辑 } }5.5 参数说明lockAtMostFor最坏情况下锁的保留时间lockAtLeastFor保证任务最短执行时间name锁的唯一标识不同任务不能重复6. 生产环境注意事项6.1 Redis方案优化建议连接池配置确保Redis连接池大小足够spring: redis: lettuce: pool: max-active: 20 max-idle: 10锁命名规范建议使用业务模块:任务名称的格式String lockKey order:stats: LocalDate.now();6.2 监控与告警建议对以下指标进行监控锁获取失败次数任务执行耗时锁续期异常可以通过Micrometer添加监控Metrics.counter(scheduler.lock.failure, task, order-stats) .increment();6.3 常见问题排查锁无法释放检查finally块是否执行网络是否正常任务重复执行检查锁有效期是否大于任务执行时间性能问题减少不必要的锁竞争优化任务执行效率7. 方案扩展与进阶7.1 多级降级策略在实际项目中我实现过一个多级降级的定时任务管控优先使用Redisson分布式锁失败后降级到数据库悲观锁最后使用本地锁保证至少单节点执行public void executeWithFallback(Runnable task) { try { // 尝试Redisson锁 if (tryRedissonLock()) { task.run(); return; } // 降级到数据库锁 if (tryDatabaseLock()) { task.run(); return; } // 最后使用本地锁 synchronized (this) { task.run(); } } finally { releaseLocks(); } }7.2 动态调整策略通过配置中心实现运行时调整Scheduled(fixedDelayString ${task.interval:5000}) SchedulerLock( name dynamicTask, lockAtMostForString ${task.lock.time:30s} ) public void dynamicTask() { // 任务逻辑 }8. 技术原理深度解析8.1 Redis分布式锁实现原理Redisson的分布式锁基于Redis的SETNX命令实现使用Hash结构存储锁信息通过Lua脚本保证原子性看门狗机制定期续期默认每10秒检查一次8.2 ShedLock工作机制ShedLock的执行流程任务启动时尝试插入锁记录如果记录已存在且未过期跳过执行执行完成后更新锁记录时间如果任务崩溃锁会根据lockAtMostFor自动释放8.3 时钟同步问题在分布式环境中各节点时钟不同步可能导致锁提前释放任务重复执行解决方案使用NTP服务同步时钟ShedLock的usingDbTime()使用数据库时间Redisson使用Redis服务器时间9. 性能优化实践9.1 锁粒度控制根据业务场景选择合适的锁粒度粗粒度整个任务加锁简单但并发度低细粒度对处理的数据分片加锁复杂但并发度高// 细粒度锁示例 public void processOrders(ListOrder orders) { orders.forEach(order - { String lockKey order:process: order.getId(); RLock lock redissonClient.getLock(lockKey); try { if (lock.tryLock(1, TimeUnit.SECONDS)) { try { // 处理单个订单 } finally { lock.unlock(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); }9.2 批量任务优化对于批量处理任务可以采用分页加锁每页数据单独加锁分布式队列使用Redis List或KafkaScheduled(fixedRate 5000) public void batchProcess() { int page 0; int size 100; while (true) { String lockKey batch:lock: page; RLock lock redissonClient.getLock(lockKey); if (lock.tryLock()) { try { ListData batch fetchData(page, size); if (batch.isEmpty()) break; processBatch(batch); page; } finally { lock.unlock(); } } else { // 锁竞争时随机退避 Thread.sleep(100 new Random().nextInt(100)); } } }10. 真实案例分享在某电商平台项目中我们遇到了促销活动期间定时任务重复执行的问题。最初使用的是简单的Spring Redis Lock方案但在大促期间出现了以下问题Redis连接数不足导致锁获取失败任务执行时间不稳定导致锁提前释放多个服务实例竞争激烈最终我们采用的解决方案升级为Redisson并优化连接池配置实现动态锁超时时间根据历史执行时间自动调整增加熔断机制当失败率过高时暂停任务优化后的指标对比指标优化前优化后任务成功率85%99.9%Redis连接数峰值200稳定50平均执行延迟2s500ms关键优化代码片段// 动态锁超时实现 private long calculateLockTime(String taskName) { // 获取历史执行时间的P99值 long p99 statsService.getExecutionTimeP99(taskName); // 基础缓冲时间 long buffer 5000; return p99 buffer; } Scheduled(fixedDelay 10000) public void dynamicLockTask() { long lockTime calculateLockTime(inventorySync); RLock lock redissonClient.getLock(inventorySync); try { if (lock.tryLock(1, lockTime, TimeUnit.MILLISECONDS)) { // 任务逻辑 } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }这个案例给我的启示是分布式定时任务的可靠性不能只依赖单一机制需要根据实际业务场景设计多层次的保障策略。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。