跳到主要内容

分布式事务解决方案

前提

什么是分布式事务 ?

本地事务

先回顾下本地事务,也是单机的数据库事务。本地事务ACID四大特性:

原子性Atomicity):事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用

一致性Consistency): 执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的

隔离性Isolation): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的。

持久性Durability): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响

只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的

在mysql中原子性和持久性需要靠undo和redo日志来实现。

undo log

undo Log 的原理很简单,为了满足事务的原子性,在操作任何数据之前,首先将数据备份到undo Log ,然后进行数据的修改。如果出现了错误或者用户执行了ROLLBACK语句,系统可以利用undo Log中的备份将数据恢复到事务开始之前的状态。数据库写入数据到磁盘之前,会把数据先缓存在内存中,事务提交时才会写入磁盘中。

用undo Log 实现原子性和持久化的事务的简化过程: 假设有A、B两个数据,值分别为1,2

A.事务开始

B.记录A=1到undo log

C.修改A=3

D.记录B=2到undo log

E.修改B=4

F.将undo log写到磁盘

G.将数据写到磁盘

H.事务提交

如何保证持久性? 事务提交前,会把修改数据到磁盘前,也就是说只要事务提交了,数据肯定持久化了

如何保证原子性?

  • 每次对数据库修改,都会把修改前数据记录在undo log,那么需要回滚时,可以读取undo log,恢复数据
  • 若系统在G和H之间崩溃,此时事务并未提交,需要回滚。而undo log已经被持久化,可以根据undo log来恢复数据
  • 若系统在G之前崩溃,此时数据并未持久化到硬盘,依然保持在事务之前的状态

缺点 每个事务提交前将数据和 undo Log写入磁盘,这样会导致大量的磁盘O,因此性能很低。解决这个问题继续看redo  log。

redo log

redo log 是 InnoDB 存储引擎独有的,它让MySQL拥有了崩溃恢复能力。

和 undo Log相反,redo Log 记录的是新数据的备份。在事务提交前,只要将 redo Log 持久化即可,不需要将数据持久化,减少了IO的次数。

简化过程:

假设有A,B两个数据,值分别为1,2

A.事务开始

B.记录A=1到 undo log buffer

C.修改A=3

D.记录A=3到 redo log buffer

E.记录B=2到 undo log buffer

F.修改B=4

G.记录B=4到 redo log buffer

H.将undo log 写入磁盘

l.将redo log 写入磁盘

J.事务提交

如何保证原子性? 如果在事务提交前故障,通过undo log日志恢复数据。如果undo log都还没写入,那么数据就尚未持久化,无需回滚

如何保证持久化? 这里并没有出现数据的持久化。因为数据已经写入redo log,而redo log持久化到了硬盘,因此只要到了步骤 i 以后,事务是可以提交的

内存中的数据库数据何时持久化到磁盘? 因为redo log 已经持久化,因此数据库数据写入磁盘与否影响不大,不过为了避免出现脏数据(内存中与磁盘不一致),事务提交后也会将内存数据刷入磁盘(也可以按照固设定的频率刷新内存数据到磁盘中)。

redo log 何时写入磁盘 ? redo log 会在事务提交之前,或者redo log buffer 满了的时候写入磁盘

以前写undo log和数据库数据到硬盘,现在是写undo和redo log到磁盘,似乎没有减少IO次数?

  • 数据库数据写入是随机IO,性能很差
  • redo log在初始化时会开辟一段连续的空间,写入是顺序IO,性能很好
  • undo log 井不是直接写入磁盘,而是先写入到redo log buffer中,当redo log持久化 时,undo log就同时特久化到硬盘了,因此事务提交前,只需要对redo log特久化即可
  • redo log 并不是写入一次就持久化一次,redo log在 内存中也有自己的缓冲池:redo log buffer。每次写redo log都是写入到buffer,在提交时一次性持久化到磁盘,减少IO次数

分布式事务

分布式事务,就是指不是在单个服务或单个数据库架构下产生的事务。而是多数据源或者多个微服务下到事务,例如:跨数据源的分布式事务、跨服务的分布式事务等。

理论基础

CAP

