Spring-Scheduled多实例重复执行怎么办-Redis分布式锁如何只让一个节点运行 📅 发布时间:2026/8/29 20:19:17 👁 浏览次数: Spring Scheduled 多实例重复执行定时任务如何保证只有一个节点运行Spring Scheduled 在单实例中按计划执行服务扩容后每个实例都会执行同一任务。重复发券、重复结算和重复扫描并不是定时框架故障而是部署模型发生了变化。关键风险用 Redis 锁只让一个节点进入任务是一种轻量方案但还要考虑任务超过锁租期、节点宕机、重入和补偿。调度互斥只能保证“尽量一个执行者”不能替代业务幂等。MetaLite 将分布式互斥封装为任务可复用的底座能力。本文先界定定时任务在集群中的正确语义再用锁实现说明哪些问题由基础设施解决、哪些必须由业务保证。一、Spring Scheduled 为什么天然会重复Scheduled由当前 Spring ApplicationContext 注册和触发。它不知道同一个服务在其他机器上是否还有实例也没有内置 leader 选举。因此扩容后每个实例都拥有一份相同调度计划。这不是 Spring 的 Bug而是进程内调度器的正常语义。需要集群只执行一次时必须增加跨实例协调机制例如Redis 锁数据库抢占记录调度平台leader 选举消息队列派发。MetaLite 为轻量 Spring Scheduled 场景选择了可选 Redis 锁。二、为什么不是在每个 Job 里手写加锁如果每个任务都写if(!locker.tryLock(key)){return;}try{runBusiness();}finally{locker.unlock(key,value);}很容易出现某个任务忘记 finally、使用固定锁值或直接 DEL。MetaLite 把所有Scheduled方法统一拦截为JOB_RUN入口Around(annotation(org.springframework.scheduling.annotation.Scheduled))publicObjectaroundSpringSchedulerJob(ProceedingJoinPointpjp){returnentranceMethodAspectAround(pjp,AspectTypeEnum.JOB_RUN);}SpringScheduledJobExecuteHandler在前置阶段抢锁后置阶段解锁JobRunLogger复用统一入口日志语义。业务方法只需声明调度与锁策略。三、一个集群任务怎样声明当前示例RedisLock(keylock:job:pwdExpireCheckJob,expireMillis60000L,autoRenewaltrue)Scheduled(cron0 0 5 * * ?)publicvoidcheckPwdExpire(){// 业务处理}三个实例同时进入前置处理只有一个能通过 RedisSET NX获得锁。未获得锁的实例返回已有其他实例执行该 Job它不会继续调用业务方法。这是一种抢占式执行没有固定 leader谁先拿到锁谁执行本次任务。四、Redis 模块关闭时会发生什么Job 配置对RedisLocker使用可选注入。前置处理器当前逻辑是if(redisLockernull){returnResp.ok();}也就是说Redis 模块关闭时RedisLock不会阻止 Job 执行。每个服务实例都会继续运行自己的任务。这保证无 Redis 环境仍能启动但安全语义是 fail-open。对于“重复执行绝对不可接受”的结算类任务更合理的选择可能是启动失败或本次任务失败而不是静默退回多实例重复执行。是否允许降级应成为任务级配置而不是所有 Job 共用一个决定。五、没抢到锁是失败还是正常跳过当前未获得锁时返回一个错误Resp统一切面不会执行目标方法但仍会进入日志处理。从技术上看本实例没有完成任务从集群语义看只要另一个实例正在执行这又可能是正常状态。监控应该区分SKIPPED_LOCKED → 其他实例已执行本实例正常跳过 FAILED → 本应执行但业务异常如果把所有未抢锁都计入错误率多实例越多告警越严重如果全部忽略又可能掩盖所有实例都没有成功完成任务。完整治理需要一个集群级任务运行记录而不只是每实例日志。六、自动续期能否保护整个任务当前锁默认按 TTL 的 30% 周期续期并通过 Lua 比较锁值后再延长。但续期次数存在上限不是任务不结束就无限续期的 watchdog。续期还使用秒级EXPIRE配置的毫秒会除以 1000。因此autoRenewaltrue不能直接推导出“任意长任务都不会丢锁”。任务执行时间必须有上限锁保护窗口必须经过计算并监控执行时长超过 TTL 或达到最大续期次数的情况。对于持续数小时、需要断点恢复的任务专业调度平台或带租约的任务表通常比一个短 TTL Redis 锁更合适。七、实例在业务执行中崩溃会怎样持锁实例崩溃后finally 无法解锁但 Redis TTL 最终会释放锁。问题是其他实例已经在本次触发时刻因为抢锁失败而跳过不会自动等锁过期后补跑。所以当前方案提供的是同一次调度触发尽量只有一个实例执行它不提供任务认领、失败重试、故障转移和补偿调度。若任务必须“最终至少成功一次”还需要运行记录与补偿机制例如记录计划时间、状态、执行实例、开始结束时间并由扫描任务重新认领超时记录。八、为什么给 Scheduled 配独立线程池Spring 默认调度器容易因单线程任务阻塞让无关 Job 相互等待。MetaLite 在SchedulingConfigurer中设置taskRegistrar.setTaskScheduler(ThreadPoolManager.createScheduledThreadPool(springScheduledJobScheduler,CPU_CORE_NUMBER));这样不同任务可以并发触发线程名也能标识调度来源。但线程池扩大只解决本 JVM 内任务互相阻塞不解决同一个 Job 多实例重复。并发调度与分布式互斥是两个不同层次。另外当前定时线程池工厂虽然接收拒绝策略参数实际固定使用 CallerRunsPolicy这项行为应与方法签名保持一致。九、固定延迟、固定频率和 Cron 要考虑什么不同调度语义对锁的影响不同Cron各实例在相近时刻竞争fixedRate任务执行时间超过周期时可能形成追赶压力fixedDelay下一次时间通常基于上一次完成不同实例时钟偏差竞争时间可能错开。锁 Key 还要决定是否包含计划批次。固定 Key 能保证同一时刻只有一个任务但前一次异常长运行可能阻止下一批。加入业务日期或计划时间能区分批次却可能让两批任务并行。Key 的设计必须结合任务是否允许跨批次并发。十、哪些任务不应该只靠 Scheduled Redis 锁以下场景更需要完整调度系统任务必须保证至少成功一次需要分片并行需要手工重跑与补数据需要 DAG 依赖任务执行数小时需要可视化运行记录和告警需要故障实例自动转移执行权。Scheduled RedisLock更适合执行时间较短、下一周期可自然补偿、调度关系简单的集群任务。十一、判断任务治理是否完成的三个问题每个定时任务上线前至少回答它应该每实例执行还是集群一次执行节点崩溃后本批任务需要补跑吗重复执行时业务是否仍然幂等MetaLite 当前通过 JOB_RUN 切面、Redis 锁、统一日志和独立调度线程池解决了轻量场景中的多实例抢占执行。它没有把 Redis 锁包装成完整调度平台也不应该这样宣传。真正可靠的任务仍需要业务幂等、执行记录和明确的失败恢复目标。框架简介MetaLite 是面向企业生产环境的新一代 Java 微服务技术底座。系列文章重点分享代码背后的设计思路、技术取舍与工程实践。源码基线JDK 21、Spring Boot 3.2.9、Spring Cloud 2023.0.1、Spring Cloud Alibaba 2023.0.1.3具体组件版本以项目backend-bom为准。作者简介15 年 Spring 体系企业级开发经验专注于 Java 微服务架构、工程治理与生产实践。持续更新MetaLite 系列内容将持续更新围绕核心设计、源码链路、技术取舍与生产实践展开。欢迎关注作者及时获取后续内容。在线演示演示地址: https://admin.metalite.top/演示账号: guess演示密码: admin2026