核心交易系统架构演进:如何兼顾强一致性与高性能?
核心交易系统:从“最终一致”到“强一致”的平滑演进之路
背景与痛点
随着业务量的增长,特别是涉及资金流转的场景,原有的基于消息队列的“最终一致性”架构开始显露疲态。虽然它解耦了系统,提升了吞吐量,但在面对严格的财务审计要求和用户对“实时到账”的期望时,数据短暂不一致的窗口期(比如消息丢失、重复消费导致的临时账不平)变得难以接受。
然而,直接引入像 2PC(两阶段提交)或分布式事务中间件(如 Seata AT 模式)来实现全链路强一致性,往往会带来巨大的性能损耗和系统复杂度。一旦事务协调器挂掉,整个系统可能陷入阻塞。这在高并发的交易场景下是致命的。
核心策略:分而治之,混合驱动
我们不能为了 1% 的强一致性需求,牺牲 99% 的性能。因此,核心思路是**“核心链路强一致,非核心链路最终一致,通过领域事件进行驱动”**。
1. 资金核心层:TCC 模式 (Try-Confirm-Cancel)
对于涉及账户余额变动、资金冻结/解冻的核心操作,必须保证强一致性。这里推荐使用 TCC 模式。
- Try 阶段:检查资源(余额是否充足),冻结资源(扣减预存款,而非直接扣减余额)。
- Confirm 阶段:如果 Try 成功,执行真正的业务提交(扣减余额,扣除冻结额)。
- Cancel 阶段:如果 Try 失败或需要回滚,释放冻结的资源。
为什么选 TCC?
相比于 Seata AT 模式(依赖数据库 Undo Log 和全局锁,对数据库侵入性强,锁竞争激烈),TCC 是业务层面的补偿,开发者主动控制颗粒度,性能更高,且能避免长事务导致的死锁。
2. 业务扩展层:Saga 模式 + 本地消息表
对于非资金核心,但需要保证最终结果的业务(如积分发放、通知发送、库存扣减),继续沿用或优化最终一致性。
- Saga 模式:将一个长事务拆分为多个本地短事务,每个短事务都有对应的补偿动作(Compensating Transaction)。如果某一步失败,逆向执行补偿动作回滚。
- 本地消息表(Transaction Message):利用本地数据库的 ACID 特性,将业务数据和消息写入放在同一个本地事务中,确保“只要业务成功,消息一定发出去”。然后由后台任务轮询发送到 MQ,消费者负责幂等消费。
3. 关键挑战与解决方案
A. 幂等性设计(Idempotency)
在分布式环境下,网络抖动导致重试是常态,无论是 TCC 的 Confirm/Cancel 还是 MQ 消费,必须保证幂等。
- 方案:利用数据库唯一索引(业务主键 + 版本号)或 Redis 的 SETNX 机制。每次操作前先查重,只有未处理过的请求才执行。
B. 空回滚与悬挂(TCC 特有顽疾)
- 空回滚:Try 阶段未执行(网络丢包),但 Cancel 阶段收到了指令。此时需要检查 Try 是否执行过,若无,则 Cancel 什么都不做直接返回成功。
- 悬挂:Cancel 比 Try 先到达。需要记录事务状态,Try 发现状态已被回滚则不再执行。
C. 对账系统(兜底方案)
即使架构设计得再完美,系统总有 Bug 或极端情况。对账是资金安全的最后一道防线。
- 实施:建立独立的对账服务,每日或实时比对核心交易流水与第三方支付渠道(如银行、支付宝)的流水,以及内部各子系统的资金流水。发现不平,自动告警或触发修复脚本。
总结
从 MQ 最终一致迁移到强一致,不是简单的技术替换,而是架构理念的升级。
不要试图用一个分布式事务框架解决所有问题。
- 核心资金流:TCC 保证强一致。
- 周边业务:Saga/本地消息表保证最终一致。
- 系统解耦:通过 Domain Event(领域事件)驱动,而非直接 RPC 调用。
- 兜底机制:完善的对账体系。
这样既满足了日益严格的数据一致性要求,又避免了系统被复杂的分布式事务锁死,保持了高可用性。