SpringBoot中事务内同步处理 + 事务后异步调用外部系统的通用模式示例
SpringBoot中事务内同步处理 事务后异步调用外部系统的通用模式示例一、解决的核心问题在业务系统中经常遇到这样的需求一个用户操作既要修改本地数据库又要调用外部系统HTTP接口、第三方平台等。直接在一个事务里做这两件事会产生严重问题❌ 错误做法 开启事务 → 写本地数据库 → 调用外部HTTP接口(耗时2-5秒) → 提交事务 问题1长事务 — HTTP超时期间数据库连接和行锁被占用阻塞其他请求 问题2数据不一致 — 外部接口成功了但本地事务提交失败外部状态无法回退 问题3数据丢失 — 外部接口超时/异常导致本地事务回滚之前的写入全部丢失二、解决方案事务提交后异步调用将流程拆分为两阶段阶段1同步事务内校验 写库 注册事务后回调 阶段2异步事务外MQ消费 → 调用外部系统 → 根据结果更新状态 ┌────────────── 阶段1用户请求处理 ──────────────┐ │ │ │ 开始事务 │ │ → 前置校验权限、数据完整性等 │ │ → 准备数据生成编号、组装参数等 │ │ → 写入数据库状态设为处理中 │ │ → 注册 afterCommit 回调发送MQ消息 │ │ 提交事务 │ │ → 触发回调 → MQ消息发出 │ │ │ └────────────────────────────────────────────────────┘ │ (消息队列) │ ▼ ┌────────────── 阶段2异步消费处理 ──────────────┐ │ │ │ MQ Consumer 收到消息 │ │ → 调用外部系统HTTP/RPC │ │ → 成功更新状态为完成 │ │ → 失败更新状态为失败可重试 │ │ → 记录操作日志 │ │ │ └────────────────────────────────────────────────────┘注博客https://blog.csdn.net/badao_liumang_qizhi三、完整通用示例场景假设用户提交一份报告系统需要本地保存报告数据调用外部审批平台创建审批流程审批平台返回审批ID后本地关联保存3.1 实体和枚举// 报告状态枚举publicenumReportStatus{DRAFT(0,草稿),SUBMITTING(1,提交中),// 已提交但审批平台还没响应SUBMITTED(2,已提交),// 审批平台已确认SUBMIT_FAILED(3,提交失败);// 审批平台调用失败privateIntegercode;privateStringdesc;}// 报告实体EntityTable(namereport)publicclassReport{IdprivateIntegerid;privateStringtitle;privateStringcontent;privateIntegerstatus;// 报告状态privateStringapprovalId;// 外部审批平台返回的IDprivateLocalDateTimesubmitTime;}// 操作日志实体记录与外部系统的每次交互EntityTable(namereport_submit_log)publicclassReportSubmitLog{IdprivateIntegerid;privateIntegerreportId;privateStringrequestData;// 发送给外部的数据JSONprivateStringresponseData;// 外部返回的数据JSONprivateStringtransmitFlag;// T成功, F失败privateStringerrorMsg;// 失败原因privateLocalDateTimecreateTime;}3.2 阶段1Service 层处理用户请求ServicepublicclassReportServiceImplimplementsReportService{AutowiredprivateReportRepositoryreportRepository;AutowiredprivateReportSubmitMqSenderreportSubmitMqSender;/** * 提交报告用户直接调用的方法. * 这个方法在事务内执行只做本地数据操作。 */Transactional(rollbackForException.class)OverridepublicResultVoidsubmitReport(IntegerreportId,IntegeruserId){// 第一步前置校验 ReportreportreportRepository.findById(reportId).orElseThrow(()-newBusinessException(报告不存在));if(!ReportStatus.DRAFT.getCode().equals(report.getStatus())){thrownewBusinessException(只有草稿状态的报告才能提交);}// 校验用户权限if(!userId.equals(report.getCreatorId())){thrownewBusinessException(只有创建者才能提交);}// 第二步准备数据、修改本地状态 report.setStatus(ReportStatus.SUBMITTING.getCode());// 设为提交中report.setSubmitTime(LocalDateTime.now());reportRepository.save(report);// 第三步组装外部调用参数 ApprovalCreateParamapprovalParamnewApprovalCreateParam();approvalParam.setTitle(report.getTitle());approvalParam.setContent(report.getContent());approvalParam.setCallbackUrl(http://my-service/api/approval-callback);// 第四步注册事务提交后的回调 // 关键不在事务内发MQ而是注册事务提交成功后的回调TransactionSynchronizationManager.registerSynchronization(newTransactionSynchronization(){OverridepublicvoidafterCommit(){// 这里在事务成功提交后才执行reportSubmitMqSender.send(approvalParam,reportId);}// 如果事务回滚afterCommit 不会被调用MQ消息不会发出});returnResult.success();}}3.3 事务后回调的封装工具类上面直接写匿名类比较繁琐实际项目中通常封装一个收集器/** * 事务后置动作收集器. * 用于收集需要在事务提交后执行的动作通常是发送MQ消息. */publicclassAfterCommitActionCollectorimplementsTransactionSynchronization{privatefinalListRunnableactionsnewArrayList();/** * 添加一个事务提交后要执行的动作. */publicvoidaddAction(Runnableaction){actions.add(action);}OverridepublicvoidafterCommit(){// 事务提交成功后依次执行所有注册的动作for(Runnableaction:actions){try{action.run();}catch(Exceptione){// 记录日志但不抛异常因为本地事务已经提交了log.warn(事务后置动作执行失败,e);}}}}使用方式更简洁// 在事务方法中AfterCommitActionCollectorcollectornewAfterCommitActionCollector();TransactionSynchronizationManager.registerSynchronization(collector);// 可以注册多个动作collector.addAction(()-reportSubmitMqSender.send(approvalParam,reportId));collector.addAction(()-notificationMqSender.send(userId,报告已提交));3.4 MQ 生产者ComponentpublicclassReportSubmitMqSender{AutowiredprivateRabbitTemplaterabbitTemplate;/** * 发送MQ消息. * 消息体包含调用外部系统所需的参数和业务ID. */publicvoidsend(ApprovalCreateParamparam,IntegerreportId){MapString,ObjectmessagenewHashMap();message.put(param,param);message.put(reportId,reportId);log.info(发送报告提交MQ: reportId{},reportId);try{rabbitTemplate.convertAndSend(report.exchange,report.submit,message);}catch(Exceptione){// MQ发送失败的兜底记录日志后续由定时任务扫描提交中超时的记录来补偿log.error(报告提交MQ发送失败: reportId{},reportId,e);}}}3.5 阶段2MQ 消费者ComponentpublicclassReportSubmitMqConsumer{AutowiredprivateReportServicereportService;/** * 消费MQ消息调用外部审批平台. */RabbitListener(queuesreport.submit.queue)publicvoidconsume(MapString,Objectmessage){ApprovalCreateParamparam(ApprovalCreateParam)message.get(param);IntegerreportId(Integer)message.get(reportId);log.info(消费报告提交MQ: reportId{},reportId);// 委托给Service层处理Service中有独立事务reportService.callExternalApproval(param,reportId);}}3.6 阶段2调用外部系统的业务逻辑ServicepublicclassReportServiceImplimplementsReportService{AutowiredprivateReportRepositoryreportRepository;AutowiredprivateReportSubmitLogRepositorysubmitLogRepository;AutowiredprivateApprovalPlatformClientapprovalClient;// HTTP客户端/** * 调用外部审批平台MQ消费后执行. * 使用 REQUIRES_NEW 确保独立事务不受Consumer框架事务影响. */Transactional(propagationPropagation.REQUIRES_NEW,rollbackForException.class)OverridepublicvoidcallExternalApproval(ApprovalCreateParamparam,IntegerreportId){ReportreportreportRepository.findById(reportId).orElse(null);if(reportnull){return;// 数据被删除了直接结束}// 创建操作日志无论成败都要记录ReportSubmitLogsubmitLognewReportSubmitLog();submitLog.setReportId(reportId);submitLog.setRequestData(JsonUtil.toJson(param));submitLog.setCreateTime(LocalDateTime.now());try{// 调用外部系统 ApprovalCreateResultresultapprovalClient.createApproval(param);if(result.isSuccess()){// 成功更新本地状态report.setStatus(ReportStatus.SUBMITTED.getCode());report.setApprovalId(result.getApprovalId());submitLog.setTransmitFlag(T);submitLog.setResponseData(JsonUtil.toJson(result));}else{// 业务失败状态回退report.setStatus(ReportStatus.SUBMIT_FAILED.getCode());submitLog.setTransmitFlag(F);submitLog.setErrorMsg(result.getErrorMsg());}reportRepository.save(report);}catch(Exceptione){// 异常状态回退记录错误report.setStatus(ReportStatus.SUBMIT_FAILED.getCode());reportRepository.save(report);submitLog.setTransmitFlag(F);submitLog.setErrorMsg(e.getMessage());log.warn(调用审批平台失败: reportId{},reportId,e);}finally{// 无论如何都保存操作日志submitLogRepository.save(submitLog);}}}四、为什么用REQUIRES_NEWTransactional(propagationPropagation.REQUIRES_NEW)MQ Consumer 本身可能有事务上下文框架自动管理ACK用REQUIRES_NEW开启独立事务的好处Consumer 框架事务 │ ├── callExternalApproval() 的独立事务 │ → 成功独立提交 │ → 失败独立回滚不影响Consumer的ACK │ └── Consumer 正常结束ACK消息如果不用REQUIRES_NEW业务异常会导致 Consumer 事务回滚 → MQ消息重新入队 → 无限重试。五、失败重试机制定时扫描补偿Scheduled(fixedRate300000)// 每5分钟publicvoidretryFailedSubmissions(){// 查找提交中超过10分钟的记录可能MQ丢失了ListReportstuckReportsreportRepository.findByStatusAndSubmitTimeBefore(ReportStatus.SUBMITTING.getCode(),LocalDateTime.now().minusMinutes(10));for(Reportreport:stuckReports){// 重新发送MQreportSubmitMqSender.send(buildParam(report),report.getId());}}手动重试接口GetMapping(/retry-submit)publicResultVoidretrySubmit(RequestParamIntegerreportId){ReportSubmitLoglastLogsubmitLogRepository.findTopByReportIdOrderByCreateTimeDesc(reportId);if(lastLog!nullF.equals(lastLog.getTransmitFlag())){ApprovalCreateParamparamJsonUtil.fromJson(lastLog.getRequestData(),...);callExternalApproval(param,reportId);}returnResult.success();}六、TransactionSynchronizationManager 原理这是 Spring 提供的事务同步管理器核心机制// Spring 事务提交流程简化publicvoidcommit(){// 1. 执行业务SQLdoCommit();// 2. 提交成功后调用所有注册的 synchronization.afterCommit()for(TransactionSynchronizationsync:synchronizations){sync.afterCommit();}// 3. 最终清理for(TransactionSynchronizationsync:synchronizations){sync.afterCompletion(STATUS_COMMITTED);}}publicvoidrollback(){// 回滚时不会调用 afterCommit()doRollback();// 只调用 afterCompletionfor(TransactionSynchronizationsync:synchronizations){sync.afterCompletion(STATUS_ROLLED_BACK);}}关键保证afterCommit()只在事务成功提交后才执行。如果事务回滚这个方法不会被调用。可用的生命周期钩子方法调用时机beforeCommit(boolean readOnly)事务提交前可以抛异常阻止提交afterCommit()事务提交成功后beforeCompletion()事务完成前提交或回滚都会调afterCompletion(int status)事务完成后status 区分提交/回滚七、操作日志表的设计意义每次与外部系统的交互都记录在日志表中┌────────────────────────────────────────────┐ │ report_submit_log │ ├────────────────────────────────────────────┤ │ id - 主键 │ │ report_id - 关联业务ID │ │ request_data - 发送数据JSON │ │ response_data - 返回数据JSON │ │ transmit_flag - T成功 / F失败 │ │ error_msg - 失败原因 │ │ create_time - 操作时间 │ └────────────────────────────────────────────┘作用可追溯— 出问题时可以看到发了什么、收到什么支持重试— 从日志中取出 request_data 重新调用问题定位— 是参数错了还是外部系统挂了数据恢复— 即使状态字段被意外修改日志表能还原真实历史八、状态流转图用户操作 MQ消费后 ┌───────────────────┐ ┌──────────────────────────────┐ │ │ │ │ │ DRAFT(草稿) │ │ 调用外部接口成功 │ │ │ │ │ → SUBMITTED(已提交) │ │ │ [提交] │ │ │ │ ▼ │ │ 调用外部接口失败 │ │ SUBMITTING │─MQ─→│ → SUBMIT_FAILED(提交失败) │ │ (提交中) │ │ │ │ │ │ 可重试 → 重新进入消费流程 │ └───────────────────┘ └──────────────────────────────┘提交中是一个过渡状态表示本地已经处理完毕但外部系统还没响应。这个中间状态的存在让系统能够阻止用户重复提交状态不是DRAFT了识别卡住的记录定时扫描超时的SUBMITTING正确显示进度前端可以展示处理中九、整体时序图用户 Controller Service(事务) DB MQ Consumer 外部系统 │ │ │ │ │ │ │ │──提交请求──→│ │ │ │ │ │ │ │──调用──→ │ │ │ │ │ │ │ │──校验查询──→│ │ │ │ │ │ │←─返回数据──│ │ │ │ │ │ │──更新状态──→│ │ │ │ │ │ │ (SUBMITTING)│ │ │ │ │ │ │──注册回调──→ (暂存) │ │ │ │ │ │──提交事务──→│ │ │ │ │ │ │ │ │ │ │ │ │ │──afterCommit触发──→ │ │ │ │ │ │ │ 发消息│ │ │ │←─返回成功──│←────────────│ │ │ │ │ │ │ │ │ │──消费──→ │ │ │ │ │ │ │ │──HTTP调用──→│ │ │ │ │ │ │←─返回结果──│ │ │ │ │←─更新状态(SUBMITTED)│ │ │ │ │ │←─保存日志──────────│ │十、这个模式的适用边界适用场景本地操作和外部调用需要最终一致性不需要强一致外部调用耗时较长500ms外部系统可能不稳定需要重试需要记录操作日志用于审计和排查不适用场景需要强一致性本地和外部必须同时成功或同时失败→ 考虑分布式事务Saga/TCC外部调用极快且稳定100ms→ 可以直接在事务内调用不需要异步用户必须等待外部结果 → 同步调用 超时处理潜在风险及应对风险应对方式事务提交成功但MQ发送失败定时任务扫描卡住的中间状态记录MQ消息重复消费Consumer 做幂等处理检查状态是否已变更外部系统长时间不可用重试次数限制 人工介入接口消息顺序问题同一业务ID的消息投递到同一队列分区十一、总结这个模式的核心要点事务内只做本地操作不调用外部系统通过afterCommit保证本地写成功后才触发下一步MQ 解耦异步调用外部系统Consumer 独立事务REQUIRES_NEW处理外部调用结果中间状态 操作日志支持追踪和重试定时补偿兜底 MQ 丢失的情况本质思想是将一个需要跨系统的复杂操作拆解为多个本地原子操作通过消息队列串联通过状态字段和日志表保证可追溯和可恢复。