CAP原则又叫CAP定理,指的是在一个分布式系统中,不可能同时满足以下三点

  • Consistency:一致性保证了不管向哪台服务器写入数据,其他的服务器能实时同步数据。
  • Availability:保证每个请求不管成功或者失败都有响应。
  • Partition Tolerance:分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务,也就是说,服务器AB发送给对方的任何消息都是可以放弃的,也就是说A和B可能因为各种意外情况,导致无法成功进行同步,分布式系统要能容忍这种情况。除非整个网络环境都发生了故障。也就是说:系统中任意信息的丢失或失败不会影响系统的继续运作。

Base理论

BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的简写。

BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。

BA(Basic Available)基本可用

响应时间上的损失:正常情况下,一个在线搜索引擎需要0.5秒内返回给用户相应的查询结果,但由于出现异常(比如系统部分机房发生断电或断网故障),查询结果的响应时间增加到了1~2秒。

功能上的损失:正常情况下,在一个电子商务网站上进行购物,消费者几乎能够顺利地完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。

S(Soft State)软状态

是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统不同节点的数据副本之间进行数据同步的过程存在延时。

E(Eventual Consisstency)最终一致性

强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

XA协议

是X/OPEN 提出的分布式事务处理规范。XA则规范了TM与RM之间的通信接口,在TM与多个RM之间形成一个双向通信桥梁,从而在多个数据库资源下保证ACID四个特性。目前知名的数据库,如Oracle, DB2,mysql等,都是实现了XA接口的,都可以作为RM。

XA是数据库的分布式事务,强一致性,在整个过程中,数据一张锁住状态,即从prepare到commit、rollback的整个过程中,TM一直把持折数据库的锁,如果有其他人要修改数据库的该条数据,就必须等待锁的释放,存在长事务风险。

如下的2PC、3PC都是基于XA协议。

解决方案:2阶段提交(2PC)

二阶段提交协议(Two-phase Commit,即 2PC)是常用的分布式事务解决方案,即将事务的提交过程分为两个阶段来进行处理。

两个阶段分别为:准备阶段、提交阶段

参与的角色:

  • 事务协调者(事务管理器,TM):事务的发起者
  • 事务参与者(资源管理器,RM):事务的执行者

准备阶段

这是两阶段的第一段,这一阶段只是准备阶段,由事务的协调者发起询问参与者是否可以提交事务,但是这一阶段并未提交事务,流程图如下图:

  • 协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待答复
  • 各参与者执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)
  • 如参与者执行成功,给协调者反馈同意,否则反馈中止

提交阶段

协调者发起正式提交事务的请求,当所有参与者都回复同意时,则意味着完成事务,流程图如下:

  • 协调者节点向所有参与者节点发出正式提交(commit)的请求。
  • 参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
  • 参与者节点向协调者节点发送ack完成消息。
  • 协调者节点收到所有参与者节点反馈的ack完成消息后,完成事务。

事务回滚

但是如果任意一个参与者节点在第一阶段返回的消息为终止,或者协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时,那么这个事务将会被回滚:

  1. 协调者节点向所有参与者节点发出回滚操作(rollback)的请求。
  2. 参与者节点利用阶段1写入的undo信息执行回滚,并释放在整个事务期间内占用的资源。
  3. 参与者节点向协调者节点发送ack回滚完成消息。
  4. 协调者节点受到所有参与者节点反馈的ack回滚完成消息后,取消事务。

缺点

  • 性能问题:执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
  • 可靠性问题:参与者发生故障。协调者需要给每个参与者额外指定超时机制,超时后整个事务失败。协调者发生故障。参与者会一直阻塞下去。需要额外的备机进行容错。
  • 数据一致性问题:二阶段无法解决的问题:协调者在发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
  • 实现复杂:牺牲了可用性,对性能影响较大,不适合高并发高性能场景。

优点

尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证强一致)

解决方案:3阶段提交(3PC)

三阶段提交协议,是二阶段提交协议的改进版本,有CanCommitPreCommitDoCommit三个阶段。

阶段一:CanCommit阶段

3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。

  • 事务询问:协调者向所有参与者发出包含事务内容的 canCommit 请求,询问是否可以提交事务,并等待所有参与者答复。
  • 响应反馈:参与者收到 canCommit 请求后,如果认为可以执行事务操作,则反馈 yes 并进入预备状态,否则反馈 no。

