- 文档
- 教程
- 后端
【免费下载链接】CodeGuide
:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!
在 CodeGuide 仓库的《本地任务消息组件》课程中,"动态任务补偿处理"是整个最终一致性方案的最后一块拼图:当 Spring Event 事件触发 MQ 推送或 HTTP 远程调用失败(网络超时、服务宕机、线程阻塞、流量洪峰)时,靠"本地消息表 + 定时任务补偿扫描"来反复重试,直到通知成功。读完本篇,你可以掌握组件的补偿扫描流程(门牌号 houseNumber 分片、最小游标查询、> id limit x批量拉取)、通知完成后回写任务状态的设计边界,以及下游必须做幂等的工程原因,从而能独立实现一套可靠的本地消息补偿机制。
一、为什么必须做任务补偿
本组件要解决的问题是:业务系统在完成数据库事务写库的同时,还要对外发送 MQ 消息或发起 HTTP 调用。但 MQ 的发送和 HTTP 的调用,都无法与数据库写操作处于同一个事务中,这就天然存在失败的可能:
- 网络超时;
- 下游服务宕机;
- 线程阻塞;
- 流量洪峰导致调用失败。
组件的整体链路是:上游业务通过注解(@LocalTaskMessage+ AOP)或直接调用组件服务ILocalTaskMessageHandleService,在同一个事务内完成业务数据写入与本地消息表写入;事务内写入完成后,组件同步推送 Spring Event 事件,由 trigger 层的监听器以@Async异步方式触发事务外的 MQ 推送或 HTTP 回调(HTTP 通道基于 Retrofit2 + OkHttp3 封装,MQ 通道使用 RabbitTemplate 推送)。这条链路中,从事务(业务数据 + 任务表数据写入)往后的一切操作——执行 http/mq 通知、更新数据库任务状态——都不是同一个事务的,也就是说全部有可能失败。
因此补偿机制的定位是兜底:当首次异步通知失败后,不能让这条任务记录停留在"未通知"状态,必须由定时任务持续检测本地消息表并重试通知,直到成功,从而保证消息最终一致性和业务流程的可靠执行。
二、补偿扫描流程设计:门牌号分片 + 最小游标查询
补偿的核心是一个定时任务扫描库表的过程。组件针对扫描效率做了两个关键设计。
1. 门牌号(houseNumber)分片扫描
为了提高整体扫描效率,组件设计了"门牌号"机制:可以配置多个任务,每个任务只扫描自己门牌号范围内的任务记录,多个任务并行扫描同一张表,互不重叠。
这一设计在仓库另一篇方案文档中也有明确阐述:针对一张表的扫描,如果数据量较大,又不希望只是一个任务扫描一张表,就需要多个任务扫描同一张表来加大扫描体量,此时就需要门牌号来隔离不同任务扫描的范围,避免多个任务扫描出重复的任务数据(参见 方案设计:基于库表分段扫描和数据Redis预热,优化分布式延迟任务触达时效性)。门牌号本质上把一张任务表切成了 N 个逻辑分片,N 个调度任务各自认领一个分片,从而在不引入分库分表的前提下提升了补偿扫描的吞吐量。
2. 最小游标查询 + 批量拉取
扫描库表时不是简单的limit x全表翻页,而是先根据条件获取一个最小符合条件的 id,之后以id > minId limit x的方式获取数据列表。这样每一轮扫描都从当前最小的待处理记录开始批量拉取,配合每轮处理完成后对任务状态的更新,可以保证任务按插入顺序被逐步推进处理,也避免了深分页带来的性能问题。
3. 多任务组的动态调度配置
从 组件总览文档 给出的能力清单可以确认,补偿调度的落地形态是:
- 使用
@ConfigurationProperties驱动多任务组动态调度配置,每个任务组配置自己的门牌号范围、触发方式和批次大小 limit; - 触发方式支持cron与fixedDelay两种,适配"按时间表达式扫描"和"按固定间隔持续补偿"两类场景;
- 调度底层使用
ThreadPoolTaskScheduler做线程池化调度管理,合理设置线程名与池大小,提升任务调度的可观测性与稳定性; - 数据访问层使用原生 JDBC 访问与 DAO 封装,完成插入、状态更新、分片条件查询、最小游标查询四类操作——其中后两类正是补偿扫描流程直接依赖的能力。
组件刻意选择 JDBC 而非引入 MyBatis,原因在 第3节:任务表设计和数据写入 中说明得很清楚:组件需要引入上游系统的 DataSource 数据源,与业务数据走同一个库连接操作;直接用 JDBC 是为了避免上游系统引入组件时产生 ORM 框架版本冲突,"最原始的方法,兼容性也是最好的"。
三、流程补充:通知完成后必须回写任务状态
仅靠定时扫描还不够。补偿机制生效的前提是任务状态真实反映通知结果,因此原流程需要做补充:http 调用操作、MQ 推送操作完成后,都要调用 DAO 更新本地消息表中的任务记录状态——成功或失败。
完整的状态流转是:
- Spring Event 监听器接收消息,执行通知操作(http 或 mq);
- 通知成功,更新任务记录状态为"成功";通知失败,更新为"失败";
- 定时补偿任务按门牌号分片 + 最小游标批量扫描出未成功的任务记录,重新执行通知并再次回写状态。
这里的关键认知是:事务边界只覆盖"业务数据 + 任务表数据"的写入,事务提交后的事件监听、远程调用、状态回写全部处于事务之外,任何一环失败都不会回滚已提交的业务数据——这正是本地消息表模式"宁可重复、不可丢失"的设计哲学,代价则是由补偿重试和下游幂等来消化。
四、补偿的代价:重复通知与下游幂等
补偿重试必然引入一个问题:补偿就可能重复。一次任务可能被首次异步通知执行了一次,又被定时补偿扫描再次执行——http 可能被重复调用一次,mq 可能被重复发送一次。
文档给出的工程约束是:对接下游业务时,一定要做幂等操作,例如以 OrderId 做唯一索引处理。也就是说,本地消息表模式把"消息可能丢失"问题转化为"消息可能重复"问题,而后者可以通过下游基于业务唯一键(订单号等)的幂等设计来彻底消化。这是接入本组件时下游服务必须满足的契约,而不是组件的可选建议。
五、组件定位与接入方式回顾
理解补偿机制后,可以回看组件在整体中的位置(详见 项目总览、第1节:组件需求分析、第4节:通知策略处理):
- 组件按 DDD 分层与端口-适配器模式组织,清晰划分 domain / infrastructure / trigger / config 模块:领域层负责通知服务的策略分发(按 notifyType 区分 http、mq,扩展新通道在此处添加),基础设施层完成 Retrofit2 封装的 HTTP 调用与 RabbitTemplate 的 MQ 推送,trigger 层承载 Spring Event 监听,config 层承载调度配置与切面逻辑;
- 本地消息表由引入组件的上游系统自行在自己的数据库中创建,组件以同一数据源操作,保证任务写入与业务写入处于同一事务;
- 上游可以通过自定义注解 + 切面拦截(见 第6节:切面拦截任务操作)或直接调用
handleService.acceptTaskMessage(taskMessageEntityCommand)的方式接入,入参为TaskMessageEntityCommand命令对象。
六、小结
本篇覆盖的"动态任务补偿处理"回答了本地消息表模式的三个关键问题:
- 如何高效扫描:门牌号 houseNumber 分片让多个定时任务并行扫描互不重叠,最小游标查询配合
> id limit x保证批量拉取的低成本与顺序性,@ConfigurationProperties+ThreadPoolTaskScheduler提供 cron / fixedDelay 双触发方式与 limit 批次大小的可配置调度; - 如何保证状态闭环:http、mq 通知完成后必须回写任务表状态,事务外的每一步失败都由下一轮扫描兜住;
- 如何处理重复:补偿天然可能重复通知,下游必须以业务唯一键(如 OrderId 唯一索引)做幂等。
三者合起来,构成了"本地事务写消息表 + 事件异步通知 + 分片定时补偿 + 下游幂等"的完整最终一致性方案。
- 文档
- 教程
- 后端
【免费下载链接】CodeGuide
:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!
相关推荐
CodeGuide 本地任务消息组件:通知策略处理,用策略模式落地 HTTP 与 RabbitMQ 双通道通知
CodeGuide 本地任务消息组件:通知策略处理,用策略模式落地 HTTP 与 RabbitMQ 双通道通知 本文基于 CodeGuide 开源仓库中《本地任
文档教程后端Java 本地任务消息组件需求分析:数据库事务与 MQ/HTTP 外部调用的最终一致性方案
Java 本地任务消息组件需求分析:数据库事务与 MQ/HTTP 外部调用的最终一致性方案 本篇基于 CodeGuide 仓库中《本地任务消息组件》项目的第 1
文档教程后端本地任务消息组件:让数据库事务与外部消息推送(HTTP/RabbitMQ)达成最终一致性的通用组件方案
本地任务消息组件:让数据库事务与外部消息推送(HTTP/RabbitMQ)达成最终一致性的通用组件方案 本文基于 CodeGuide 仓库中的《本地任务消息组件
文档教程后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考