分布式事务

分布式事务

啥是事务

数据库事务(简称事务)是数据库执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。

如商品订单逻辑:

  • 查询商品的库存
  • 扣减商品的库存
  • 生成订单

事务拥有以下四个特性,习惯上称为 ACID 特性:

  • 原子性(Atomicity):事务作为一个整体被执行,操作序列要么全部执行,要么全部不执行,记录之前的版本,允许回滚;
  • 一致性(Consistency):一致性是指事务使得系统从一个状态转换到另一个一致的状态,事务的一致性决定了一个系统设计和实现的复杂程度,也导致了事务的不同隔离级别。事务的开始和结束之间的中间状态不会被其他事物看到,例如商品库存扣减时,查询库存的结果应该是扣减事务提交之前的数据状态;
  • 隔离性(Isolusion):多个事务并发执行时,并发事务之间户互相影响的程度,它是事务一致性的延伸,通常通过适当破坏一致性来提升系统的并行度,它通常又分为以下几种:
    • 读未提交:A可以读到B未提交的事务操作;(所有并发事务问题都会发生)
    • 读已提交:A只能读到B已提交的事务操作;(解决脏读)
    • 可重复读:A不能读到B已提交的事务操作;(解决脏读、不可重复读,mysql默认隔离级别)
    • 序列化读:A等待B事务完成前阻塞;(解决脏读、不可重复读、幻读 [只针对数据新增])
  • 持久性(Durability):已被提交的事务对数据库的需改应该永久保存在数据库中,每一次的事务提交后的数据保证不丢失;


事务的传播属性

  • propagation_requierd:如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中,这是最常见的选择。
  • propagation_required_new:新建事务,如果当前存在事务,把当前事务挂起。
  • propagation_nested:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与propagation_required类似的操作。
  • propagation_supports:支持当前事务,如果没有当前事务,就以非事务方法执行。
  • propagation_not_supported:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  • propagation_mandatory:使用当前事务,如果没有当前事务,就抛出异常。
  • propagation_never:以非事务方式执行操作,如果当前事务存在则抛出异常。


啥是分布式事务

分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同的节点上,且属于不同的应用,分布式事务要么保证这些操作要么全部成功、要么全部失败。本质上说,分布式事务就是为了保证不同数据库的数据一致性


CAP定理

定理简介

CAP定理又被称作布鲁尔定理,说的是在分布式系统中,一致性(C)、可用性(A)和分区容错性(P)三者不可兼得,最多只能同时满足两个。

  • C(一致性):对某个指定的客户端,读操作返回最新的写操作结果。对于数据分布在不同节点上的数据来说,如果某个节点更新了数据,在其他节点若能读取到这个最新的数据,那么就称该系统为强一致性;如果某个节点没有读取到,那就是分布式不一致。
  • A(可用性):非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应),可用性的两个关键指标是 合理的时间 和 合理的响应。合理的时间是指请求不能被无线阻塞,应该在合理的时间内给出返回;合理的响应指的是系统应该明确返回结果并且结果是正确的,比如正常结果或提示“系统繁忙,请稍后操作”等。
  • P(网络分区容错性):当出现网络分区后,系统能够继续工作。打个比方,这里集群有多台机器,有机器网络出现了问题,但是这个集群仍然可以正常工作。

CAP定理可以分为下面三种情况:

  • CA系统:单机系统
  • CP系统:如dubbo框架,zk在选举时dubbo是不能对外提供服务的(!A)
  • AP系统:如spring cloud框架,通过熔断机制保证了A,但是不能很好保证C

但这里有一个极其核心的现实误区需要首先纠正:在现代分布式网络中,P(分区容错性)是必选项,你没得选。分布式系统是由多台机器通过网络连接的。只要是网络,就一定会抖动、会丢包、会断线(比如你的虚拟机静态 IP 变了、交换机烧了、甚至仅仅是 JVM 触发了 Full GC 导致工作线程短暂失联)。这种网络不通导致集群分裂成孤岛的现象,就叫网络分区(Network Partition)。接受 P,意味着当网络断开时,你的系统不能直接原地崩溃,得撑住。所以,架构师真正的考卷上其实只有两道单选题:要么选 CP,要么选 AP。


CP 与 AP 的真实权衡

为了让你彻底看清 CP 和 AP 在实际架构中的残酷取舍,我们用一个最通俗的场景来推演: 假设你的 User 账户模块部署了两个节点:节点 A(在北京)节点 B(在上海)。突然,京沪之间的光缆被挖断了,发生了网络分区(P)。此时,一个北京的用户发起了一笔扣款。


