分布式事务的背景和解决方案

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

在常用的关系型数据库都是具备事务特性的。

那什么是事务呢事务是数据库运行的一个逻辑工作单元在这个工作单元内的一系列SQL命令具有原子性操作的特点也就是说这一系列SQL指令要么全部执行成功要么全部回滚不执行。

如果是不执行那么对于数据库中的数据来说数据没有发生任何改变。

数据库事务要满足四个需求

  • 原子性Atomic 事务必须是原子工作单元对数据进行修改要么全部执行要么全部都不执行

  • 一致性Consistent 事务在完成时必须使所有数据都保持一致状态事务结束时所有的内部数据结构都必须是正确的如果事务是并发多个系统也必须如同串行事务一样操作。其主要特征是保护性和不变性。(Preserving an Invariant)

  • 隔离性Isolation 由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。

  • 持久性duration 事务完成之后对系统的影响是永久性的不会被回滚。

关于分布式事务的背景

通常情况下传统的关系型数据库只能保证单个数据库中多个数据表的事务特性。

一旦多个SQL操作涉及到多个数据库这类的事务无法解决跨库事务问题。

在传统架构下这种问题出现的情况非常少但是在分布式微服务架构中分布式事务的问题变得更加突出。

以电商项目为例假设我们要实现电商系统中的支付功能它的实现流程如下

image-20230109221145774

在微服务架构中应用被拆分成以业务模块为单元的服务并且每个服务有自己的数据库系统。

当用户发起支付时会涉及到以下几个事务操作

  1. 创建支付订单

  2. 从资金服务中扣除余额

  3. 从红包服务中扣除余额

  4. 更新支付结果

这是四个典型的事务操作而且这些操作分别属于不同的数据库最终期望的结果是希望这三个服务所对应的数据是一致的很显然传统的事务无法解决这个问题

这就引出了分布式事务问题所谓分布式事务就是事务具有分布式特性简单理解就是如何保证多个小事务组成的大事务的ACID特性。

X/Open DTP 事务模型

说到分布式事务我们不得不提的就是 X/OPENDTP事务模型它是X/Open这个组织定义的一套分布式事务的标准也就是定义了规范和API接口由各个厂商进行具体的实现。

这个标准提出了使用二阶段提交2PC来保证分布式事务的完整性。

二阶段提交模型 2PC

在了解 DTP 事务模型之前我们先来了解一下什么是2PC协议。

两阶段提交又称2PC2PC是一个非常经典的强一致、中心化的原子提交协议 。

如下图所示我们知道在分布式事务中多个小事务的提交与回滚只有当前进程知道其他进程是不清楚的。

image-20230109222612031

而为了实现多个数据库的事务一致性就必然需要引入第三方节点来进行事务协调如下图所示。

image-20230109222636253

从图中可以看出通过一个全局的分布式事务协调工具来实现多个数据库事务的提交和回滚在这样的架构下事务的管理方式就变成了两个步骤。

  1. 第一个步骤开启事务并向各个数据库节点写入事务日志。

  2. 第二个步骤根据第一个步骤中各个节点的执行结果来决定对事务进行提交或者回滚。

这就是所谓的2PC提交协议。

image-20230109222754389

