🌟 后端 | SpringCloud -> 分布式事务



Seata 官方文档

不同的微服务都会有自己独立的数据库,完成一个业务通常也会跨多个数据库完成业务。而每个微服务都会执行自己负责的本地事务。

整个业务中,各个本地事务是有关联的,因此每个微服务的本地事务,也可以称为分支事务。多个有关联的分支事务一起就组成了全局事务。我们必须保证整个全局事务同时成功或失败。

每个单独的分支事务就是传统的单体事务,都可以满足 ACID 特性,但全局事务跨越多个服务、多个数据库则相互之间没有协同,无法保证最终结果的统一。

CAP 定理

分布式系统(Distributed System)是指由多个独立的计算节点组成的系统,这些节点通过网络协同工作,共同完成一项任务或提供一个统一的服务。每个节点可能是物理上分开的服务器或虚拟机,它们之间通过消息传递进行通信,看起来像是一个整体。

分布式系统有三个指标:

  • Consistency(一致性):用户访问分布式系统中的任意节点,得到的数据必须一致。
  • Availability(可用性):用户访问分布式系统时,读或写操作总能成功。
  • Partition tolerance (分区容错性):当分布式系统节点之间出现网络故障导致节点之间无法通信的情况,节点形成几个独立的网络分区,整个系统也要持续对外提供服务。

但是通常 A 和 C 只能选一个,想要一致,那就不能让用户再继续更新信息,那么就牺牲了可用性;而允许用户在出现网络状态后继续更新信息,那么就不用保证数据同步,因此一致性不能保证。

  • AP 思想:各个子事务分别执行和提交,无需锁定数据。允许出现结果不一致,然后采用弥补措施恢复,实现最终一致即可。例如 AT 模式就是如此
  • CP 思想:各个子事务执行后不要提交,而是等待彼此结果,然后同时提交或回滚。在这个过程中锁定资源,不允许其它人访问,数据处于不可用状态,但能保证一致性。例如 XA 模式

BASE 理论

