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

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

相关推荐
贫民窟的勇敢爷们4 小时前
SpringBoot整合AOP切面编程实战,实现日志统一记录+接口权限校验
java·spring boot·spring
candyTong4 小时前
Claude Code Agent Teams:多 Agent 协作的生命周期与实现机制
后端·架构
AC赳赳老秦4 小时前
供应链专员提效:OpenClaw自动跟踪物流信息、更新库存数据,异常自动提醒
java·大数据·服务器·数据库·人工智能·自动化·openclaw
迈巴赫车主5 小时前
Java基础:list、set、map一遍过
java·开发语言
灵犀学长5 小时前
基于 Spring ThreadPoolTaskScheduler + CronTrigger 实现的动态定时任务调度系统
java·数据库·spring
好家伙VCC6 小时前
【无标题】
java
小碗羊肉7 小时前
【JavaWeb | 第十一篇】文件上传(本地&阿里云OSS)
java·阿里云·servlet
吾疾唯君医7 小时前
Java SpringBoot集成积木报表实操记录
java·spring boot·spring·导出excel·积木报表·数据文件下载
Byron Loong8 小时前
【c++】为什么有了dll和.h,还需要包含lib
java·开发语言·c++
hexu_blog8 小时前
vue+java实现图片批量压缩
java·前端·vue.js