2PC提交流程如下

  1. 表决阶段此时 TM协调者向所有的参与者发送一个 事务请求参与者在收到这请求后如果准备好了(写事务日志就会向 TM发送一个执行成功消息作为回应告知 TM 自己已经做好了准备否则会返回一个失败消息
  2. 提交阶段TM 收到所有参与者的表决信息如果所有参与者一致认为可以提交事务那么TM就会发送 提交消息否则发送回滚消息对于参与者而言如果收到提交消息就会提交本地事务否则就会取消本地事务。

三阶段提交模型 3PC

既然提到了2PC那我们就顺便说一下三阶段提交模型 3PC 。

三阶段提交是在两阶段提交的基础上增加了CanCommit阶段并引入了超时机制。一旦事务参与者迟迟没有收到协调者的Commit请求就会自动进行本地commit这样相对有效地解决了协调者单点故障的问题。

但是性能问题和不一致问题仍然没有根本解决。下面我们还是一起看下三阶段流程的是什么样的

  • 1PC

    image-20230111221609938

    这个阶段类似于2PC中的第二个阶段中的Ready阶段是一种事务询问操作。事务的协调者向所有参与者询问“你们是否可以完成本次事务”如果参与者节点认为自身可以完成事务就返回“YES”否则“NO”。而在实际的场景中参与者节点会对自身逻辑进行事务尝试简单来说就是检查下自身状态的健康性看有没有能力进行事务操作。

  • 2PC

    image-20230111221712178

    在阶段一中如果所有的参与者都返回Yes的话那么就会进入PreCommit阶段进行事务预提交。此时分布式事务协调者会向所有的参与者节点发送PreCommit请求参与者收到后开始执行事务操作并将Undo和Redo信息记录到事务日志中。参与者执行完事务操作后此时属于未提交事务的状态就会向协调者反馈“Ack”表示我已经准备好提交了并等待协调者的下一步指令。

    否则如果阶段一中有任何一个参与者节点返回的结果是No响应或者协调者在等待参与者节点反馈的过程中超时2PC中只有协调者可以超时参与者没有超时机制。整个分布式事务就会中断协调者就会向所有的参与者发送**“abort”**请求。

  • 3PC

    image-20230111222331246

    在阶段二中如果所有的参与者节点都可以进行PreCommit提交那么协调者就会从预提交状态提交状态。然后向所有的参与者节点发送"doCommit"请求参与者节点在收到提交请求后就会各自执行事务提交操作并向协调者节点反馈**“Ack”**消息协调者收到所有参与者的Ack消息后完成事务。

    相反如果有一个参与者节点未完成PreCommit的反馈或者反馈超时那么协调者都会向所有的参与者 节点发送abort请求从而中断事务。

相比较2PC而言3PC对于协调者Coordinator和参与者Partcipant都设置了超时时间而2PC只有协调者才拥有超时机制。这解决了一个什么问题呢这个优化点主要是避免了参与者在长时间无法与协调者节点通讯协调者挂掉了的情况下无法释放资源的问题因为参与者自身拥有超时机制会在超时后自动进行本地commit从而进行释放资源。而这种机制也侧面降低了整个事务的阻塞时间和范围。

另外通过CanCommitPreCommitDoCommit三个阶段的设计相较于2PC而言多设置了一个缓冲阶段保证了在最后提交阶段之前各参与节点的状态是一致的。

以上就是3PC相对于2PC的一个提高相对缓解了2PC中的前两个问题但是3PC依然没有完全解决数据不一致的问题。

DTP 事务模型

基于上述分析了解2PC之后我们再来理解下 DTP 事务模型。

X/Open了定义了规范和API接口这个标准提出了使用二阶段提交来保证分布式事务的完整性如下图所示。

image-20230109223310581

X/Open DTP模型定义了三个角色和两个协议其中三个角色分别如下

  • APApplication Program表示应用程序也可以理解成使用DTP模型的程序。

  • RMResource Manager资源管理器这个资源可以是数据库 应用程序通过资源管理器对资源进行控制资源管理器必须实现XA定义的接口。

  • TMTransaction Manager表示事务管理器负责协调和管理全局事务事务管理器控制整个全局事务管理事务的生命周期并且协调资源。

两个协议分别是

  • XA协议 XA 是X/Open DTP定义的资源管理器和事务管理器之间的接口规范TM用它来通知和协调相关RM事务的开始、结束、提交或回滚。目前Oracle、Mysql、DB2都提供了对XA的支持 XA接口是双向的系统接口在事务管理器™以及多个资源管理器之间形成通信的桥梁XA不能自动提交

    https://dev.mysql.com/doc/refman/8.0/en/xa.html

    https://dev.mysql.com/doc/refman/8.0/en/xa-statements.html

    XA协议的语法主流的数据库都支持 XA协议从而能够实现跨数据库事务。

    XA {START|BEGIN} xid [JOIN|RESUME] # 负责开启或者恢复一个事务分支并且管理XID到调用线程
    
    XA END xid [SUSPEND [FOR MIGRATE]] # 负责取消当前线程与事务分支的关联
    
    XA PREPARE xid # 负责询问RM 是否准备好了提交事务分支
    
    XA COMMIT xid [ONE PHASE] # 通知RM提交事务分支
    
    XA ROLLBACK xid # 通知RM回滚事务分支
    
    XA RECOVER [CONVERT XID]
    
  • TX协议 全局事务管理器与资源管理器之间通信的接口

在分布式系统中每一个机器节点虽然都能够明确知道自己在进行事务操作过程中的结果是成功还是失败但却无法直接获取到其他分布式节点的操作结果。因此当一个事务操作需要跨越多个分布式节点的时候为了保持事务处理的ACID特性就需要引入一个“协调者”TM来统一调度所有分布式节点的执行逻辑这些被调度的分布式节点被称为AP。TM负责调度AP的行为并最终决定这些AP是否要把事务真正进行提交到RM。

基于XA协议的2PC提交流程

在X/OpenDTP模型中一个分布式事务所涉及的SQL逻辑都执行完成并到了RM要最后提交事务的关键时刻为了避免分布式系统所固有的不可靠性导致提交事务意外失败TM 果断决定实施两步走的方案这个就称为二阶提交如图所示。

image-20230109224335307

我们按照如上2PC的提交流程在MySQL中模拟一下。

# 启动一个XA事务 (xid 必须是一个唯一值; [JOIN|RESUME] 字句不被支持)
xa start 'xatest';
insert into user(username,password) values('cc','cc');
# 结束一个XA事务 ( [SUSPEND [FOR MIGRATE]] 字句不被支持)
xa end 'xatest';

# 准备 
# 此动作会把这个事务的redo日志写入innodb redo log只要这一阶段是成功的那么后续XACommit一定会成功
xa prepare 'xatest';

XA COMMIT 'xatest'; //提交事务
# 或者回滚代码如果xa prepare这个环节出现错误事务协调者就把这个事务回滚。
xa rollback 'xatest'; //回滚

基于 XA 协议的开源框架

主流的数据库如oracle、mysql都支持XA协议因此都可以基于xa协议规范通过二阶段提交来实现数据的一致性。

而J2EE也遵循了X/OpenDTP规范设计并实现了Java里面的分布式事务编程接口规范-JTA。因此我们可以利用这些JTA中提供的API来完成多个数据库的事务一致性处理。

但是在XA事务中根据前面我们的操作过程可以发现另外在XA COMMIT阶段如果其中一个RM因为网络超时没有收到数据提交的指令就会导致数据不一致。

所以如果我们要基于这些API来实现一个比较成熟的分布式事务解决方案还需要考虑到这些问题并提出解决方案。比如针对这个问题我们可以采用重试的机制来完成数据一致性。

因此针对这类分布式事务解决方案的开源框架也很多比如

  • Atomikos

    Atomikos是为Java平台提供的开源的事务管理工具它包含收费和开源两个版本开源版本基本能满足我们的需求。

  • Bitronix

    Bitronix是一个流行的开源JTA事务管理器实现。

  • Seata

    阿里巴巴开源的事务解决方案。

主流分布式事务解决方案

我们要知道即便是基于2pc协议提交它也是有两类落地形式的。

  1. 基于zookeeper的 改进版本的2pc提交投票方式少数服从多数

  2. 基于Atomikos、Bitronix等使用XA协议实现的强一致性提交也就是所有参与者都成功才能提交。

对于这些不同的落地方案核心还是由场景来决定但同时又引出来一个问题为什么要有不同的落地方案

CAP 理论

大家都听过CAP理论所谓CAP理论说的是在分布式架构下的数据一致性问题和性能问题的平衡方案 它代表三个关键词

  • Consistency 一致性同一数据的多个副本是否实时相同。

  • Availability 可用性 一定时间内 & 系统返回一个明确的结果则称为该系统可用。

  • Partition tolerance 分区容错性 将同一服务分布在多个系统中从而保证某一个系统宕机仍然有其他系统提供相同的服务。

CAP理论告诉我们在分布式系统中C、A、P三个条件中我们最多只能选择两个。那么问题来了究竟选择哪两个条件较为合适呢

对于一个业务系统来说可用性和分区容错性是必须要满足的两个条件并且这两者是相辅相成的。业务系统之所以使用分布式系统主要原因有两个

  • 提升整体性能

    当业务量猛增单个服务器已经无法满足我们的业务需求的时候就需要使用分布式系统使用多个节点提供相同的功能从而整体上提升系统的性能这就是使用分布式系统的第一个原因。

  • 实现分区容错性

    单一节点 或 多个节点处于相同的网络环境下那么会存在一定的风险万一该机房断电、该地区发生自然灾害那么业务系统就全面瘫痪了。为了防止这一问题采用分布式系统将多个子系统分布在不同的地域、不同的机房中从而保证系统高可用性。

这说明分区容错性是分布式系统的根本如果分区容错性不能满足那使用分布式系统将失去意义。

此外可用性对业务系统也尤为重要。在大谈用户体验的今天如果业务系统时常出现“系统异常”、响应时间过长等情况这使得用户对系统的好感度大打折扣在互联网行业竞争激烈的今天相同领域的竞争者不甚枚举系统的间歇性不可用会立马导致用户流向竞争对手。因此我们只能通过牺牲一致性来换取系统的可用性和分区容错。

所以这才有了Zookeeper中的基于少数服从多数的2pc落地方案Zab协议。

因此也引出了另外一个理论叫Base理论。

Base 理论

CAP理论告诉我们一个悲惨但不得不接受的事实——我们只能在C、A、P中选择两个条件。而对于业务系统而言我们往往选择牺牲一致性来换取系统的可用性和分区容错性。不过这里要指出的是所谓的“牺牲一致性”并不是完全放弃数据一致性而是牺牲强一致性换取弱一致性。

  • BA Basic Available 基本可用

    整个系统在某些不可抗力的情况下仍然能够保证“可用性”即一定时间内仍然能够返回一个明确的结果。只不过“基本可用”和“高可用”的区别是

    • “一定时间”可以适当延长
    • 给部分用户直接返回一个降级页面从而缓解服务器压力。但要注意返回降级页面仍然是返回明确结果。
  • SSoft State柔性状态

    同一数据的不同副本的状态可以不需要实时一致。

  • EEventual Consisstency最终一致性

    同一数据的不同副本的状态可以不需要实时一致但一定要保证经过一定时间后仍然是一致的。

所以对于服务来说我们有很多的方案去选择

  1. 提供查询服务确认数据状态

  2. 幂等操作对于重发保证数据的安全性

  3. TCC事务操作

  4. 补偿操作

  5. 定期校对

下面分别对不同的解决方案做一个详细的说明。

基于可靠性消息的最终一致性方案

最终一致性方案也称为弱一致性方案。

它是基于BASE理论的落地也就是说多个数据库节点的数据运行存在中间状态但是在未来的某个时间内这些数据会达成一致。

可靠性消息最终一致性方案是指在多个事务中当前事务发起方执行完本地事务后发送一条数据同步消息给到事务参与方。

基于可靠性消息队列需要保证这个消息一定能够被其他事务参与方接收并执行成功从而实现多个事务参与者之间的数据最终一致性。

实现原理如下图所示

image-20230110225936070

假设在用户注册的场景中用户注册成功后需要给用户增加积分。在这里就涉及到两个事务操作

  • 一个是用户服务中的用户记录新增

  • 另一个是积分服务中的积分增加

在这个场景中我们可以采用最终一致性方案当用户服务的本地事务执行成功后再把增加积分的消息通过可靠性消息传递到积分服务中积分服务消费该消息后执行积分新增操作。

这个方案看起来好像没问题但是在整个流程中还是会存在数据不一致的情况。

本地事务与消息发送的原子性问题

不难发现上面这个方案仍然存在原子性问题我们需要保证本地事务执行成功且消息发送必须成功。

  1. 假设我们先发送消息再操作数据库

    这种方案有可能会出现消息发送成功但是数据库操作失败的问题从而导致数据不一致。

  2. 先操作数据库再发送消息

    这种情况下看起来没问题因为如果MQ发送失败就会抛出异常导致数据库事务回滚。

    但是如果是MQ超时异常使得数据库回滚但MQ可能已经正常发送了同样也会导致数据不一致问题。

事务参与方接收消息的可靠性

事务参与方也就是MQ的消费端必须要能够从消息队列接收到消息也就是无论什么情况下消费者必须至少收到一个消息。

例如在 rabbitmq 中开启confirm模式消费者消费成功后会给生产者返回确认信息告知生产者成功消费信息了。

image-20230110230903316

消息重复消费的问题

为了保证“消费者至少收到一个消息”的情况那么MQ这边需要实现消息的可靠投递为了保证这一点必然会采用重试机制。

同样假设存在这样一种情况 消费者已经收到了这个消息但是在发送消息确认通知时MQ因为超时得到一个超时异常。

此时消息中间件会重复投递这个消息就会导致消费者重复接收消息的问题。如下图所示

image-20230110231530795

可靠性消息最终一致性解决方案

因此通过上述分析要实现完全的可靠性解决方案我们还需要在整个流程中分别解决这三个问题。

基于本地消息表实现重发

本地消息表这个方案最初是 eBay 提出的此方案的核心是通过本地事务保证数据业务操作和消息的一致性然后通过定时任务将消息发送至消息中间件待确认消息发送给消费方成功再将消息删除。

下面以注册送积分为例来说明下例共有两个微服务交互用户服务和积分服务用户服务负责添加用户积分服务负责增加积分。

image-20230110231758932

整体流程如下

  1. 用户注册 用户服务在本地事务新增用户和增加 “积分消息日志”。用户表和消息表通过本地事务保证一致这种情况下本地数据库操作与存储积分消息日志处于同一个事务中本地数据库操作与记录消息日志操作具备原子性。

  2. 定时任务扫描日志在第一步中我们把需要发送到消息队列的事务消息保存到了消息日志表为了保证消息能够百分之百的发送给消息队列这里可以启动一个定时任务不断扫描这个消息表中的消息发送到消

    息队列。当消息队列反馈发送成功后删除该消息日志否则等到下一个任务周期重试。

  3. 消息的可靠性消费主流的MQ都带了消息确认机制ack消费者监听MQ的消息。

    • 当消费者受到消息并处理完成后返回一个ACK给到MQ告诉MQ该消息已经消费完成MQ不需要再向消费者投递该消息。
    • 否则MQ会不断重新投递这个消息给到消费者。

    当消费者收到“新增积分”消息后根据该消息的逻辑规则完成指定用户的积分更新再基于MQ的ACK机制从而可以实现可靠的消息投递功能。

消息重复投递的幂等性保障

在可靠性消息投递过程中由于MQ的重试机制有可能会出现消费者重复收到同一个消息的情况。

因此我们需要保证消息投递的幂等性所谓的幂等性就是MQ重复调用多次产生的业务结果与调用一次产生的业务结果相同

在分布式架构中我们调用一个远程服务去完成一个操作除了成功和失败以外还有未知状态那么针对这个未知状态我们会采取一些重试的行为

或者在消息中间件的使用场景中消费者可能会重复收到消息。对于这两种情况消费端或者服务端需要采取一定的手段也就是考虑到重发的情况下保证数据的安全性。一般我们常用的手段

  1. 状态机实现幂等

    有限状态机(Finite-state machine FSM)又称有限状态自动机简称状态机是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。用于处理复杂的状态转换。

    比如在支付的例子中为了简单理解我们不考虑退款、取消订单等复杂的状态只考虑未支付和已支付两种状态之间的转换。

    image-20230110232743158

    由上面的状态转换图可以看到相同支付订单ID从未支付状态要不就是支付不成功停留在未支付状态要不就是支付成功状态转移为已支付。

    此状态转移过程不可逆。

    所以相同支付ID的请求支付状态只能进行一次从未支付到已支付的转换。通过这种方式从而保证了其幂等性。

  2. 数据库唯一约束实现幂等

  3. 使用 redis 提供的 setNx 命令

基于RocketMQ的事务消息方案

基于可靠性消息的最终一致性方案我们可以采用消息中间件MQ实现事务最终一致。

这里我们采用RocketMQ来实现。 RocketMQ 自4.3版本之后开始支持事务消息事务消息为分布式事务提供了非常方便的解决方案整体实现流程如下图所示

image-20230111224800536

  1. MQ生产者发送事务消息到MQ Broker中此时MQ Broker把消息状态标记为(Prepared)此时这条消息MQ消费者是无法消费的。

  2. MQ Broker回应MQ生产者消息发送成功表示MQ Broker已经接收到了消息并且保存成功。

  3. MQ 生产者此时开始执行本地事务。

  4. MQ 生产者本地事务执行成功后自动向MQ Broker发送一个Commit消息MQ Broker收到该消息后把第一步发送的那条消息标记为“可消费”此时MQ 消费者可以正常收到这条消息进行处理。

    MQ消费者消费完这条消息后向MQ Broker发送一个ACK表示成功消费否则MQ Broker会不断重试。

  5. 如果MQ 生产者执行本地事务的过程中MQ生产者宕机或者一直没有发送commit给到MQ BrokerMQ Server将会不停的询问同组的其他Producer来获取事务执行状态。MQ Server会根据事务回查结果来决定是否投递消息。

RocketMQ事务消息的设计主要是解决Producer端发送消息和本地事务执行结果的原子性问题因此RocketMQ的设计中Broker和Producer端提供了双向通信的能力使得Broker天生可以作为一个事务协调者。

而RocketMQ本身提供的存储机制未事务消息提供了持久化的能力再加上RocketMQ的高可用机制以及可靠性消息设计能力为事务消息在系统发生异常时仍然能够保证消息的成功投递。

因此它的实现思想其实就是本地消息表的实现思路只不过本地消息表移动到了MQ的内部最终解决Producer端的消息发送和本地事务的原子性问题。

实际上基于可靠性消息的最终一致性在实际应用还有很多地方可以借鉴并使用这些案例比如支付宝的支付状态同步或者我们做短信发送时的设计方案如下图所示

image-20230111230034279

TCC事务解决方案

最后除了我们常说的 2PC 方案TCC 也是一种分布式事务的实现方案。

TCC的方案在电商、金融领域落地也比较多他是一种两阶段提交的基于应用层的改进方案。TCCTry-Confirm-Cancel又称补偿事务。其核心思想是“针对每个操作都要注册一个与其对应的确认和补偿撤销操作”。它将整个业务逻辑的每个分支分成了Try、Confirm、Cancel三个操作。

  • Try阶段主要是对业务系统做检测及资源预留。

  • Confirm阶段确认执行业务操作。

  • Cancel阶段取消执行业务操作。

TCC事务解决方案本质上是一种补偿的思路它把事务运行过程分成Try、Confirm/cancel 两个阶段每个阶段由业务代码控制这样事务的锁力度可以完全自由控制。

需要注意的是TCC事务和2pc的思想类似但并不是2pc的实现TCC不再是两阶段提交而只是它对事务的提交/回滚是通过执行一段confirm/cancel业务逻辑来实现并且也并没有全局事务来把控整个事务逻辑。

image-20230110233341610

假设在一个支付场景中用户购买商品的业务逻辑如下

  1. 支付服务根据采购需求创建订单发起支付。

  2. 资金服务从用户资金帐户中扣除余额。

  3. 红包服务从用户红包账户中扣除余额。

image-20230110233542234

在TCC事务中整体的工作流程如下

  1. 首先在支付服务中创建支付订单在在TRY阶段把支付订单状态修改为PAYING。同时远程调用红包服务和资金服务把付款方的余额进行扣减预扣款

  2. 如果在TRY阶段任何一个服务失败TCC事务管理器自动调用这些服务的CANCEL方法支付订单状态变为PAY_FAILED同时远程调用红包服务和资金服务把扣减的余额增加回去。

  3. 如果TRY阶段正常完成TCC事务管理器自动调用CONFIRM方法在该方法中把支付订单状态变更为CONFIRMED同时远程调用红包服务和资金服务对应的CONFIRM方法增加收款方的余额。

以上便是TCC 的工作流程但是也有不足之处。

不足之处则在于对应用的侵入性非常强业务逻辑的每个分支都需要实现try、confirm、cancel三个操作。此外其实现难度也比较大需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。为了满足一致性的要求confirm和cancel接口还必须实现幂等。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6