一、分布式事务
首先要明白事务是指数据库中的一组操作,这些操作要么全部成功执行,要么全部不执行,以保持数据的一致性和完整性。在本地事务中,也就是传统的单机事务,必须要满足原子性(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是不可避免的,C和A只能在两者之中选择一个实现:
- 放弃A(CP):就是在节点存在故障后,为了保证各个节点间数据的强一致性,就必须等故障节点恢复正常后,再将数据同步过去,在等待故障节点恢复正常的这段时间服务处于阻塞状态,不可用。也就是放弃服务的可用性,从而保证节点间数据的强一致性。
- 放弃C(AP):就是在节点存在故障后,不用再等故障节点恢复正常,依旧对外提供服务,只不过使用的是故障前的数据提供服务。也就是可能存在节点间的数据不一致,用放弃数据强一致性来实现服务的可用性。其实这里放弃一致性,并不是完全不需要数据一致性,是指放弃数据的强一致性,保留数据的最终一致性。
CAP理论具体应用:
- Redis :属于 cp 模型。
- Redis-cluster 属于 ap 模型
- Zookeeper:属于cp模型。
- MongoDB :属于cp模型。
- Eureka:属于ap模型。
3.2 BASE理论
BASE理论指的是:基本可用(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) 事务协调者:
- 全局事务的协调中心,独立部署(如Seata-Server)。
- 维护全局事务状态(GlobalTransaction)和分支事务状态(BranchTransaction)。
- 驱动全局事务的提交或回滚。
- TM (Transaction Manager) 事务管理器:
- 定义全局事务边界(@GlobalTransactional注解)。
- 向TC发起全局事务的Begin、Commit或Rollback指令。
- RM (Resource Manager) - 资源管理器:
- 管理分支事务(如数据库操作),向TC注册分支事务、上报状态。
- 执行TC下发的提交/回滚指令。
4.2 Seata事务整体执行流程
TM TC (Seata-Server) RM (DB Proxy)
│ 1. @GlobalTransactional │ │
│───GlobalBeginRequest───────> │ │
│<─────XID─────────────────────┤ │
│ │ │
│ 2. Call Service A │ │
│──────────────────────────────┼─────RPC(XID)───────────────────────> │
│ │ 3. Register Branch (product:100) │
│ │<────BranchRegisterRequest────────────┤
│ │ │
│ │ 4. Execute: │
│ │ - 获取全局锁 │
│ │ - 执行业务SQL │
│ │ - 生成Undo Log │
│ │ - 提交本地事务 │
│ │<────BranchReportRequest(Phase1_Done)─┤
│ │ │
│ 5. Call Service B... │ │
│ │ │
│ 6. 成功 → Commit │ │
│───GlobalCommitRequest───────>│ │
│ │───BranchCommitRequest───────────────>│
│ │ - 删除Undo Log │
│ │ - 释放全局锁 │
│ │<───────Success───────────────────────┤
│ │ │
│ 6. 失败 → Rollback │ │
│───GlobalRollbackRequest─────>│ │
│ │───BranchRollbackRequest─────────────>│
│ │ - 校验当前数据 │
│ │ - 执行反向SQL │
│ │ - 删除Undo Log │
│ │<───────Success───────────────────────┤
- 由TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID;
- XID会在微服务调用链路的上下文中传播;
- RM向TC注册分支事务,纳入XID对应全局事务的范围;
- RM驱动分支事务的执行,并报告分支事务的执行结果给TC;
- TM向TC发起针对XID的全局事务提交或回滚决议;
- TC根据各个RM报告的实际分支事务的执行结果进行决策,TC会向RM发送一个提交或回滚消息。
- RM接收到TC的提交或回滚消息后进行本地的事务提交或者回退操作。
五、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 = 'Tom';
- 执行业务 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 AT模式实现
- 添加配置seata:data-source-proxy-mode: AT
- 在需要分布式事务的业务代码上添加注解 @GlobalTransactional
5.1.7 AT模式的优缺点
优点:
- 无侵入性:业务代码无需改造,仅需添加 @GlobalTransactional 注解。
- 高性能:一阶段已提交本地事务,释放数据库锁;二阶段异步清理,吞吐量高。
- 简单易用:适合快速接入,降低开发门槛。
- 自动补偿:Seata 自动生成反向 SQL 回滚数据(如 update set money=100 的回滚是 update set money=200)。
缺点:
- 最终一致性:异步回滚可能导致短暂数据不一致(通常 < 1s)。
- 全局锁冲突:高并发更新同一行数据时,TC 的全局锁可能成为瓶颈。
- 依赖数据库类型:仅支持 MySQL、PostgreSQL 等关系型数据库(需有主键)。
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 {
@Override
public boolean tryTransfer() {
// 尝试预留或锁定资源
// 如果成功,返回 true;如果失败,返回 false
}
@Override
public boolean confirmTransfer() {
// 最终的确认操作
// 如果成功,返回 true;如果失败,返回 false
}
@Override
public 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模式的优缺点
优点:
- 无全局锁:避免锁竞争,适合高并发场景。
- 业务灵活:不依赖数据库事务,可自定义资源管理逻辑(如非数据库操作)。
- 强一致性:Confirm/Cancel 由 TC 严格调度,数据最终强一致。
缺点:
- 高侵入性:需为每个业务编写 Try/Confirm/Cancel 接口,开发量翻倍。
- 设计复杂:需处理 空回滚、幂等性、悬挂问题 等边界场景。
- 资源预留成本:长期冻结资源可能影响业务(如占用库存)。
5.3 SAGA模式
Saga模式是将一个长事务分解为多个小的、可逆的事务片段,每个事务片段都是一个真实的本地事务。每个事务片段都有对应的补偿动作,补偿动作用于撤销因事务片段执行所造成的结果。
5.3.1 整体机制
Saga模式的提交过程也分为两个阶段:
- 一阶段:直接提交子事务
- 二阶段:成功则什么都不做;失败则执行补偿业务来回滚;
5.3.2 saga模式的工作流程
一阶段:执行正向操作
- 按照事务的逻辑顺序,依次执行正向操作。每个正向操作都会记录事务的执行状态。
- 如果所有的正向操作都成功执行,则事务提交完成。
- 如果某个正向操作失败,将会触发相应的补偿操作。
二阶段:执行补偿操作
- 按照逆序依次执行已经触发的补偿操作。补偿操作应该具备幂等性,以便可以多次执行而不会造成副作用。
- 如果所有的补偿操作都成功执行,则事务回滚完成。
- 如果补偿操作也失败,需要人工介入或其他手段来解决事务的一致性问题。
5.3.3 saga模式的优缺点
优点:
- 适合长事务:单个事务可跨多个服务长时间执行(如旅行订票:航班+酒店+租车)。
- 无锁设计:每个子事务独立提交,无全局锁阻塞。
- 灵活补偿:补偿操作可自定义(如取消订单、退款)。
- 异步执行:可通过事件驱动架构(如消息队列)实现。
缺点:
- 弱隔离性:可能出现“脏读”(其他事务可能读到未完成的中间状态)。
- 补偿逻辑复杂:需保证补偿操作的幂等性和可逆性。
- 调试困难:长事务链路长,问题定位复杂。
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模式的优缺点
优点:
- 强一致性:所有 RM 原子性提交或回滚。
- 标准化协议:主流数据库(MySQL、Oracle)均支持 XA。
- 无代码侵入:与 AT 模式类似,只需加注解。
缺点:
- 性能低:一阶段不提交本地事务,会一直持有数据库锁,阻塞时间长,影响系统的吞吐量和并发性能。
- 单点故障:TM 宕机可能导致资源长期锁定。
- 协议限制:不支持非 XA 数据源(如 Redis、MongoDB)。
六、四种模式对比
维度 | AT 模式 | TCC 模式 | Saga 模式 | XA 模式 |
---|---|---|---|---|
一致性 | 最终一致 | 强一致 | 最终一致 | 强一致 |
性能 | ⭐⭐⭐⭐(高) | ⭐⭐(中下) | ⭐⭐⭐(中) | ⭐(低) |
侵入性 | 无侵入 | 高侵入 | 中侵入 | 无侵入 |
锁机制 | 全局锁(可能冲突) | 无锁 | 无锁 | 数据库锁(阻塞) |
适用事务时长 | 秒级 | 秒级 | 分钟/小时级 | 秒级 |
支持资源 | 关系型数据库 | 任意资源 | 任意资源 | XA 数据库 |
开发复杂度 | 低 | 高 | 中 | 低 |
容错能力 | 自动回滚 | 需处理边界问题 | 需设计补偿 | 依赖数据库恢复 |