强迫症流派——选 CP

  • 核心思想:“宁可系统瘫痪,数据绝不能错!”
  • 推演现场:
    • 用户在节点 A(北京)成功扣款 100 元,节点 A 的数据变成了 900 元。
    • 节点 A 试图把这个新余额同步给节点 B(上海)。
    • 卡住了!因为网络断了(P),节点 A 无法收到节点 B 的确认。
    • 既然无法保证上海那边的节点 B 也是 900 元,CP 系统为了维护全局绝对的强一致性(C),节点 A 只能咬牙向用户报错:“系统开小差了,请稍后再试(拒绝服务)。”
  • 典型代表:Zookeeper、Seata AT 模式全局锁锁定期间、金融的核心账务系统。
  • CP 的代价:牺牲了可用性 A。在高并发下,如果网络出现轻微抖动,系统会爆出大量的 Timeout 或拒绝服务,用户体验极差。


及时行乐流派——选 AP

  • 核心思想:“数据错一会儿没关系,系统必须能点得动!”
  • 推演现场:
    • 用户在节点 A(北京)成功扣款 100 元。
    • 节点 A 同样尝试同步给节点 B(上海),发现网络不通。
    • 但由于系统追求的是高可用性(A),节点 A 直接返回用户:“扣款成功!”
    • 代价发生:在这一瞬间,如果有另一个并发请求打到了节点 B(上海),由于网络还没修复,节点 B 查出来的余额依然是旧的 1000 元!此时,系统的一致性被彻底破坏了。
    • 典型代表:Eureka、Nacos(AP模式)、Spring Cloud 里的大多数常规微服务、Saga模式。


BASE 理论:向现实低头的艺术

CAP 定理是严苛的物理定律(选 CP 就会失去 A)。但开公司的老板不能接受系统动不动就报错拒绝服务。于是,eBay 的架构师提出了 BASE 理论。BASE 是对 CAP 中 AP 方案的延伸和落地,它的核心哲学是:既然我们无法做到“强一致性”,那我们就退而求其次,用“最终一致性”来换取“基本可用”。

BASE 是三个英文短语的缩写:

  • BA:Basically Available(基本可用)。当系统遭遇不可抗力(高并发流量洪峰、网络分区)时,系统允许损失一部分非核心功能或性能,来保证核心链路依旧可用。
    • 响应时间上的损失:平时下单 0.1 秒搞定,今天双十一大促,系统响应变慢了,需要 3 秒——这就是基本可用。
    • 功能上的降级(丢车保帅):平时主页会根据算法精准推荐商品。今天服务器顶不住了,主页直接降级成展示静态的固定图片,甚至直接弹出“前方拥堵,请稍后再试”,但依然允许你进去付款——这就是基本可用。
  • S:Soft State(软状态 / 弱状态)。允许系统中的数据存在中间状态(或称“过渡状态”),并且认为这个中间状态不会影响系统的整体稳定性。比如 Seata 的 TCC 或者是 Saga 模式,当你的账户扣了钱,库存锁了资源,但由于订单服务还在处理,此时整笔订单的状态叫 PENDING(处理中) 或者本地控制表里的 STATE = 1 (TRY)。这个 PENDING 就是软状态。它既不是成功,也不是失败。
  • E:Eventually Consistent(最终一致性)。这是整个分布式事务追求的最高境界:系统允许数据在一段时间内是不一致的(软状态),但随着时间的推移(比如网络修好了、重试线程执行成功了),数据最终必须达到一致的状态。

总结起来就是:

  • CAP 是绝对理想的边界限制:它告诉你,鱼和熊掌不可兼得,高并发长链路下必须向 P 妥协。
  • BASE 是妥协后的工程艺术:它指导我们,通过引入软状态(如 TCC 的冻结、控制表的状态位),允许数据先乱一会儿,再利用异步重试机制(如 Seata 的二阶段重试),最终实现数据的最终一致性。


分布式事务的解决方案

  • XA两端提交(强一致)
  • TCC三段提交(强一致)
  • Seata(alibaba)
  • 本地消息表(MQ+Table,最终一致)
  • RabbitMQ的ACK机制实现分布式事务
  • 事务消息(RocketMQ [alibaba],最终一致性)


两阶段提交(2PC)

聊完了现代微服务中向现实妥协的 AP/BASE 理论,我们必须把时钟拨回几十年前,去复盘一下传统关系型数据库(如 Oracle、MySQL XA 事务)所坚守的强一致性(CP)老祖宗——刚性事务

刚性事务的代名词是 2PC(Two-Phase Commit,两阶段提交) 和它的改良版 3PC(Three-Phase Commit,三阶段提交)。它们的目标是达成物理级别的“完美主义”:要么全部高高兴兴落盘,要么全都不留痕迹地回滚。 但为了这份完美,它们在分布式网络中付出了极其惨烈的代价。

2PC 把事务控制分为两个核心角色:协调者(Coordinator) 和 参与者(Voter/Cohort,即各个本地数据库)。

