为什么远程调用别包进 Spring 事务里

为什么远程调用别包进 Spring 事务里

这两天看 interview-guide 里的 Agent 执行链路,我又把一个老问题重新确认了一遍:@Transactional 不是"整段流程保护罩",它更适合包住短而确定的本地状态变更,不适合顺手把远程模型调用、工具调用这类慢操作一起包进去。

很多人第一次写这类流程时,很容易觉得"这一整轮要么全成功,要么全失败",于是想把 chat() 整体放进一个事务里。直觉上很完整,实际上风险很高。因为数据库事务一旦拉长,连接、锁、行版本和回滚成本都会一起拉长;而远程调用偏偏最不稳定,慢的时候慢,失败的时候还不一定能按你的预期回滚。

interview-guide 这里的处理我觉得很对路。AgentOrchestrator.chat() 在注释里写得很明确:远程模型调用和工具调用都放在事务外,只在 turn 的开始和结束阶段做持久化,见 interview-guide/app/src/main/java/interview/guide/modules/agent/service/AgentOrchestrator.java:93。对应的本地持久化被拆到了 AgentSessionService.startTurn()waitForApproval()completeTurn()failTurn() 这些短事务里,见 interview-guide/app/src/main/java/interview/guide/modules/agent/service/AgentSessionService.java:91:117:179:213

事务里最怕的不是报错,是"慢且不确定"

如果你把一次远程调用包进事务,最直接的问题不是"失败会不会回滚",而是这段时间数据库资源一直被你占着。连接不会因为你在等 HTTP 响应就自动释放,锁也不会因为你在等模型吐字就先让别人用。并发一上来,吞吐就会掉得很明显。

这还只是性能问题。更麻烦的是语义问题。数据库事务只能回滚数据库里的状态,回滚不了已经发出去的外部副作用。比如消息已经投出、第三方接口已经收到了请求、远程工具已经执行了一半,这时候你本地事务回滚了,也只是把"我这里的记录"抹掉,不代表外部世界真的回到原点。

所以面试里如果有人问"为什么不要把 RPC、HTTP、消息发送包进事务",核心答案其实就两句:

  • 事务持有时间会被外部依赖放大,带来锁等待、连接占用和超时风险。
  • 数据库事务不能回滚外部副作用,强行包进去只会制造"本地回滚了,但外部已经动了"的假一致性。

更稳的做法,是把事务收缩到状态切换点

这个项目里,startTurn() 只做几件本地且必要的事:回收过期 turn、拒绝并发冲突、创建新 turn、先落用户消息,再刷新 session 更新时间,见 interview-guide/app/src/main/java/interview/guide/modules/agent/service/AgentSessionService.java:92-112。这就是一个很典型的"短事务开场"。

后面的模型决策、工具执行、审批恢复都在事务外跑。等结果拿到了,再用另一个短事务做收口。比如 completeTurn() 的顺序是先写 memory,再追加 assistant 消息,最后推进 turn 终态,见 interview-guide/app/src/main/java/interview/guide/modules/agent/service/AgentSessionService.java:180-208。这个顺序很有工程味,因为它不是追求"一个大事务包住一切",而是追求"每次落库时都尽量保证局部状态自洽"。

审批挂起那段也一样。AgentApprovalRuntimeService.parkTurnForApproval() 用一个本地事务把 trace、approval、turn 一次性推进到 WAITING_APPROVAL,但它明确不把远程模型调用或工具执行包进数据库事务,见 interview-guide/app/src/main/java/interview/guide/modules/agent/service/AgentApprovalRuntimeService.java:19-21:32-74。这其实就是把"状态切换原子化"和"外部执行解耦"分开做。

真正该追求的不是"大一统回滚",而是可恢复

很多系统走到后面,都会从"幻想一次事务包完全部流程"转向"承认流程会中断,所以把恢复路径设计好"。这个仓库里的 turn、approval、trace 其实就是这个思路。

远程调用放事务外,并不等于放弃一致性,而是把一致性的重点从"单事务全包"转成"关键状态有明确边界,失败后能恢复、能补偿、能重试"。比如 turn 有 RUNNINGWAITING_APPROVALCOMPLETEDFAILED,审批恢复时还会先 claim 执行权,避免重复推进同一个 turn,见 interview-guide/app/src/main/java/interview/guide/modules/agent/service/AgentSessionService.java:162-175。这比一个长事务更接近真实线上系统。

所以如果把这件事压缩成一句面试回答,我会这么说:

"@Transactional 适合保护短事务里的本地状态一致性,不适合包远程调用。远程调用会拉长锁持有时间,而且数据库事务回滚不了外部副作用。更稳的做法是把事务收缩到状态变更点,用显式状态机、补偿和恢复机制处理长链路。"

这类题表面上是事务题,本质上其实是你有没有真的做过线上链路题。🙂

相关推荐
xieliyu.1 小时前
Java算法精讲:双指针(三)
java·开发语言·算法
星辰徐哥1 小时前
Spring Boot 微服务架构设计与实现
spring boot·后端·微服务
星辰徐哥1 小时前
Spring Boot 数据导入导出与报表生成
spring boot·后端·ui
明夜之约1 小时前
Spring Boot 自动装配源码
java·spring boot·后端
Leaton Lee1 小时前
Spring Boot分层架构详解:从Controller到Service再到Mapper的完整流程
java·spring boot·后端·架构
Micro麦可乐1 小时前
Spring Boot 实战:从零设计一个短链系统(含完整代码与数据库设计)
数据库·spring boot·后端·哈希算法·雪花算法·短链系统
Jinkxs1 小时前
Resilience4j- 与 Spring Boot 快速集成:自动配置与基础注解使用
java·spring boot·后端
毕设源码_郑学姐1 小时前
计算机毕业设计springboot网络相册设计与实现 基于Spring Boot框架的在线相册管理系统开发与应用 Spring Boot驱动的网络影集设计与实践
spring boot·后端·课程设计
辣机小司1 小时前
【踩坑记录:Spring Boot 配置文件读取值不一致?警惕 YAML 的“八进制陷阱”与 SnakeYAML 版本之谜】
java·spring boot·后端·yaml·踩坑记录
码农阿豪1 小时前
从零到一:Spring Boot快速接入金仓数据库实战
数据库·spring boot·后端