一、分布式事务
首先要明白事务是指数据库中的一组操作,这些操作要么全部成功执行,要么全部不执行,以保持数据的一致性和完整性。在本地事务中,也就是传统的单机事务,必须要满足原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)四个特性,通常称为ACID特性。而分布式事务是指跨越多个分布式系统的事务,不再是单机事务,其中涉及到多个独立服务。分布式事务主要是为了保证这多个跨服务的系统之间的操作的一致性和原子性。
二、分布式系统面临的问题
在分布式系统中,通常是根据业务逻辑分成多个微服务独立部署,且每个微服务都有自己单独的数据源。以最常见的用户购买商品的业务逻辑,可以分为3个微服务:
- 订单服务(Order):用户根据商品的库存量来创建订单。
- 仓储服务(Stock):创建订单成功后对给定的商品扣除库存量。
- 账户服务(Account):订单支付成功后从用户账户中扣除余额。
当用户购买一件商品从下单到支付成功,总共要涉及3个服务的操作。由于各个服务间调用可能存在网络延迟、节点故障、通信失败等原因,导致分布式事务无法像单个系统的事务那样简单的就能实现ACID特性。这期间往往会产生许多问题,最常见的如下:
- 部分失败:在一个分布式事务中,有些参与者执行成功,而其他参与者执行失败,导致事务的部分操作成功,部分操作失败。
- 数据不一致:在一个分布式事务中,数据的一致性无法保证,可能因为参与者之间的数据冲突或者数据同步延迟。
三、分布式理论基础
3.1 CAP定理
CAP定理是分布式当中一个非常重要的理论,指的是在一个分布式系统中一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance):
- 一致性(Consistency):在任何时刻,对于同一个数据项,所有节点上的值都是相同的。
- 可用性(Availability):系统在任何时候都能够响应客户端请求,不会出现宕机或不可用的情况。
- 分区容错性(Partition tolerance):分布式系统中的节点之间可能会出现网络故障,但是无论哪个节点出现故障,整个分布式系统任然能够对外提供服务。
CAP定理中不可能同时满足这三者,最多只能同时满足其中两项:
- 放弃P(CA):在分布式系统中,系统间的网络不可能100%保证正常,一定会有故障的时候,又想在节点故障的情况下还保证节点间数据的一致性和可用性是不现实的。因此分区容错性不可避免。
- 放弃A(CP):就是在节点存在故障后,为了保证各个节点间数据的强一致性,就必须等故障节点恢复正常后,再将数据同步过去,在等待故障节点恢复正常的这段时间服务处于阻塞状态,不可用。也就是放弃服务的可用性,从而保证节点间数据的强一致性。
- 放弃C(AP):就是在节点存在故障后,不用再等故障节点恢复正常,依旧对外提供服务,只不过使用的是故障前的数据提供服务。也就是可能存在节点间的数据不一致,用放弃数据强一致性来实现服务的可用性。其实这里放弃一致性,并不是完全不需要数据一致性,是指放弃数据的强一致性,保留数据的最终一致性。
CAP理论具体应用:
- Redis :属于 cp 模型。
- Redis-cluster 属于 ap 模型
- Zookeeper:属于cp模型。
- MongoDB :属于cp模型。
- Eureka:属于ap模型。
3.2 BASE理论
CAPBASE理论指的是:基本可用(Basically Available),软状态(Soft State),最终一致性(Eventual Consistency),核心思想是即便无法做到强一致性,但应该采用适合的方式保证最终一致性。
- BA:Basically Available 基本可用,分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。
- S:Soft State 软状态,允许系统存在中间状态,而该中间状态不会影响系统整体可用性。
- E:Eventual Consistency 最终一致性,系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。
一致性可分为以下3类:
- 强一致性:在任意时刻,所有节点看到的数据都必须是一样的。
- 弱一致性:数据的更新可能会出现延迟,允许在延迟的这段时间内,节点看到的数据不是最新的。
- 最终一致性:不保证在任意时刻任意节点上的同一份数据都是相同的,但是在一段时间后,节点间的数据会最终达到一致状态。
四、Seata框架
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
4.1 Seata中三个重要的角色
- TC (Transaction Coordinator) 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。
- TM (Transaction Manager) 事务管理器:定义全局事务的范围、向TC申请开始全局事务、向TC申请提交或回滚全局事务。
- RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
4.2 Seata事务整体执行流程
- 由TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID;
- XID会在微服务调用链路的上下文中传播;
- RM向TC注册分支事务,纳入XID对应全局事务的范围;
- RM驱动分支事务的执行,并报告分支事务的执行结果给TC;
- TM向TC发起针对XID的全局事务提交或回滚决议;
- TC根据RM报告的实际分支事务的执行结果进行决策,TC会向RM发送一个提交或回滚消息。
- RM会将这个提交或回滚消息广播给所有参与该全局事务的分支服务,让它们各自提交或回滚分支事务。
五、Seata中四种事务模式
5.1 AT模式
AT模式是Seata的默认模式,在该模式下Seata工作在应用层,无业务侵入,主要是通过对本地关系数据库的分支事务的协调来完成全局事务。
5.1.1 前提
- 基于支持本地 ACID 事务的关系型数据库。
- Java 应用,通过 JDBC 访问数据库。
5.1.2 整体机制
两阶段提交协议的演变:
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:
- 提交异步化,非常快速地完成。
- 回滚通过一阶段的回滚日志进行反向补偿。
5.1.3 写隔离
Seata通过 全局锁 来保证多个全局事务之间的写隔离,从而防止脏写的发生。全局锁 本身就是一条记录,由xid-事务、table-表名、pk-数据的行组成,全局锁 由事务协调者TC控制,只有持有该全局锁的全局事务,才具备本地事务的执行权。
- 一阶段本地事务提交前,需要确保先拿到 全局锁 。
- 拿不到 全局锁 ,不能提交本地事务。
- 拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
在这整个过程中全局事务2都没能拿到 全局锁,也就没能提交全局事务,所以不会发生 脏写 的问题。
5.1.4 读隔离
在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted)。如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。
SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。
5.1.5 AT模式的工作流程
以更新 person 业务表的数据为例:update person set age = 18 where name = 'Tom';
一阶段:
- 解析 SQL:得到 SQL 的类型(UPDATE),表(person),条件(where name = ‘Tom’)等相关的信息。
- 查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。
select id, name, since from product where name = 'TXC';
- 执行业务 SQL:更新这条记录的 age 为 18。
- 查询后镜像:根据前镜像的结果,通过 主键 定位数据。
select id, name, age from person where id = 1;
- 插入回滚日志:把前、后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中。
- 提交本地事务前,向 TC 注册分支:申请 person 表中,主键值等于 1 的记录的 全局锁 。
- 拿到全局锁后,提交本地事务。
- 将本地事务提交的结果上报给 TC。
二阶段–回滚:
- 收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
- 通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
- 数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理。
- 根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句。
- 提交本地事务,并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。
二阶段–提交:
- 收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
- 异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。
5.1.6 XA模式实现
- 添加配置seata:data-source-proxy-mode: AT
- 在需要分布式事务的业务代码上添加注解 @GlobalTransactional
5.1.7 AT模式的优缺点
优点:
- 一阶段完成直接提交事务,释放数据库资源,性能比较好。
- 没有代码侵入,框架自动完成回滚和提交。
- 使用方便,在需要分布式事务的业务代码上添加注解@GlobalTransactional即可。
缺点:
- 属于最终一致,对一致性的要求相对较低,中间可能会出现数据不一致的情况。
- 由于AT模式依赖于本地事务,所以受限于本地事务管理器支持的隔离级别,可能无法满足某些特定的隔离需求。
5.2 TCC模式
TCC模式下,Seata也是在业务层面实现的二阶段提交方案,不过AT模式不同的是,TCC模式不再依赖本地事务,而是通过人工编码定义一个接口,接口中包含三个方法,供每个分支事务来实现各种的提交和回滚逻辑。因此,会有业务代码侵入。
// 定义一个全局事务的接口
public interface IGlobalService {
// 尝试预留或锁定资源
boolean tryTransfer();
// 最终的确认操作
boolean confirmTransfer();
// 最终的回滚操作
boolean cancelTransfer();
}
5.2.1 整体机制
也是基于两阶段提交:
- 一阶段:尝试阶段,各个参与者尝试预留或锁定资源。
- 二阶段:
- 确认阶段,各个参与者进行最终的确认操作。
- 取消阶段,各个参与者进行最终的确认操作。
5.2.2 TCC模式的工作流程
一阶段:
Try阶段(尝试阶段):在这个阶段,各个参与者尝试预留或锁定资源,并执行必要的前置检查。如果所有参与者的Try操作都成功,表示资源可用,并进入下一阶段。如果有任何一个参与者的Try操作失败,表示资源不可用或发生冲突,事务将中止。
二阶段:
Confirm阶段(确认阶段):在这个阶段,各个参与者进行最终的确认操作,将资源真正提交或应用到系统中。如果所有参与者的Confirm操作都成功,事务完成,提交操作得到确认。如果有任何一个参与者的Confirm操作失败,事务将进入Cancel阶段。
二阶段:
Cancel阶段(取消阶段):在这个阶段,各个参与者进行回滚或取消操作,将之前尝试预留或锁定的资源恢复到原始状态。如果所有参与者的Cancel操作都成功,事务被取消,资源释放。如果有任何一个参与者的Cancel操作失败,可能需要进行补偿或人工介入来恢复系统一致性。
5.2.3 用例实现
- 分支事务具体实现
public class BranchServiceImpl implements IGlobalService {@Overridepublic boolean tryTransfer() {// 尝试预留或锁定资源// 如果成功,返回 true;如果失败,返回 false}@Overridepublic boolean confirmTransfer() {// 最终的确认操作// 如果成功,返回 true;如果失败,返回 false}@Overridepublic boolean cancelTransfer() {// 进行回滚或取消操作// 如果成功,返回 true;如果失败,返回 false}
}
- 客户端调用
String xid = RootContext.getXID();
// 开启全局事务
TransactionContext context = new TransactionContext();
context.setXid(xid);
GlobalTransaction tx = GlobalTransactionContext.getCurrentOrCreate();
try {
// 调用参与者的tryTransfer方法
boolean tryResult = branchServiceImpl.tryTransfer();
if (tryResult) {
// 提交全局事务
tx.commit();
} else {
// 回滚全局事务
tx.rollback();
}
} catch (Exception e) {
// 异常时回滚全局事务tx.rollback();
}
5.2.4 TCC模式的优缺点
优点:
- 一阶段直接提交事务,释放数据库资源,性能好;
- 相比AT模型,无需生成快照,无需使用全局锁,性能最强;
- 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库;
缺点:
- 有代码侵入,需要人为的设计和编写try、Confirm和Cancel接口,太麻烦;
- 软状态,事务是最终一致;
- 需要考虑Confirm和Cancel的失败情况,做好幂等处理;
5.3 SAGA模式
Saga模式是将一个长事务分解为多个小的、可逆的事务片段,每个事务片段都是一个真实的本地事务。每个事务片段都有对应的补偿动作,补偿动作用于撤销因事务片段执行所造成的结果。
5.3.1 整体机制
Saga模式的提交过程也分为两个阶段:
- 一阶段:直接提交子事务
- 二阶段:成功则什么都不做;失败则执行补偿业务来回滚;
5.3.2 saga模式的工作流程
一阶段:执行正向操作
- 按照事务的逻辑顺序,依次执行正向操作。每个正向操作都会记录事务的执行状态。
- 如果所有的正向操作都成功执行,则事务提交完成。
- 如果某个正向操作失败,将会触发相应的补偿操作。
二阶段:执行补偿操作
- 按照逆序依次执行已经触发的补偿操作。补偿操作应该具备幂等性,以便可以多次执行而不会造成副作用。
- 如果所有的补偿操作都成功执行,则事务回滚完成。
- 如果补偿操作也失败,需要人工介入或其他手段来解决事务的一致性问题。
5.3.3 saga模式的优缺点
优点:
- 事务参与者可以基于事件驱动实现异步调用,吞吐高;
- 一阶段直接提交事务,无锁,性能好;
- 补偿服务易于实现,不用编写TCC中的三个阶段,实现简单
缺点:
- 软状态持续时间不确定,时效性差;
- 没有锁,没有业务层面的事务隔离,会有脏写
5.4 XA模式
XA模式是事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种事务模式。具有强一致性,牺牲了一定的可用性,无业务侵入。
5.4.1 整体机制
XA模式的提交过程也分为两个阶段:
- 一阶段:准备阶段,各个分支事务执行完本地事务,但不提交本地事务。
- 二阶段:
- 提交阶段,各个分支事务同时进行提交本地事务操作。
- 中断阶段,各个分支事务同时进行回滚本地事务操作。
5.4.2 XA模式的事务执行流程
一阶段:准备阶段
- 每个分支事务的RM注册分支事务到TC;
- 执行分支业务sql但不提交,继续持有数据库的锁;
- RM报告执行状态到TC;
二阶段:提交阶段
- TC检测各分支事务执行状态全部为成功;
- 通知所有RM提交本地事务;
- RM接收TC的通知,提交分支事务,并释放之前持有的数据库锁;
二阶段:中断阶段
- TC检测所有分支事务执行状态中有失败的;
- 通知所有RM回滚本地事务;
- RM接收TC的通知,回滚分支事务,并释放之前持有的数据库锁;
5.4.3 XA模式实现
- 添加配置seata:data-source-proxy-mode: XA
- 在需要分布式事务的业务代码上添加注解 @GlobalTransactional
5.4.4 XA模式的优缺点
优点:
- 通过XA 协议的机制来管理分支事务实现简单,并且没有代码侵入;
- 事务具有强一致性,确保所有参与者要么一起提交事务,要么一起中断事务;
缺点:
- 必须依赖于支持XA 事务的数据库;
- 由于一阶段不提交本地事务,所有的参与者都需要等待事务协调器的指令来进行提交或者回滚,期间可能造成阻塞,影响系统的吞吐量和并发性能。