分布式定时任务重复执行与故障恢复

分布式定时任务重复执行与故障恢复

一、业务背景

某金融系统每天凌晨需要为大量用户计算收益,并更新账户余额。

随着业务规模扩大,系统部署了多个工作节点,由调度中心分配任务。

某天凌晨,系统出现以下问题:

  1. 部分用户的收益被重复计算。
  2. 部分用户没有更新收益。
  3. 部分任务执行时间过长,影响后续任务。
  4. 工作节点执行到一半突然宕机,调度中心无法确定哪些账户已经处理完成。

核心目标:保证任务能够可靠执行、故障后能够恢复,并确保收益不会重复入账或遗漏。


二、整体解决方案

采用以下机制协同保障:

  • 任务分片:提高大批量任务的处理效率。
  • 任务执行记录表:持久化任务状态和处理进度。
  • Redis 分布式锁:减少多个节点同时执行同一任务的情况。
  • 幂等机制:防止同一笔业务重复生效。
  • 数据库唯一约束:从数据层防止重复创建同一笔业务记录。
  • 数据库事务:保证收益入账和处理记录的一致性。
  • 超时检测、重试与补偿:恢复异常中断的任务。
  • 定期对账:发现并修复漏处理或错误处理的业务。

这些机制各司其职,不能简单地相互替代。


三、任务分片:解决大批量任务的处理效率问题

假设系统需要处理 100 万个账户。

如果由一个工作节点串行处理全部账户,任务耗时可能过长。

可以将任务拆分成多个批次:

任务 ID 处理范围 状态
TASK_001 账户 1~10000 成功
TASK_002 账户 10001~20000 成功
TASK_003 账户 20001~30000 执行中
TASK_004 账户 30001~40000 待执行

多个工作节点可以并行处理不同批次。

设计注意事项

  1. 分片之间应尽可能互不重叠。
  2. 每个分片需要有唯一的任务标识。
  3. 分片状态必须持久化,不能只保存在工作节点内存中。
  4. 应根据数据库负载、任务耗时和数据量合理设置分片大小。
  5. 任务分片解决的是处理效率问题,不能单独解决重复执行问题。

四、任务执行记录表:实现故障恢复

1. 为什么需要任务记录表?

假设某个工作节点处理到一半突然宕机。

如果系统只知道任务处于「执行中」,却没有保存处理进度,那么恢复时就不知道哪些账户已经完成处理。

因此,需要将执行状态持久化到数据库。

可以设计两类记录。

任务批次表:

记录任务的整体状态,例如:

  • 任务 ID
  • 业务日期
  • 分片编号
  • 执行状态
  • 执行节点
  • 开始时间
  • 最近进度更新时间
  • 完成时间
  • 重试次数

账户级处理记录表:

记录每个账户在指定计息业务中的处理状态,例如:

  • 账户 ID
  • 计息日期
  • 收益类型
  • 业务唯一标识
  • 处理状态
  • 处理时间

账户级记录可以帮助系统精确识别哪些账户已经完成,哪些账户尚未完成。

如果采用更小的独立提交批次,也可以使用批次级进度记录减少记录数量,但必须保证恢复粒度与实际提交粒度匹配。

2. 如何恢复任务?

假设 100 万个账户中,80 万个已经成功完成,20 万个尚未完成。

恢复流程如下:

  1. 调度中心检测到任务异常或长时间没有进展。
  2. 将任务标记为待恢复,并确认原执行者不再拥有有效的执行权限。
  3. 查询持久化的任务状态和账户处理记录。
  4. 找出尚未完成的账户或子批次。
  5. 将剩余工作重新分配给可用的工作节点。
  6. 工作节点处理任务时再次执行幂等校验。
  7. 全部处理完成后,更新任务状态为成功。

核心原则:恢复任务时,以持久化的业务状态为依据,而不是单纯根据工作节点是否返回成功响应来判断。


五、幂等:防止重复计息

1. 为什么需要幂等?

假设账户 A 在某天应获得 100 元收益。

第一次执行时:

  1. 收益成功入账。
  2. 数据库事务提交成功。
  3. 工作节点突然宕机,没有向调度中心返回成功响应。

调度中心认为任务可能失败,于是重新分配任务。

如果新节点再次入账,账户就会获得 200 元收益。

因此,不能只依赖任务执行状态防止重复处理,还必须保证业务操作本身具有幂等性。

2. 设计稳定的业务唯一标识

可以根据业务规则设计唯一键:

账户ID + 计息日期 + 收益类型

例如:

ACCOUNT_001 + 2026-10-21 + DAILY_INTEREST

同一账户在同一计息日期、同一收益类型下的同一笔计息业务,无论重试多少次,都应该能够识别为同一笔业务。

注意:

  • 不能在每次重试时随意生成新的业务唯一标识。
  • 如果业务允许同一天内多个独立计息批次,唯一键需要纳入批次或版本等业务维度。
  • 不能为了方便而将所有操作错误地视为同一笔业务。

3. 数据库唯一约束

例如:

UNIQUE(account_id, interest_date, interest_type)