相关新闻

C++新手必学:用libcurl轻松实现文件下载,分钟上手!

C++新手必学:用libcurl轻松实现文件下载,分钟上手!

C新手必学:用libcurl轻松实现文件下载,分钟上手! 为什么选择libcurl?在C开发中,网络编程常常让人望而生畏——处理socket、解析HTTP协议、管理连接状态……这些细节足以让新手头疼。但有了libcurl,一切都变…

2026/8/26 5:40:59 阅读更多 →
Java集合框架实战指南:从核心原理到高频面试题解析

Java集合框架实战指南:从核心原理到高频面试题解析

1. 项目概述:一份持续更新的Java学习实战笔记最近在系统性地重温Java,起因是团队里来了几位新人,在带他们上手项目时,发现很多基础概念,比如集合框架里ArrayList和LinkedList的区别、HashMap的扩容机制,大家…

2026/8/27 3:54:26 阅读更多 →
二阶锥松弛在配电网最优潮流计算中的应用与MATLAB实现

二阶锥松弛在配电网最优潮流计算中的应用与MATLAB实现

1. 项目概述:二阶锥松弛在配电网最优潮流计算中的核心价值配电网最优潮流(Optimal Power Flow, OPF)是电力系统运行与规划中的经典问题,其核心目标是在满足电网安全约束的前提下,实现发电成本最小化或网损最小化等经济…