运转流程:

  • 阶段一:表决阶段 (Prepare Phase) 。协调者向所有参与者发送“准备提交”指令。参与者在本地执行 SQL,写 Undo/Redo 日志,锁定数据库行记录,但不提交事务。随后向协调者投票(Yes 或 No)。
  • 阶段二:执行阶段 (Commit Phase) 如果所有人投 Yes,协调者广播 Commit,大家正式提交;只要有一个人投 No 或者超时没回应,协调者广播 Rollback,大家全部回滚。

2PC 的三大原生致命缺陷:

  • 缺陷一:严重的同步阻塞(Performance Killer),这是 2PC 被现代高并发架构抛弃的最主要原因。
    • 痛点:在阶段一中,所有参与者拿到 Prepare 指令后,都会在本地锁住对应的数据库资源(行锁、表锁),这个锁会一直持续到阶段二的 Commit 指令送达后才释放。
    • 后果:整个分布式事务执行期间,哪怕某个参与者 1 毫秒就做完了 Prepare,它也必须死死咬住数据库锁,干等着最慢的那台机器。高并发流量一进来,数据库连接池瞬间被卡死,系统整体吞吐量呈断崖式下跌。
  • 缺陷二:无法承受的单点故障(Single Point of Failure),2PC 的运转高度依赖“协调者”这个中心大脑。
    • 痛点:如果协调者在阶段二发起 Commit 之前突然宕机了(比如机房断电),此时所有的参与者都会陷入漫长的、不知所措的“悬挂(Hanging)”状态。
    • 后果:参与者不知道该提交还是该回滚,为了保证一致性,它们只能继续死死锁住本地的数据库资源。整个集群的生命线完全卡死,只能等待协调者重启恢复。
  • 缺陷三:不可避免的数据不一致(Data Inconsistency),在极端网络分区(P)下,2PC 的强一致性神话会彻底破灭。
    • 脑裂现场:在阶段二,协调者向所有参与者广播 Commit 指令。
    • 网络背刺:指令刚发给参与者 A,网络突然断开(脑裂),导致参与者 B、C 压根没收到这条 Commit 指令。紧接着,协调者也彻底挂了。
    • 惨烈后果:参与者 A 乖乖提交了数据,而 B 和 C 还在原地傻等。分布式系统的数据在这一瞬间发生了实质性的永久不一致(资损诞生)。


三阶段提交(3PC)

为了解决 2PC 的“单点故障”和“阻塞时间过长”的问题,学术界提出了 3PC。 3PC 做了两件事:把 2PC 的阶段一拆成了两步(CanCommit + PreCommit),并且引入了超时机制。

运转流程:

  • 阶段一:CanCommit(询问) 协调者只问一句:“大家能提交吗?” 参与者看一眼自身状态,不锁资源,只回 Yes/No。这降低了盲目锁资源的概率。
  • 阶段二:PreCommit(锁资源) 大家都说行,协调者下发 PreCommit。参与者此时才开始执行 SQL,锁资源,写日志,回传 Ack。
  • 阶段三:DoCommit(正式提交) 协调者下发正式 Commit。核心改良是如果参与者在阶段三迟迟没有收到协调者的 Commit 指令(超时了),参与者不会死等,而是会自作主张地自动执行本地 Commit!

3PC 利用 “参与者超时自动提交” 的机制,确实解决了 2PC 的单点故障和死锁问题(即使协调者挂了,参与者超时也会自己提交,释放锁)。但它却带进来了更具毁灭性的新灾难:更大概率的数据不一致!我们来推演 3PC 极其经典的回滚变提交翻车现场:

  • 系统进入了阶段二。参与者 A、B、C 都锁好了资源。
  • 就在这时,协调者突然发现由于某个边缘因素,这笔事务不能提交,应该回滚。
  • 协调者开始广播 Abort(回滚) 指令。
  • 网络大断裂(脑裂发生):
    • 参与者 A 运气好,收到了 Abort 指令,本地果断执行了 Rollback。
    • 参与者 B 和 C 因为网络分区,没有收到这条回滚指令。
  • 由于 3PC 的契约规定“参与者如果在阶段三等协调者指令超时了,就默认全网都同意了,直接自动 Commit”。
  • 最终参与者 A 回滚了,参与者 B 和 C 却自动提交了!

2PC 的不一致,是因为网络断开导致“有人提交了,有人没提交”;而 3PC 的不一致更离谱,因为加入了超时盲目提交机制,导致网络断开时 “本该全部回滚的事务,有人回滚了,有人却提交了”。

这也是为什么在如今微服务、大并发的互联网架构中,2PC 和 3PC 基本已经退化为底层基础设施(如数据库主从同步),而在上层应用架构中,我们全部在大刀阔斧地全面拥抱 BASE 理论下的柔性事务(TCC、Saga)