这个约束能够防止相同业务标识对应的成功记录被重复创建。

但必须注意:唯一约束本身不能保证账户余额不会被重复更新。

它必须与事务和正确的业务执行流程结合使用。


六、数据库事务:保证收益入账与处理记录一致

这是整个设计中最重要的部分之一。

假设系统需要完成两项操作:

  1. 更新账户收益或余额。
  2. 写入计息成功记录。

如果这两个操作不在同一个事务中,就可能出现部分成功的情况。

错误方案 A:先入账,再记录成功

text 复制代码
更新账户收益
提交成功

工作节点宕机

写入处理记录

此时,收益已经入账,但处理记录缺失。

恢复任务时,系统可能再次入账。

错误方案 B:先记录成功,再入账

text 复制代码
写入处理记录
提交成功

工作节点宕机

更新账户收益

此时,系统可能认为任务已完成,但实际收益并没有入账。

正确方案:在同一数据库事务中完成

text 复制代码
BEGIN TRANSACTION

1. 尝试创建唯一的计息业务记录
2. 确认本事务成功取得该笔业务的处理权
3. 更新账户收益或余额
4. 将计息业务记录更新为成功

COMMIT

如果任何一步失败,整个事务回滚。

如果两个工作节点并发执行,只有成功取得该笔业务处理权的事务才能执行入账;另一方需要根据唯一约束冲突及已提交的业务结果进行处理。

这里有两个关键要求:

  • 只有取得处理权的事务才能更新余额。
  • 业务记录与余额更新必须在同一事务中提交。

如果数据库事务已经提交,但工作节点在收到提交结果之前宕机,恢复时应查询数据库中的真实结果,不能直接认定事务失败。

这才是避免重复入账的核心保障。


七、Redis 分布式锁:减少并发竞争

1. 为什么使用分布式锁?

当多个工作节点同时收到同一笔任务时,可以使用 Redis 分布式锁,减少重复执行。

例如:

text 复制代码
服务器 A 获取锁 → 成功
服务器 B 获取同一把锁 → 失败
服务器 A 执行任务 → 完成
服务器 A 释放锁

锁的粒度应根据任务特点设计,可以是任务级或分片级。

2. 为什么不能只依赖 Redis 锁?

考虑以下场景:

  1. 服务器 A 获取锁。
  2. A 执行时间过长,锁自动过期。
  3. 服务器 B 获取同一把锁。
  4. A 和 B 同时继续处理任务。

此时,两个节点可能同时操作数据库。

即使使用锁自动续期,也仍然需要考虑进程暂停、网络异常等情况。

3. 推荐的保障组合

机制 职责
Redis 分布式锁 减少并发竞争
数据库唯一约束 防止同一业务记录重复创建
数据库事务 保证业务结果与处理记录一致
幂等机制 让重复请求不会重复产生业务效果
任务状态管理 支持任务恢复和进度跟踪

对于要求较高的金融业务,还可以根据系统架构引入数据库条件更新、租约校验或 fencing token 等机制,避免失去执行权的旧节点继续写入。

Redis 锁是一种并发控制手段,不应成为金融业务正确性的唯一保障。


八、超时检测、重试与补偿

1. 超时检测

调度中心定期检查任务是否长时间没有进展。

例如:

  • 任务一直处于执行中。
  • 最近进度更新时间过旧。
  • 执行节点已经失联。

满足条件时,将任务识别为疑似异常任务。

但不能只因为心跳超时,就立即认定原任务已经停止。

原节点可能只是网络延迟,仍在执行任务。

因此,在重新分配任务前,需要结合租约、执行权校验或其他协调机制,确保原节点失去继续写入的权限。

2. 失败重试

对可恢复的临时故障,可以进行有限次数的重试。

例如:

  • 数据库短暂不可用。
  • 网络请求超时。
  • 依赖服务短暂异常。

应设置重试次数、退避间隔和最大执行时长,避免大量任务同时重试,进一步加剧系统压力。

3. 补偿机制

如果重试多次仍然失败,可以将任务记录为异常,交由补偿任务继续处理。

补偿前应先确认:

  • 业务是否已经成功提交。
  • 是否存在尚未完成的事务。
  • 是否需要重新计算收益。
  • 是否可以安全地再次执行。

补偿任务本身也必须具备幂等性。


九、对账:发现并修复业务不一致

即使系统已经具备幂等、事务、分布式锁和任务恢复机制,仍然建议建立定期对账机制。

对账可以检查:

  • 应计息的账户是否全部完成。
  • 计息业务记录与实际入账结果是否一致。
  • 是否存在缺失的收益流水。
  • 是否存在重复入账。
  • 账户余额是否与有效的业务流水一致。

发现异常后,应根据权威业务数据和流水记录进行核对。

如果发现收益漏记,可以补记;如果发现重复入账,则应根据业务规则进行冲正或生成反向调整流水,而不是直接修改余额掩盖问题。

所有修复操作同样需要幂等,并保留审计记录。


十、任务执行时间过长:先定位瓶颈,再扩容

任务执行缓慢时,不能简单地认为增加服务器就能解决问题。