阶段二:PreCommit阶段

协调者根据参与者的反应情况来决定是否可以进行事务的PreCommit操作。根据响应情况,有以下两种可能。

  • 假如所有参与者均反馈 yes,协调者预执行事务。
    1. 发送预提交请求 :协调者向参与者发送PreCommit请求,并进入准备阶段
    2. 事务预提交 :参与者接收到PreCommit请求后,会执行事务操作,并将undoredo信息记录到事务日志中(但不提交事务)
    3. 响应反馈 :如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。
  • 假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。
    1. 发送中断请求 :协调者向所有参与者发送abort请求。
    2. 中断事务 :参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

阶段三:doCommit阶段

该阶段进行真正的事务提交,也可以分为以下两种情况。

  • 中断事务:任何一个参与者反馈 no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务
    1. 发送中断请求:如果协调者处于工作状态,向所有参与者发出 abort 请求
    2. 事务回滚:参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
    3. 反馈结果:参与者完成事务回滚之后,向协调者反馈ACK消息
    4. 中断事务:协调者接收到参与者反馈的ACK消息之后,执行事务的中断。
  • 执行提交
    1. 发送提交请求:协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。
    2. 事务提交:参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
    3. 响应反馈:事务提交完之后,向协调者发送ack响应。
    4. 完成事务:协调者接收到所有参与者的ack响应之后,完成事务。

进入阶段 3 后,无论协调者出现问题,或者协调者与参与者网络出现问题,都会导致参与者无法接收到协调者发出的 do Commit 请求或 abort 请求。此时,参与者都会在等待超时之后,继续执行事务提交。

优点

相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题,阶段 3 中协调者出现问题时,参与者会继续提交事务。

缺点

数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 doCommit 指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。

解决方案:TCC(事务补偿)

TCC(Try Confirm Cancel)方案是一种应用层面侵入业务的两阶段提交。是目前最火的一种柔性事务方案,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作

TCC分为两个阶段,分别如下:

  • 第一阶段:Try(尝试),主要是对业务系统做检测及资源预留 (加锁,锁住资源)
  • 第二阶段:本阶段根据第一阶段的结果,决定是执行confirm还是cancel
    1. Confirm(确认):执行真正的业务(执行业务,释放锁)
    2. Cancle(取消):是预留资源的取消(出问题,释放锁)

最终一致性保证

  • TCC 事务机制以初步操作(Try)为中心的,确认操作(Confirm)和取消操作(Cancel)都是围绕初步操作(Try)而展开。因此,Try 阶段中的操作,其保障性是最好的,即使失败,仍然有取消操作(Cancel)可以将其执行结果撤销。
  • Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。也就是说只要Try成功,Confirm一定成功(TCC设计之初的定义) 。
  • Confirm与Cancel如果失败,由TCC框架进行重试补偿
  • 存在极低概率在CC环节彻底失败,则需要定时任务或人工介入

优点

  • 性能提升:具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。
  • 数据最终一致性:基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据的一致性。
  • 可靠性:解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群。

缺点

TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本。

解决方案:可靠消息服务

基本原理

一般分为事务到发起者和事务到参与者:

  • 事务发起者A执行本地事务
  • 事务发起者A通过MQ将需要执行到事务信息发送给参与者们
  • 事务参与者们接收到消息后执行本地事务

所以只要MQ中到消息还在,那么事务参与者即便失败了也没有关系,可以不断的重试,直到成功!所以问题到关键点来到了以下两点:

  • 我发的消息一定要成功
  • 消息一定要可靠,用于参与者到重试等等

除此之外,事务的参与者失败了,能不能让事务到发起者回滚? 答案:不能。因为事务到发起者已经提交事务了,所以这个是最终一致性的,适用于主业务+子业务的场景。

本地消息表

本地消息表的方案最初是由 eBay 提出,核心思路是将分布式事务拆分成本地事务进行处理。

角色包括:事务主动方、事务被动方

运行原理

