分库分表后跨库事务必然失效,需用本地事务+可重放事件实现最终一致性:订单写入同时落outbox表,异步发消息并重试;分片键须均匀分布(如user_id);避免跨分片join;通过版本号、对账、监控保障可观测的最终一致性。

分库分表后跨库事务必然失效,别再用tx.Commit()幻想兜底
本地事务在单库内有效,但一旦按user_id % 4拆到db_0~db_3,order写db_0、inventory写db_1,tx.Commit()就只管得住自己那一个连接。这不是 Go 的 bug,是分片架构的硬约束——你得接受“没法一次全成功”,转而设计可追踪、可补偿的流程。
跨分片写操作必须拆成「本地事务 + 可重放事件」两步
常见错误是 HTTP handler 里先调inventorySvc.DecreaseStock(),再插订单;结果库存服务超时,订单却落库了,状态永久不一致。
- 正确顺序:订单服务在
db_0执行INSERT INTO orders→ 同一事务内写入本地outbox表(含topic="inventory.deduct"、payload、status="pending")→tx.Commit() - 另起 goroutine 轮询
outbox表,查status = 'pending'记录,用kafka-go发消息并更新为'sent';失败则指数退避重试(如time.Second * 1、*2、*4) - 库存服务消费到事件后,先用
WHERE id = ? AND status = 'reserved'条件更新,保证幂等;成功后再发inventory.deducted事件给下游
分片键选错会导致一致性修复成本飙升
用status或created_at做分片键,数据会严重倾斜——比如所有“待支付”订单挤在db_0,而db_3空闲。一旦该库故障,整个支付链路卡死,且无法用简单路由规则恢复。
在 Go 中使用 google/wire 实现编译时依赖注入——wire.NewSet、wire.Build、wire.Bind(接口→实现)、wire.Struct、wire.Value、wire.Interface
- 分片键必须高频参与查询且分布均匀,
user_id或order_id(UUID 做 hash 后取模)是安全选择 - 避免跨分片 JOIN:需要关联用户和订单时,冗余
user_name到订单表,或用全局表存基础信息 - 扩容时若用简单取模(
% 4→% 8),50% 数据要迁移;改用一致性哈希,仅需搬动约 12.5% 数据
最终一致性不是放任不管,而是把「不一致窗口」变成可观测指标
你无法消灭延迟,但能把它暴露出来。比如库存预占后异步扣减,用户看到“已下单”但库存数没变——这 2 秒窗口期必须可查、可告警、可修复。
- 每个关键状态变更都落库+发事件,字段带
updated_at和version(用于乐观锁防覆盖) - 建对账任务,每 5 分钟扫
orders.status = 'paid'但inventory.updated_at 的记录,触发补偿 - 监控
outbox.pending_count和inventory_deduct_failed_total,阈值超 10 就发钉钉告警
真正容易被忽略的,是把“分库分表”当成纯性能优化手段——它本质是把单点一致性问题,转化成多节点状态协同问题。所有方案都绕不开状态持久化、幂等设计、显式超时控制这三件事,缺一不可。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










