为什么远程调用别包进 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 有 RUNNING、WAITING_APPROVAL、COMPLETED、FAILED,审批恢复时还会先 claim 执行权,避免重复推进同一个 turn,见 interview-guide/app/src/main/java/interview/guide/modules/agent/service/AgentSessionService.java:162-175。这比一个长事务更接近真实线上系统。
所以如果把这件事压缩成一句面试回答,我会这么说:
"@Transactional 适合保护短事务里的本地状态一致性,不适合包远程调用。远程调用会拉长锁持有时间,而且数据库事务回滚不了外部副作用。更稳的做法是把事务收缩到状态变更点,用显式状态机、补偿和恢复机制处理长链路。"
这类题表面上是事务题,本质上其实是你有没有真的做过线上链路题。🙂