通过在事务主动发起方额外新建事务消息表,事务发起方处理业务和记录事务消息在本地事务中完成。轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。

  • 事务主动方在同一个本地事务中处理业务和写消息表操作
  • 事务主动方通过消息中间件,通知事务被动方处理事务
  • 事务被动方通过消息中间件,通知事务主动方事务已处理的消息
  • 事务主动方接收中间件的消息,更新消息表的状态为已处理

出错的处理:

  • 步骤1处理出错,由于还在事务主动方的本地事务中,直接回滚即可
  • 步骤2、3处理出错,由于事务主动方本地保存了消息,只需要轮询消息重新通过消息中间件发送,事务被动方重新读取消息处理业务即可
  • 如果是业务上处理失败,事务被动方可以发消息给事务主动方回滚事务
  • 如果事务被动方已经消费了消息,事务主动方需要回滚事务的话,需要发消息通知事务主动方进行回滚事务

优点

  • 从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于消息中间件,弱化了对 MQ 中间件特性的依赖。
  • 方案轻量,容易实现。

缺点

  • 与具体的业务场景绑定,耦合性强,不可公用。
  • 消息数据与业务数据同库,占用业务系统资源。
  • 业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限。

RocketMQ事务消息

处理逻辑

基于 MQ 的分布式事务方案其实是对本地消息表的封装,将本地消息表基于 MQ 内部,其他方面的协议基本与本地消息表一致。如下所示,本地消息表方案唯一不同就是将本地消息表存在了MQ内部,而不是业务数据库中。

所以消息中间件如何处理成为了关键。

在本地消息表方案中,保证事务主动方发写业务表数据和写消息表数据的一致性是基于数据库事务,RocketMQ 的事务消息相对于普通 MQ提供了2PC 的提交接口。

正常情况:事务主动方发消息

  • 步骤1,发送方向 MQ 服务端(MQ Server)发送 half 消息
  • 步骤2,MQ Server 将消息持久化成功之后,向发送方 ack 确认消息已经发送成功
  • 步骤3,发送方开始执行本地事务逻辑
  • 步骤4,发送方根据本地事务执行结果向 MQ Server 提交二次确认(commit 或是 rollback)
  • 步骤5,MQ Server 收到 commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 rollback 状态则删除半消息,订阅方将不会接受该消息

异常情况:事务主动方消息恢复

在断网或者应用重启等异常情况下,图中 4 提交的二次确认超时未到达 MQ Server,此时处理逻辑如下:

  • 步骤5:MQ Server 对该消息发起消息回查
  • 步骤6:发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果
  • 步骤7:发送方根据检查得到的本地事务的最终状态再次提交二次确认
  • 步骤8:MQ Server基于 commit/rollback 对消息进行投递或者删除

优点

相比本地消息表方案,MQ 事务方案优点是

  • 消息数据独立存储 ,降低业务系统与消息系统之间的耦合
  • 吞吐量大于使用本地消息表方案

缺点

  • 一次消息发送需要两次网络请求(half 消息 + commit/rollback 消息)
  • 业务处理服务需要实现消息状态回查接口

优缺点

优点:

  • 业务相对简单,不需要编写三个阶段业务
  • 是多个本地事务的结合,因此资源锁定周期短,性能好

缺点:

  • 代码侵入
  • 依赖于MQ的可靠性
  • 消息发起者可以回滚,但是消息参与者无法引起事务回滚
  • 事务时效性差,取决于MQ消息发送是否及时,还有消息参与者的执行情况

解决方案:AT模式

2019年1月份,Seata开源了AT模式。AT模式是一种无侵入的分布式事务解决方案。可以看做是对TCC或者二阶段提交模型的一种优化,解决了TCC模式中的代码侵入、编码复杂等问题。

也分两个阶段:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
  • 二阶段:
    • 提交异步化,非常快速地完成。
    • 回滚通过一阶段的回滚日志进行反向补偿。

基本原理

一阶段正常的处理业务,二阶段根据一阶段的结果自动提交或者回滚事务。

那么是怎么自动实现的呢?

工作机制

参考官网:https://seata.io/zh-cn/docs/dev/mode/at-mode.html

解决方案:Saga 事务

Saga 事务源于 1987 年普林斯顿大学的 Hecto 和 Kenneth 发表的如何处理 long lived transaction(长活事务)论文。

Saga 事务核心思想是将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。


参考: