type
status
date
slug
summary
tags
category
icon
password
在前序文章《当你扫码支付的那3秒,后台到底发生了什么?》中,我们从上帝视角俯瞰了整个支付业务的流转,并拆解了“断直连”与“四方模式”的宏观架构。然而,对于开发人员而言,那只是海面上的冰山一角。
当我们潜入海底之下,直面每秒万级的并发请求,会发现“在这个横跨支付宝、网联、银行三方的长链路中,如何保证钱既不多扣、也不少扣?”
这条看似完美的支付链路,实则危机四伏。
这就是分布式系统中最核心的挑战——数据一致性。
问题原点:跨越物理鸿沟的“掉单”
场景对应:“支付宝 -> 网联 -> 银行”这个跨机构的长链路。
什么是掉单?
- 问题描述: 银行核心系统已经扣了用户的钱,但是回传给网联的时候网络超时了。支付宝这边没收到成功响应,认为支付失败。结果:用户钱没了,订单状态还是“未支付”。
- 技术点: 分布式事务、冲正机制、TCC模式、最终一致性补偿任务。
为什么会发生这种情况?
在单体应用中,我们依靠数据库的ACID特性(例如MySql的事务)就能保证数据的一致性。
但是当业务横跨支付宝、网联、银行三个物理隔离的数据库时,没有任何一个数据库能锁住另一个数据库的资源。同样也无法让这三个数据库同时提交或回滚,一旦出现网络原因(超时、丢包等),就会出现“既成功又失败”的中间状态。这也是分布式事务面临的挑战。
理论基石:银行核心绝不等待
为了解决这个问题,业界衍生出了两种解决思路:刚性事务与柔性事务。
刚性事务
- 刚性事务追求的是强一致性,理论基础是ACID,即原子性、一致性、隔离性、持久性。
- 代表技术:2PC。
- 核心特征:参与事务的所有节点要么成功要么失败。在事务提交之前,所有节点都必须锁住资源不能动
为什么银行不用刚性事务?
刚性事务是为了一致性而牺牲了性能。比如在你的房贷自动扣款时,此时如果是刚性事务,那么银行的数据库就会锁住,此时所有其他的请求扣款的需求都会失败,如果你想买零食,那么不好意思,买不了。所有的其他请求都会被卡住,直到超时失败。
- 性能崩塌:银行的核心数据库是非常昂贵的资源。如果此时因为网络问题多等2秒,在双十一每秒几万笔交易的情况下,锁持有的时间就会被网络原因放大,这将导致银行数据库的连接池瞬间被消耗完,整个银行系统就会面临瘫痪的风险。
- 可用性丧失:如果链路中有一个节点堵了,那么所有的参与者都得等着,整个交易链路就会被卡死。
柔性事务
- 柔性事务允许出现不完美的情况,理论基础是BASE。
- BA (Basically Available):基本可用,系统掉了一部分没关系,核心能用就行。
- S (Soft state):软状态。允许系统存在中间态(如“待清算”、“待完成”),而不是非黑即白。
- E (Eventual consistency):最终一致性,不要求在每一秒都要保持一致,但必须要保证过一段时间(比如 T + 1 日),账目必须是要平的。
为什么银行必须选用柔性事务?
前序文章 中我们提到了“信息流与资金流”,这其实就是一种柔性事务的体现。
在你付款成功的时候,其实只是支付宝、网联和银行层面的状态成功了,真正的资金其实还没有划拨。直到T+1清算的时候,通过“批量轧差”和“对账”,把存在的不一致抹平,这是“最终一致性”的体现。
- 高可用:支付宝断网不会影响微信,建行掉了不影响工行。
- 高性能:数据库锁只存在银行划拨你自己的那一个非常短暂的时间,不会受到网络状态的影响。
既然我们已经选择了柔性事务,那么柔性事务我们是用TCC模式呢还是Seata AT?搞清楚这个问题,我们需要先知道TCC和Seata AT是什么。
方案博弈:TCC与Seata AT
既然选择了柔性事务,我们面临两个流派的选择:业务层的 TCC 和 数据层的 Seata AT。
TCC
TCC(Try-Confirm-Cancel),它的核心不在于数据库,而在于业务逻辑。
TCC模式下,假如你买了100元的东西(Try),系统不会直接扣掉你的钱,而是会冻结,这个时候你钱还在你的账目上,但是你已经不能用了。相当于把你的数据库变成了这样。