2026/8/9 5:18:08 阅读更多 →

最新新闻

Partmode开源CAD:浏览器里的SolidWorks替代方案体验与部署评估

Partmode开源CAD:浏览器里的SolidWorks替代方案体验与部署评估

Partmode 这个开源项目,最近引起我注意的倒不是“开源 CAD”这个概念本身,而是它的 Live browser demo——不需要安装庞大的桌面客户端,打开浏览器就能实际体验建模流程。定位上,它被看作 SolidWorks 的开源替代思路,对…

2026/8/29 20:04:25 阅读更多 →
最小生成树算法实战:Kruskal与Prim原理、实现与应用场景解析

最小生成树算法实战:Kruskal与Prim原理、实现与应用场景解析

1. 项目概述:从实际问题到最小生成树在解决现实中的网络连接问题时,我们常常会遇到一个经典场景:如何用最低的成本,将一组分散的点(比如城市、基站、服务器节点)全部连接起来,并且保证整个网络是…

2026/8/29 20:04:25 阅读更多 →
最大流算法详解:从Edmonds-Karp到最小割定理的实战指南

最大流算法详解:从Edmonds-Karp到最小割定理的实战指南

1. 项目概述:从水管网络到信息高速公路 想象一下,你所在的城市有一个庞大的自来水供水网络。水源地是几个大型水库,而千家万户则是用水终端。连接水库和用户之间的,是粗细不一、错综复杂的输水管道,每条管道在单位时间…