应先分析瓶颈:

问题 可能的解决方案
单节点处理数据量过大 任务分片
数据库查询缓慢 SQL 优化、索引优化
数据库锁竞争严重 优化事务范围、调整并发度
大量逐条写入 合理使用批量操作
工作节点 CPU 或内存不足 增加工作节点或扩容
下游服务成为瓶颈 限流、批量处理、异步化

扩容时还应考虑数据库连接数、数据库吞吐量和下游服务的承载能力。

否则,增加工作节点可能使数据库压力更大,反而导致任务整体变慢。


十一、Outbox 在定时计息场景中的作用

Outbox 并不是所有定时任务都必须使用的机制。

如果计息结果和处理记录位于同一个数据库中,可以通过数据库事务保证两者的一致性,通常不需要专门引入 Outbox。

但如果计息完成后还需要发送 MQ 消息通知其他系统,例如:

  • 通知报表系统。
  • 通知账户流水服务。
  • 触发收益明细生成。
  • 更新下游业务系统。

就可以使用 Transactional Outbox。

在同一个数据库事务中:

  1. 保存计息结果。
  2. 写入待发送事件。

随后由后台发布程序将事件发送到 MQ。

发送成功后,再更新 Outbox 记录状态。

如果发送成功后、更新发送状态前程序宕机,事件可能再次发送,因此消费者仍然需要实现幂等处理。

Outbox 主要解决业务事务与事件发布之间的可靠性问题,而不是替代任务执行记录表或计息业务幂等。


十二、完整故障恢复流程

假设某天凌晨需要处理 100 万个账户,系统已经成功完成 80 万个账户。

随后,某个工作节点突然宕机。

推荐的处理流程如下:

text 复制代码
调度中心分配计息任务
        ↓
将任务拆分成多个批次
        ↓
工作节点获取任务执行权
        ↓
读取账户处理状态
        ↓
执行计息业务
        ↓
在数据库事务中:
    创建或取得唯一业务记录
    更新账户收益
    记录计息成功状态
        ↓
事务提交成功
        ↓
更新批次进度
        ↓
所有账户完成?
   ↙          ↘
  否            是
  ↓             ↓
继续处理      标记任务成功

如果工作节点宕机:

text 复制代码
检测到任务超时或节点异常
        ↓
确认原节点不再拥有有效执行权
        ↓
读取任务记录与账户处理记录
        ↓
识别未完成的账户或批次
        ↓
重新分配剩余任务
        ↓
执行幂等校验
        ↓
已成功的业务跳过
未完成的业务继续执行
        ↓
全部完成后进行必要的对账

需要特别注意:即使某个账户的业务记录显示成功,也应保证这个状态与真实入账结果具有事务一致性,不能只依赖一个可能滞后的批次进度字段。


十三、关键知识点总结

知识点 解决的问题
任务分片 提高大批量任务的处理效率
任务执行记录表 记录任务状态和执行进度
账户级处理记录 精确识别已完成与未完成的业务
幂等 防止同一笔业务重复生效
稳定的业务唯一标识 识别重试请求是否属于同一笔业务
数据库唯一约束 防止重复创建相同业务记录
数据库事务 保证收益入账与处理记录一致
Redis 分布式锁 减少多个节点同时执行同一任务
超时检测与重试 发现异常并恢复执行
补偿机制 处理多次重试仍然失败的任务
对账机制 发现和修复业务结果不一致
Transactional Outbox 可靠关联业务事务与事件发布

最重要的三个原则

原则一:任务可以重复执行,但业务结果不能重复生效。

原则二:任务进度必须持久化,系统不能依赖工作节点的内存状态进行故障恢复。

原则三:分布式锁负责减少竞争,数据库事务、唯一约束与幂等机制共同保障业务正确性。

相关推荐
Sayai2 小时前
Kafka 磁盘空间治理实战:retention 保留策略、log.dirs 在线扩容、分区跨盘迁移与不停机整体换盘
大数据·运维·分布式·kafka·消息队列
爱吃牛肉的大老虎3 小时前
Redis之分布式 Session问题
数据库·redis·分布式
阿标在干嘛3 小时前
分布式锁的“场景适配“:我们为什么在部分链路中选择了数据库乐观锁?
数据库·分布式
磐链科技4 小时前
Web3区块链开发需要哪些核心技术?解析分布式网络、数字钱包与跨链交互方案
分布式·web3·区块链
爱吃火鸡面呀5 小时前
PySpark 入门实战:从 DataFrame 到 SQL、DSL 与窗口函数
分布式·spark
德迅--文琪5 小时前
新一代智能分布式云接入:应用加速守护 TCP 业务安全与体验
分布式·tcp/ip·安全
凯凯姜5 小时前
高并发秒杀库存超卖
分布式
一条泥憨鱼21 小时前
【Redis | 黑马点评】分布式锁;Lua脚本;Redisson与看门狗 |源码万字详解
redis·分布式·lua
stark张宇1 天前
库存扣减的“锁”事:悲观锁、乐观锁、分布式锁
redis·分布式·mysql