如果交易成功了(Confirm),系统只会把你的冻结资金正式划掉。
如果交易失败了(Cancel),系统会把你的冻结资金再还回去变成资金。
Seata AT
Seata AT是阿里开源的黑科技,主打的是无侵入。你可以直接写,分布式事务由框架自动搞定。
Seata AT更像是一个有“时光倒流”的管家,你尽管去消费,他会在你消费之前提前给你的数据库拍个照,如果最后需要回滚,它直接拿出照片来把数据还原回去即可。
注意:Seata AT是通过生成反向SQL来执行的。

深度解析:Seata AT 的致命隐患——“脏写”
因为Seata AT是通过逆向SQL完成的数据恢复,那么就有可能出现脏写的情况。
什么是脏写
Seata AT在没进行最终确认前出现了插队的本地事务,使得数据发生了变化。
举个例子,张三共有100元,想花10元买个游戏,此时经过两个节点:
- a节点:扣掉银行卡10元。
- b节点:游戏厂商获得扣款成功的信息。
但是在经过a节点的时候李四又往卡里转了50元,此时卡里的余额为140元,因为a节点已经完成了,90+50=140元。
但是在还没到b节点的时候张三又说算了,不买了,于是Seata AT进行数据回滚,一看原来的是100元,那我恢复吧,于是就把余额从140元变成了100元,此时李四的50元就丢掉了。

Seata 的补救与局限:全局锁
为了解决脏写,Seata 引入了全局锁。
- 事务A在二阶段修改数据前,必须拿到全局锁,拿到全局锁才能提交本地事务。
- 如果事务B也是一个Seata事务,也想修改同一条数据,那么就需要等事务A的二阶段完成,释放了全局锁,事务B才能修改数据。
如果事务B不是Seata事务而是本地事务呢?此时Seata是不会去校验全局锁的,此时在回滚前会进行一次比对,比对数据库当前值和修改后的快照,比如上述例子中的90元和140元,Seata发现本来回滚应该是从90元回滚到100元,怎么现在不是90元而是140元,此时Seata就会报错,回滚失败,转为人工介入。
架构决策:为什么银行核心不用Seata呢?
全局锁导致的热点账户瘫痪
- Seata AT:为了防止脏写,于是会加全局锁
双十一大促:全国几万人同时购买一个商户的产品,50万的货但是有100万人买,加了全局锁就需要排队等待,和刚性事务差不多了
- TCC:锁由业务控制
Try阶段只是冻结了你的货物,并没有锁死这条记录,你可以继续购买只要还有库存。
强依赖数据库
- Seata AT:强依赖数据库,必须拦截JDBC才能解析SQL,必须能在你的库里建立
undo_log表,以达到回滚的目的
- TCC:天然适合跨服务、跨机构。Try/Confirm/Cancel 就是三个普通的 HTTP 接口。只要银行开了这个口子,就能执行。
注意:银行核心的API接口都是很老旧的,或者存储过程很老旧,有些甚至数据库的IP都不给,所以没法用Seata AT。
回滚失败风险
- Seata AT:依赖“数据还原”。如果在 Seata 回滚之前,有人(比如管理员手动)绕过 Seata 修改了数据库,那么回滚前的数据就不对了,回滚就会报错(脏写)。
- TCC: 依赖“业务逻辑”。Cancel 接口是写死的代码,不管数据怎么变,只要业务逻辑是对的,就能回滚成功。
总结
在分布式事务的抉择中,Seata AT 胜在开发效率,TCC 胜在控制粒度。
对于银行核心这种跨机构、异构系统、高并发、零容忍的场景,TCC 虽然开发繁琐(代码量 X3),但它赋予了我们对每一个阶段的极致控制权.
既然 TCC 这么强,那如果 Confirm 或者 Cancel 阶段因为网络断了,连“取消”指令都发不过去怎么办? 这就需要引入支付系统的另一道防线——“冲正机制”。 下期预告:《支付网关技术解构(二):冲正机制与幂等性设计》
- Author:程知非
- URL:http://preview.tangly1024.com/article/tcc-vs-seata-payment
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!