MQ事务消息

有一些第三方的MQ是支持事务消息的,比如RocketMQ,他们支持事务消息的方式也是类似于二阶段提交,但是市面上一些主流的MQ是不支持事务消息的,比如RabbitMQ和kafka都不支持(他们都基于ACK机制)。

以阿里的RocketMQ中间件为例,流程为:

  1. 发送一个事务消息,这个时候,RocketMQ将消息的状态标记为Prepared,注意此时这条消息消费者是无法消费到的;
  2. 执行业务代码逻辑;
  3. 确认发送消息,RocketMQ将消息状态置为可消费,这个时候消费者才能真正消费到这条消息;
  4. 如果步骤3的确认消息发送失败,RocketMQ会定期扫描消息集群中的事务消息,如果发现了Prepared消息,它会向消息发送端(生产者)确认。RocketMQ会根据发送端设置的策略来决定回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或者同时失败;

Seata

seata简介

2019年1月,阿里巴巴中间件团队发起了开源项目Fescar(Fast & Easy Commit and Rollback),和社区一起共建开源分布式事务解决方案。Fescar的愿景是让分布式事务的使用能够像本地事务的使用一样,简单和高效,并逐步解决开发者们遇到的分布式事务方面的所有难题。

Fescar开源以后,蚂蚁金服参与Fescar社区参与共建,并在Fescar 0.4.0 版本中贡献了TCC模式。

为了打造更中立、更开放、生态更加丰富的分布式事务开源社区,经过社区核心成员的投票,大家决定对Fescar进行品牌升级,更名为Seata,意为 Simple Extensible Autonomous Transaction Architecture,是一套一站式分布式事务解决方案。

Seata融合了阿里巴巴和蚂蚁金服在分布式事务技术上的积累,并沉淀了新零售、云计算和新金融等场景下的丰富的实践经验。


核心组件

  • Transaction Manager(TM):控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议;(告知全局所有事务的参与者,到底进行什么样的操作)

  • Transaction Coordinator(TC):事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚;(存储了所有服务的本地事务状态)

  • Resource Manager(RM):控制分支事务,负责分支注册,状态汇报,并接收事务协调器的指令,驱动分支事务(本地事务)的提交和回滚;


整体工作流程

  1. TM向TC发送申请开启一个全局事务,去哪聚事务创建成功并生成一个全局唯一的事务ID(XID),XID在微服务调用链路的上下文中进行传播;
  2. RM向TC注册分支事务,接着执行这个分支事务并提交事务(重点是RM在此阶段就已经执行了本地事务的提交/回滚),最后将执行结果汇报给TC;
  3. TM根据TC中所有分支事务的执行情况,发起全局提交或者回滚决议;
  4. TC协调XID下管辖的全部分支事务完成提交或者回滚请求;


使用方式

  1. @GlobalTransactional
  2. 数据源 DataSource 换成 Seata提供的 DatasourceProxy


Seata支持的模式

支持两种常见的分布式事务实现方案,AT 和 TCC


AT模式

第一阶段:本地数据备份阶段

Seata AT模式是基于XA事务演进而来的一个分布式事务中间件,XA是基于数据库实现的分布式事务协议,本质和两阶段提交一样,需要数据库支持,Mysql5.6以上版本支持XA协议,其他数据库Oracle、DB2也实现了XA接口。

AT模式分为两个阶段,如下:

  1. Seata的JDBC数据源代理通过对业务SQL的解析,把业务数据在变化前后的数据镜像组织成回滚日志(XID、分支事务ID、变化前的数据、变化后的数据]);
  2. 将回滚日志存入一张日志表 UNDO_LOG(需要手动创建),并对UNDO_LOG这张表中的数据形成行锁(for update)
  3. 若锁定失败,说明有其他事务在操作这条记录,它会在一定时间内重试,重试失败则回滚本地事务,并向TC汇报本地事务执行失败;


第二阶段:全局事务提交或回滚阶段

全局提交:

  1. 所有分支事务此时已经完成提交,所有分支事务提交都正常;
  2. TM从TC获知后决议执行全局提交,TC异步通知所有RM释放UNDO_LOG表中的行锁,同时清理掉UNDO_LOG表中刚才释放锁的那条数据;

全局回滚:

  1. 若任何一个RM阶段事务提交失败,通知TC提交失败;
  2. TM从TC获知后决议执行全局回滚,TC向所有RM发送回滚请求;
  3. RM通过XID和BranchId找到相应的回滚日志记录,通过回滚记录生成反向的更新SQL并执行,以完成分支的回滚,同时释放锁,清除UNDO_LOG表中释放锁的那条数据;


TCC模式

Seata也针对TCC做了适配兼容,基本思路就是使用侵入业务的补偿以及事务管理器的协调来达到全局事务一起提交或回滚的目的。