在进行业务拆分或分库分表之后,原本依赖单机数据库 ACID 本地事务即可完成的“扣减库存 + 创建订单 + 扣减余额”操作,变成了跨越多个独立网络节点与存储实例的分布式协同问题。在工程实践中,盲目追求强一致性往往会导致系统吞吐量断崖式下跌。
一、为什么 XA / 2PC 很少用于高并发互联网核心链路
传统的两阶段提交(Two-Phase Commit, 2PC)包含 准备阶段(Prepare) 和 提交阶段(Commit)。虽然数据库层面普遍支持 XA 协议,但在高并发场景下它有两个致命短板:
- 长周期行锁占用:参与者在收到
Prepare请求并写入 Redo/Undo 日志后,必须一直持有数据库行锁,直到协调者收集完所有节点的响应并发出最终的Commit指令。原本微秒级的内存锁变成了跨越数个网络 RTT 的毫秒级锁,热点行(如爆款商品库存)吞吐量直接受限于网络延迟。 - 协调者与参与者同时故障导致数据悬挂:如果在第二阶段协调者发出部分
Commit后宕机,且收到该指令的唯一参与者也同时宕机,其余参与者将长期处于状态未知的阻塞状态,必须人工介入恢复。
二、本地消息表(Transactional Outbox)实现最终一致性
对于 90% 以上的异步解耦场景(例如:订单支付成功后通知积分服务增加积分、通知仓储系统生成发货单),最稳健、侵入性最低的方案是 本地消息表(Transactional Outbox)。
其核心思路是利用关系型数据库的本地 ACID 事务,将“业务数据修改”与“待发送事件记录”写入同一个数据库的同一个事务中:
sql
BEGIN;
-- 1. 更新业务订单状态为已支付
UPDATE orders
SET status = 'PAID', paid_at = NOW()
WHERE order_id = 'ORD202502150091' AND status = 'PENDING';
-- 2. 在同一个本地事务中写入本地事件投递表
INSERT INTO outbox_events (
event_id, aggregate_type, aggregate_id, event_type, payload, status, created_at
) VALUES (
'evt_98f1a2b3c4',
'ORDER',
'ORD202502150091',
'ORDER_PAID',
'{"orderId":"ORD202502150091","userId":10294,"amountCents":19900}',
'PENDING',
NOW()
);
COMMIT;随后,由独立的后台轮询 Relay 协程(或通过监听数据库 WAL/Binlog 的 CDC 组件)读取 outbox_events 中状态为 PENDING 的记录投递到消息队列,投递成功后再标记为 PUBLISHED。
幂等性铁律:由于网络超时重传,本地消息表保证的是 At-least-once(至少投递一次) 语义。因此下游消费端必须依靠唯一
event_id或业务单号建立去重表(或唯一索引),确保重复消费不会导致双倍加积分或重复发货。