⭐ 推荐:社区规则条款 V1.0

数据库API设计:事务边界在代码中如何控制。

东来东往
东来东往 正式会员正式会员认证极客认证极客
发布于 2026-07-31 01:23 ·90 浏览 ·0 回复
内容摘要

数据库访问层事务边界该由谁控制:作者主张事务边界应对应业务不变量、画在 Service/Application 层,Repository 只做持久化而不自行提交,并对比 DAO 内提交、Service 注解和显式事务模板三种方式,指出声明式事务易隐藏边界、长事务占用连接锁资源、事务内发消息导致数据不一致等陷阱,结论是短事务、显式边界、提交后副作用,配合幂等重试与 Outbox 等方案。

很多团队在数据库访问层封装得很漂亮:Repository、DAO、ORM 一应俱全,但一遇到“转账”“下单扣库存”这种需要原子性的场景,事务边界就开始打架。有人把 BEGIN 写在 DAO 里,有人让 Service 持有 Connection,还有人靠框架注解一层层透传。结果就是:小操作没问题,一组合就出现部分提交、连接泄漏、锁等待。数据库 API 设计里,事务边界不是语法问题,而是职责划分问题。

事务边界应该由业务用例决定

一个事务对应一个业务不变量。比如“创建订单并扣减库存并写流水”是一个用例,边界就应画在 Service/Application 层。Repository 只负责持久化,不负责提交;这样组合多个 Repository 时,仍然是一个原子操作。反之,如果 DAO 内部自行 commit,上层就失去了组合能力,事务边界被切碎,后面只能靠分布式事务或补偿去补锅。

三种常见放置方式

第一种,DAO 内提交。适合单条 SQL 的 CRUD,简单直接,但无法组合,一旦业务需要跨表跨仓储就会失控。

第二种,Service 方法注解。最常见,@Transactional 包住用例。它足够省心,但要注意自调用失效、private 方法不生效、默认只对运行时异常回滚、REQUIRES_NEW 会新开连接等问题。别把注解当成银弹。

第三种,显式事务模板或 Unit of Work。用 TransactionTemplate 或 with transaction(): ... 让用例代码明确 commit/rollback。它比注解啰嗦,但边界最清晰,适合复杂流程、跨仓储组合和需要精细控制回滚点的场景。

声明式事务的陷阱

声明式事务最大的风险是“边界被隐藏”。调用方以为只是一个查询,实际却在一个长事务里;或者在事务中调用 HTTP/RPC、发送 MQ。事务未提交时消息已发出,回滚后消息无法撤回,数据与消息就不一致了。正确做法是提交后回调,或用 Outbox 表把事件先落库,再异步投递。

另外,长事务会长时间持有连接和锁,放大死锁概率,拖垮连接池。REQUIRES_NEW 也不是随便用的,它可能拿第二条连接,在高并发下把池子耗尽。

API 设计原则

  • 让上层决定边界,底层不擅自 commit。
  • 一个事务只做数据库工作,保持短小。
  • 异常必须向上抛,统一由事务边界回滚。
  • 重试放在事务外层,且业务必须幂等。
  • 明确隔离级别和超时,别用默认值糊弄。
  • 异步/多线程不共享事务上下文。
  • 分布式场景优先 Saga、TCC、Outbox,不要硬上 XA。

一个最小示例

transactionTemplate.execute(status -> {
    orderRepo.insert(order);
    stockRepo.decrease(skuId, qty);
    outboxRepo.save(event);
    return order.id();
});

Repository 内部只有 SQL,没有 commit。边界清晰,测试也好 mock。

事务边界是 API 契约的一部分。设计时先问“这个业务用例的不变量是什么”,再决定边界画在哪。别让框架注解替你思考,也别让 DAO 偷偷提交。短事务、显式边界、提交后副作用,基本能避开大多数坑。

本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-252.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 0

还没有回复,来抢沙发~