BASE 理论就是一种取舍的方案,不再追求完美,而是最终达成目标。

  • Basically Available (基本可用):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
  • Soft State(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态。
  • Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。

Seata

Seata 作为一款解决分布式事务的框架,目前功能最完善、使用最多。

解决分布式事务的思想其实很简单,即找一个统一的事务协调者,与多个分支事务通信,检测每个分支事务的执行状态,保证全局事务下的每一个分支事务同时成功或失败。
The thought solving the problem of distributed transaction is simple. It just needs to find the one that is unified transaction coordinator, conmunicating with multiple branch, checking the execution status of each branch transaction and ensuring that branch transaction under the global transaction succeeds or fails at the same time.

Seata 也是这样实现,Seata 事务管理拥有 3 个重要的角色:

  • TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
  • TM (Transaction Manager) - 事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。
  • RM (Resource Manager) - 资源管理器:管理分支事务,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

TM 和 RM 可以理解为 Seata 的客户端部分,引入到参与事务的微服务依赖中即可。将来 TM 和 RM 就会协助微服务,实现本地分支事务与 TC 之间交互,实现事务的提交或回滚。

而 TC 服务则是事务协调中心,是一个独立的微服务,需要单独部署。

微服务部署 TC 服务

数据库表 - 持久化

单独的微服务,则需要单独的数据库。需建立:

  • branch_table
  • distributed_table
  • global_table
  • lock_table

引入依赖

为方便各个微服务集成 seata,可以把 seata 的配置共享到注册中心(nacos),通常微服务也需要同时引入 seata、nacos 依赖。

The config of Seata can be shared in the register center like Nacos for facilitating the microservice integration with Seata. Usually the microservice needs to add the dependencies of seata and nacos at the same time.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!--统一配置管理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--读取bootstrap文件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

配置 Seata

在 nacos 上添加一个共享的 seata 配置,命名为 shared-seata.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
seata:
registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
type: nacos # 注册中心类型 nacos
nacos:
server-addr: 192.168.150.101:8848 # nacos地址
namespace: "" # namespace,默认为空
group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUP
application: seata-server # seata服务名称
username: nacos
password: nacos
tx-service-group: hmall # 事务组名称
service:
vgroup-mapping: # 事务组与tc集群的映射关系
hmall: "default"

在微服务模块 application.yaml 同级下添加 bootstrap.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring:
application:
name: trade-service # 服务名称
profiles:
active: dev
cloud:
nacos:
server-addr: 192.168.150.101 # nacos地址
config:
file-extension: yaml # 文件后缀名
shared-configs: # 共享配置
- dataId: shared-jdbc.yaml # 共享 mybatis 配置
- dataId: shared-log.yaml # 共享日志配置
- dataId: shared-swagger.yaml # 共享日志配置
- dataId: shared-seata.yaml # 共享 seata 配置

改造 application.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 8085
feign:
okhttp:
enabled: true # 开启OKHttp连接池支持
sentinel:
enabled: true # 开启Feign对Sentinel的整合
hm:
swagger:
title: 交易服务接口文档
package: com.hmall.trade.controller
db:
database: hm-trade

在业务方法前边使用 @GlobalTransactional 注解,也就是在标记事务的起点,将来 TM 就会基于这个方法判断全局事务范围,初始化全局事务。

Seata 分布式事务解决方案

  • XA
  • TCC
  • AT
  • SAGA

XA

XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范 描述了全局的 TM 与局部的 RM 之间的接口,几乎所有主流的数据库都对 XA 规范 提供了支持。

两阶段提交

一阶段

  • 事务协调者通知每个事务参与者执行本地事务
  • 本地事务执行完成后报告事务执行状态给事务协调者,此时事务不提交,继续持有数据库锁

二阶段

  • 事务协调者基于一阶段的报告来判断下一步操作
  • 如果一阶段都成功,则通知所有事务参与者,提交事务
  • 如果一阶段任意一个参与者失败,则通知所有事务参与者回滚事务

XA 模型

RM 一阶段的工作:

  1. 注册分支事务到 TC
  2. 执行分支业务 sql 但不提交
  3. 报告执行状态到 TC

TC 二阶段的工作:

  • TC 检测各分支事务执行状态
    • 如果都成功,通知所有 RM 提交事务
    • 如果有失败,通知所有 RM 回滚事务

RM 二阶段的工作:

  • 接收 TC 指令,提交或回滚事务

优缺点

XA模式的优点是什么?

  • 事务的强一致性,满足 ACID 原则
  • 常用数据库都支持,实现简单,并且没有代码侵入

XA模式的缺点是什么?

  • 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
  • 依赖关系型数据库实现事务

实现 XA

  1. 在 shared-seata.yaml 配置文件中设置:
1
2
seata:
data-source-proxy-mode: XA
  1. 利用 @GlobalTransactional 标记分布式事务的入口方法
1
2
3
4
5
6
7
8
@GlobalTransactional
public Long createOrder(OrderFormDTO orderFormDTO) {
// 1. 订单数据
Order order = new Order();
// 1.1 查询商品
List<OrderDetailDTO> detailDTOS = orderFormDTO.getDetails();
// 1.2 ...
}

AT

AT 模式同样是分阶段提交的事务模型,不过弥补了 XA 模型中资源锁定周期过长的缺陷。

AT 模型

阶段一 RM 的工作:

  • 注册分支事务
  • 记录 undo-log(数据快照)
  • 执行业务 sql 并提交
  • 报告事务状态

阶段二提交时 RM 的工作:

  • 删除undo-log即可

阶段二回滚时 RM 的工作:

  • 根据 undo-log 恢复数据到更新前

流程梳理

一阶段:

  1. TM 发起并注册全局事务到 TC
  2. TM 调用分支事务
  3. 分支事务准备执行业务 SQL
  4. RM 拦截业务 SQL,根据 where 条件查询原始数据,形成快照。
  5. RM 执行业务 SQL,提交本地事务,释放数据库锁。此时 money = 90
  6. RM 报告本地事务状态给 TC

二阶段:

  1. TM 通知 TC 事务结束
  2. TC 检查分支事务状态
    • 如果都成功,则立即删除快照
    • 如果有分支事务失败,需要回滚。读取快照数据,将快照恢复到数据库。此时数据库再次恢复为 初始数据

脏写问题

在极端情况下,特别是多线程并发访问 AT 模式的分布式事务时,有可能出现脏写问题。在有时事务执行失败回退时是根据快照恢复,再删除快照,然而在该事务提交完成到删除这一时间段,另一个事务刚好进来更新,那么就造成了一次更新失败的情况,出现脏写。

解决思路就是引入了全局锁的概念。在释放 DB 锁之前,先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据

AT 与 XA 的区别

AT 模式与 XA 模式最大的区别:

  • XA 模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。
  • XA 模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚。
  • XA 模式强一致;AT 模式最终一致

可见,AT 模式使用起来更加简单,无业务侵入,性能更好。
因此企业 90% 的分布式事务都可以用 AT 模式来解决。

TCC

TCC 模式与 AT 模式非常相似,每阶段都是独立事务,不同的是 TCC 通过人工编码来实现数据恢复。需要实现三个方法:

  • try:资源的检测和预留;
  • confirm:完成资源操作业务;要求 try 成功 confirm 一定要能成功。
  • cancel:预留资源释放(回滚),可以理解为try的反向操作。

事务悬挂和空回滚

如一个分布式事务中包含两个分支事务,try 阶段(锁定资源)时一个分支执行成功,另一个分支事务阻塞,如果阻塞时间太长,可能导致全局事务超时而触发二阶段的 cancel 操作,两个分支事务都会执行 cancel 操作

但是一个分支是未执行 try 操作的,直接执行 cancel 操作反而导致数据错误。所以这种情况下尽管 cancel 方法需要执行,但却不能做任何回滚操作,因此称为空回滚。

对于整个空回滚的分支事务,将来 try 方法阻塞结束依然会执行,但是整个全局事务其实已经结束了,因此永远不会再有 confirm 和 cancel ,也就是说这个事务执行了一半,处于悬挂状态。

TCC 的优缺点

优点:

  • 一阶段完成直接提交事务,释放数据库资源,性能好
  • 相比 AT 模型,无需生成快照,无需使用全局锁,性能最强
  • 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库

缺点:

  • 有代码侵入,需要人为编写 try、Confirm 和 Cancel 接口,太麻烦
  • 软状态,事务是最终一致
  • 需要考虑 Confirm 和 Cancel 的失败情况,做好幂等处理、事务悬挂和空回滚处理