2026/8/29 20:04:25 阅读更多 →
最小生成树算法详解:Kruskal与Prim的核心思想、代码实现与选型指南

最小生成树算法详解:Kruskal与Prim的核心思想、代码实现与选型指南

1. 从实际问题到图论模型:为什么我们需要最小生成树?如果你做过一些关于资源分配、网络铺设或者路径规划的方案,大概率会遇到一个经典问题:如何用最低的成本,把一堆分散的点连接成一个连通的整体,并且保证任…

2026/8/29 20:04:25 阅读更多 →
每日资讯快报:Cursor 被 SpaceX 收购,OpenAI 直接断供模型~

每日资讯快报:Cursor 被 SpaceX 收购,OpenAI 直接断供模型~

今天 AI 圈最炸的只有一条:Cursor 被 SpaceX 收购,OpenAI 直接断供模型。往下还有 GitHub AI 热榜和 DeepSeek harness 插件生态的新动静,三分钟扫完。 【今日 AI 快报】 Cursor 被收购,OpenAI 断供模型:SpaceX 以 60…

2026/8/29 20:04:25 阅读更多 →
【和豆包一起工作】无限余额钱包应用

【和豆包一起工作】无限余额钱包应用

这是和豆包一起工作,开发的一个钱包应用,哪位同事或者朋友帮我验证一下收款里面的付款二维码的功能是否已经实现。 无限钱包 用户: 添加二维码给支付宝,微信付款功能 豆包: 我识别到用户的核心诉求是为支付宝和微信付款功能添加二维码&#…

2026/8/29 20:03:24 阅读更多 →

日新闻

etc目录下的profile.d文件目录设置环境变量和全局脚本shell

etc目录下的profile.d文件目录设置环境变量和全局脚本shell

一、设置环境变量etc目录下的profile.d文件目录 /etc/profile.d1、编写 vi test.sh文件内容# jdk变量 export ZHK_HOME/root export PATH$PATH:$ZHK_HOME/test # 可以取出来ZHK_HOME变量给ZZZ_HOME赋值 export ZZZ_HOME${ZHK_HOME}/test2、刷新 执行source /etc/profile 命令使…

2026/8/29 0:00:24 阅读更多 →
【JavaScript】内存管理-垃圾回收机制-内存泄露

【JavaScript】内存管理-垃圾回收机制-内存泄露

内存管理 C 语言这样的底层语言一般都有底层的内存管理接口,比如 malloc()和free()。 而 JavaScript 是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时“自动”释放。释放的过程称为垃圾回收。 整…

2026/8/29 0:00:24 阅读更多 →
Labgrid-MCP:为嵌入式硬件实验室接入AI Agent操控能力

Labgrid-MCP:为嵌入式硬件实验室接入AI Agent操控能力

Labgrid-MCP 的目标是把 MCP(Model Context Protocol)能力延伸到真实嵌入式硬件实验室:AI Agent 通过一个标准化的 MCP Server,就能查看目标板状态、控制上电断电、复位开发板、读取串口日志,甚至执行镜像刷写。对于经…

2026/8/29 0:00:24 阅读更多 →

周新闻

[光学原理与应用-521]:对光的错误理解与纠偏

[光学原理与应用-521]:对光的错误理解与纠偏

首先光是一种能量的载体和形态,宏观上观察到的光是由无数个微观的光量子组成的,每个光子在产生的瞬间,其在真空的空间中以确定不变的速度沿着一个初始的方向一直向前,在微观层面,每个光量子的运动轨迹是以波函数所展现…

2026/8/29 18:08:35 阅读更多 →
SIP通话转接原理与REFER方法实战解析

SIP通话转接原理与REFER方法实战解析

1. 通话转接不是“挂断再拨号”,而是SIP会话的动态重定向你有没有遇到过这样的场景:客服坐席A正在和客户通电话,突然需要把这通对话无缝转给专家坐席B,客户完全感知不到中间的断连——既没听到忙音,也没被要求重新拨号…

2026/8/28 23:05:07 阅读更多 →
Kolla-ansible单节点OpenStack部署实战:从环境准备到排坑指南

Kolla-ansible单节点OpenStack部署实战:从环境准备到排坑指南

1. 为什么选择Kolla-ansible来部署单节点OpenStack?如果你正在寻找一种能把OpenStack从“概念”快速变成“可用的实验环境”的方法,那么Kolla-ansible几乎是当前最主流、最省心的选择。我见过太多人卡在手动编译依赖、配置服务、处理版本冲突的泥潭里&am…

2026/8/28 19:47:53 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/29 4:34:53 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗?ncmdump解密工具帮你轻松解决这个困…

2026/8/28 17:43:04 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片:为英语学习 App 打造桌面级学习助手适用平台:HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0(API 26 Beta)新增了 AgentCard 智能体卡片能力,这是继 HMAF(鸿蒙智能体框架&#x…

2026/8/29 2:05:18 阅读更多 →