事务边界画在了分片线上
单库时代,事务是个幸福的问题:@Transactional 一标注,扣库存、减余额、写订单全在一个事务里,要么全成要么全无。分库之后,这个幸福的边界被物理切开了:MySQL 的事务只属于一个实例,跨实例没有天然的「要么全成」。分片系统的事务问题,本质就是重新回答——哪些写操作还能共享一个本地事务?不能共享的怎么办?
第一种:单片事务,假装没拆过
好消息是,按这个系列的设计走过来,大多数业务写天然落在单片:用户的订单、购物车、地址都用 user_id 做分片键(第 5 篇的决策),围绕一个用户的完整操作写的是同一个库,本地事务原样可用,连代码都不用改。绑定表设计(第 10 篇)又把订单与明细锁进同片。这是设计期最值得下的功夫:用分片键的规划,把事务的边界提前画对——比任何分布式事务框架都便宜。
第二种:跨片写,中间件不会替你兜底
真正要警惕的是跨片写:一笔转账要减用户 A 的余额(在分片 3)、加用户 B 的余额(在分片 7)。不带分片键感知的代码里,这类操作会写两个库——中间件默认把跨片事务拆成多个独立的本地事务顺序执行,第二个库失败时,第一个库的提交不会回滚:部分提交,账就错了一半。面对跨片写,路线有三条:XA 强一致——两阶段提交锁资源到全局事务结束,性能代价大、阻塞窗口长,分片场景用得少;柔性最终一致——本地事务加消息表,先落库再发消息补偿,短暂不一致换取高可用;业务规避——重新审视操作是否真的必须跨片,换个数据组织方式让它在单片完成。XA 那套两阶段的细节、柔性方案的完整谱系(本地消息表、TCC、Saga),是另一个大话题——后面会开一个专门的系列来讲,这里先立住结论:分片系统的第一选择永远是柔性方案,XA 是最后的保险。
// 柔性路线的最小骨架:本地事务 + 消息表
@Transactional // 单片事务:扣减与消息同库落盘
public void transferInShard(...) {
accountDao.deduct(from, amount); // 业务写
msgDao.save(transferMsg, PENDING); // 同事务记一笔待处理消息
}
// 异步任务扫 PENDING 消息,补偿对方分片,成功后标记 DONE补偿的配套:幂等与对账
只要走上柔性路线,两件配套就不能省。幂等:补偿消息可能重复投递,加余额的操作必须天然可重——幂等键、状态机、唯一约束,总得占一样;对账:消息丢了、任务挂了,补偿链路自己也会漏,每天跑一次全局对账把漏网的差异捞出来修复。幂等与对账是柔性方案的氧气,平时没人注意,缺了立刻窒息。
一张决策卡
分片事务的决策顺序收束成四步:一查——这个业务操作能不能用分片键设计锁进单片?能就不需要任何分布式事务;二避——跨片操作能否改组织方式(冗余、异步化)减少跨片;三柔——必须跨片的走本地消息表加补偿,配齐幂等与对账;四慎——XA 只留给强一致刚需且 QPS 可控的极少数场景。顺序别反——上来就问「用什么分布式事务框架」,说明设计期欠了账。
事务讲完,系列继续往下走。下一篇转到数据本身:分片之后主键怎么办——自增 ID 在分片世界全线撞车,分布式 ID 的时代到了。
评论 (0)