go-zero的聚合事务边界由开发者显式定义并强制执行,遵循“一个聚合根+一次事务提交”原则,禁止绕过聚合根直接操作DAO,跨聚合需最终一致性。

go-zero 的聚合事务边界不是靠框架自动划定的,而是由开发者在领域建模阶段显式定义、并在数据访问层强制执行的。它本质是「一个聚合根 + 一次事务提交」的强绑定关系,不是语法糖,也不是装饰器能解决的问题。
聚合根必须作为事务唯一入口
所有对子实体(如 OrderItem、ShippingAddress)的写操作,不能绕过聚合根直接调用 DAO 方法。否则就等于跳过业务规则校验,破坏一致性。
- 错误写法:
itemModel.Update(ctx, item)单独执行 —— 这会绕过订单总金额重算、库存预占等聚合内规则 - 正确路径:必须走
orderModel.Save(ctx, order),其中order是完整聚合对象,含全部子实体和状态 -
Save方法内部需开启事务,并按顺序校验、更新、插入 —— 顺序本身也是业务规则的一部分(比如先锁库存再生成订单项)
事务生命周期必须与聚合生命周期对齐
聚合根的创建、修改、删除,必须对应事务的 Begin → 操作 → Commit/Rollback 全过程。不能把多个聚合混在一个事务里,也不能把一个聚合拆到多个事务中。
- 常见误用:在同一个事务里同时保存两个独立订单(
orderA和orderB)—— 违反「单聚合单事务」原则,任一失败会导致另一个也回滚,扩大故障面 - 正确做法:每个聚合根各自封装自己的事务逻辑,例如
orderModel.CreateWithTx内部调用db.Begin(),不暴露裸*sql.Tx - go-zero 的
StartSession仅提供会话上下文,真正事务控制仍需在聚合方法内显式调用WithTransaction并传入聚合操作闭包
跨聚合操作必须退化为最终一致性
当业务需要联动多个聚合(如创建订单后扣减库存),不能强行用本地事务包裹两个聚合根。这是分布式事务的典型场景,必须接受「中间态」存在。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 不允许:
tx.Exec("UPDATE orders..."); tx.Exec("UPDATE inventory...")—— 库存服务很可能不在同一数据库甚至同一进程 - 可行方案:
OrderService发出「扣减库存」事件,由独立的InventoryService消费并执行本地事务;配合幂等表 + 补偿任务处理失败 - go-zero 中推荐用
core/queue推送事件,或对接dtm等分布式事务中间件,而非在模型层硬编码跨库 SQL
聚合内事务失败时,状态回滚必须可预测
聚合根不是黑盒,它的失败点必须明确、可观测、可测试。尤其要注意值对象变更不触发持久化,而实体状态变更必须被事务捕获。
立即学习“go语言免费学习笔记(深入)”;
- 陷阱:在聚合方法中修改了
order.Status,但忘记调用orderModel.UpdateStatus—— 状态变更丢失,事务看似成功,实则不一致 - 建议:聚合根结构体字段应全为只读(
status string),所有变更通过方法(如order.Confirm())触发,方法内部同步更新状态并标记脏标志 - go-zero 的
model层不自动跟踪字段变更,所以「是否要更新」的判断逻辑必须由聚合方法自己承担,不能依赖 ORM 的 dirty check

















