# 一、分布式事务
# 1.1 基础概念
# 幂等性
幂等性是指同一个操作无论请求多少次,其结果都相同。
幂等操作实现方式有:
- 操作之前在业务方法进行判断如果执行过了就不再执行。
- 缓存所有请求和处理的结果,已经处理的请求则直接返回结果。
- 在数据库表中加一个状态字段(未处理,已处理),数据操作时判断未处理时再处理。
# 事务
事务提供一种机制将一个活动涉及的所有操作纳入到一个不可分割的执行单元,组成事务的所有操作只有在所有操作均能正常执行的情况下方能提交,只要其中任一操作执行失败,都将导致整个事务的回滚。
简单地说,事务提供一种“要么什么都不做,要么做全套(All or Nothing)”机制。
# ACID
数据库事务需要满足 ACID(原子性、一致性、隔离性、持久性)四个特性
- 原子性(Atomicity)指事务作为整体来执行,要么全部执行,要么全不执行
- 一致性(Consistency)指事务应确保数据从一个一致的状态转变为另一个一致的状态
- 隔离性(Isolation)指多个事务并发执行时,一个事务的执行不应影响其他事务的执行
- 持久性(Durability)指已提交的事务修改数据会被持久保存
# CAP
CAP 定理,又被叫作布鲁尔定理。对于设计分布式系统(不仅仅是分布式事务)的架构师来说,CAP 就是你的入门理论。
C (一致性):对某个指定的客户端来说,读操作能返回的写操作。
对于数据分布在不同节点上的数据来说,如果在某个节点更新了数据,那么在其他节点如果都能读取到这个***的数据,那么就称为强一致,如果有某个节点没有读取到,那就是分布式不一致。
A (可用性):非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。可用性的两个关键一个是合理的时间,一个是合理的响应。
合理的时间指的是请求不能被阻塞,应该在合理的时间给出返回。合理的响应指的是系统应该明确返回结果并且结果是正确的,这里的正确指的是比如应该返回 50,而不是返回 40。
P (分区容错性):当出现网络分区后,系统能够继续工作。打个比方,这里集群有多台机器,有台机器网络出现了问题,但是这个集群仍然可以正常工作。
熟悉 CAP 的人都知道,三者不能共有,如果感兴趣可以搜索 CAP 的证明,在分布式系统中,网络无法 100% 可靠,分区其实是一个必然现象。
如果我们选择了 CA 而放弃了 P,那么当发生分区现象时,为了保证一致性,这个时候必须拒绝请求,但是 A 又不允许,所以分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。
对于 CP 来说,放弃可用性,追求一致性和分区容错性,我们的 ZooKeeper 其实就是追求的强一致。
对于 AP 来说,放弃一致性(这里说的一致性是强一致性),追求分区容错性和可用性,这是很多分布式系统设计时的选择,后面的 BASE 也是根据 AP 来扩展。
顺便一提,CAP 理论中是忽略网络延迟,也就是当事务提交时,从节点 A 复制到节点 B 没有延迟,但是在现实中这个是明显不可能的,所以总会有一定的时间是不一致。
同时 CAP 中选择两个,比如你选择了 CP,并不是叫你放弃 A。因为 P 出现的概率实在是太小了,大部分的时间你仍然需要保证 CA。就算分区出现了你也要为后来的 A 做准备,比如通过一些日志的手段,使其他机器恢复至可用。
# BASE
BASE 是 Basically Available
(基本可用)、Soft state
(软状态)和 Eventually consistent
(最终一致性)三个短语的缩写,是对 CAP 中 AP 的一个扩展。
- 基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。
- 软状态:允许系统中存在中间状态,这个状态不影响系统可用性,这里指的是 CAP 中的不一致。
- 最终一致:最终一致是指经过一段时间后,所有节点数据都将会达到一致。
BASE 解决了 CAP 中理论没有网络延迟,在 BASE 中用软状态和最终一致,保证了延迟后的一致性。
BASE 和 ACID 是相反的,它完全不同于 ACID 的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。
# 数据库本地事务
在单一数据节点中,事务仅限于对单一数据库资源的访问控制,称之为 本地事务。几乎所有的成熟的关系型数据库都提供了对本地事务的原生支持。
# 柔性事务(弱一致性)
如果将实现了 ACID 的事务要素的事务称为 刚性事务 的话,那么基于 BASE 事务要素的事务则称为柔性事务。 BASE 是基本可用、柔性状态和最终一致性这三个要素的缩写(还记得之前说的 CAP 定理与 BASE 理论嘛)。
- 基本可用(Basically Available)保证分布式事务参与方不一定同时在线;
- 柔性状态(Soft state)则允许系统状态更新有一定的延时,这个延时对客户来说不一定能够察觉;
- 而最终一致性(Eventually consistent)通常是通过消息传递的方式保证系统的最终一致性;
在 ACID 事务中对隔离性的要求很高,在事务执行过程中,必须将所有的资源锁定。 柔性事务的理念则是通过业务逻辑将互斥锁操作从资源层面上移至业务层面。通过 放宽对强一致性要求(不保证隔离性),来换取系统吞吐量的提升。基于 ACID 的强一致性事务和基于 BASE 的最终一致性事务都不是银弹,只有在最适合的场景中才能发挥它们的最大长处。
可通过下表详细对比它们之间的区别,以帮助开发者进行技术选型。
本地事务 | 两(三)阶段事务 | 柔性事务 | |
---|---|---|---|
业务改造 | 无 | 无 | 实现相关接口 |
一致性 | 不支持 | 支持 | 最终一致 |
隔离性 | 不支持 | 支持 | 业务方保证 |
并发性能 | 无影响 | 严重衰退 | 略微衰退 |
适合场景 | 业务方处理不一致 | 短事务 & 低并发 | 长事务 & 高并发 |
# 1.2 什么是分布式事务
在基于微服务的分布式应用环境下,越来越多的应用场景要求对多个服务的访问及其相对应的多个数据库资源能纳入到同一个事务当中,分布式事务应运而生
关系型数据库虽然对本地事务提供了完美的 ACID 原生支持。 但在分布式的场景下,它却成为系统性能的桎梏。如何让数据库在分布式场景下满足 ACID 的特性或找寻相应的替代方案,是分布式事务的重点工作
分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。
本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
# 1.3 是否真的需要分布式事务
在说方案之前,首先你一定要明确你是否真的需要分布式事务?
上面说过出现分布式事务的两个原因,其中有个原因是因为微服务过多。我见过太多团队一个人维护几个微服务,太多团队过度设计,搞得所有人疲劳不堪。
而微服务过多就会引出分布式事务,这个时候我不会建议你去采用下面任何一种方案,而是请把需要事务的微服务聚合成一个单机服务,使用数据库的本地事务。
因为不论任何一种方案都会增加你系统的复杂度,这样的成本实在是太高了,千万不要因为追求某些设计,而引入不必要的成本和复杂度。
如果你确定需要引入分布式事务可以看看下面几种常见的方案。
# 1.4 分布式事务的解决方案
# 1、两阶段提交(2PC)
最早由 X/Open 国际联盟提出的 X/Open Distributed Transaction Processing(DTP) 模型,简称 XA 协议,通过抽象出来的 AP, TM, RM 的概念可以 保证事务的强一致性。 其中 TM 和 RM 间采用 XA 的协议进行双向通信。 与传统的本地事务相比,XA 事务增加了 prepare 阶段,数据库除了被动接受提交指令外,还可以反向通知调用方事务是否可以被提交。 因此 TM 可以收集所有分支事务的 prepare 结果,最后进行原子的提交,保证事务的强一致性。
- 在 XA 协议中分为两阶段:
- 第一阶段:准备阶段(prepare) 协调者通知参与者准备提交订单,参与者开始投票。 协调者完成准备工作向协调者回应Yes。
- 第二阶段:提交(commit)/回滚(rollback)阶段 协调者根据参与者的投票结果发起最终的提交指令。 如果有参与者没有准备好则发起回滚指令。
- 优点:
- 尽量保证了数据的强一致,实现成本较低,在各大主流数据库都有自己实现,对于 MySQL 是从 5.5 开始支持。
- 缺点:
- 单点问题:事务管理器在整个流程中扮演的角色很关键,如果其宕机,比如在阶段已经完成,在第二阶段正准备提交的时候事务管理器宕机,资源管理器就会一直阻塞,导致数据库无法使用。
- 同步阻塞:在准备就绪之后,资源管理器中的资源一直处于阻塞,直到提交完成,释放资源。
- 数据不一致:两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能。
比如在第二阶段中,假设协调者发出了事务 Commit 的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了 Commit 操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。
总的来说,XA 协议比较简单,成本较低,但是其单点问题,以及不能支持高并发(由于同步阻塞)依然是其的弱点。
# 2、事务补偿(TCC)
关于 TCC(Try-Confirm-Cancel)的概念,最早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。
TCC事务补偿是基于2PC实现的业务层事务控制方案,它是Try、Confirm和Cancel三个单词的首字母,含义如下:
- Try 阶段:尝试执行,完成所有业务检查(一致性),预留必需业务资源(准隔离性)。
- Confirm 阶段:确认真正执行业务,不作任何业务检查,只使用 Try 阶段预留的业务资源,Confirm 操作满足幂等性。要求具备幂等设计,Confirm 失败后需要进行重试。
- Cancel 阶段:取消执行,释放 Try 阶段预留的业务资源,Cancel 操作满足幂等性。Cancel 阶段的异常和 Confirm 阶段异常处理方案基本上一致。
TCC 事务机制相比于上面介绍的 XA,解决了如下几个缺点:
- 解决了协调者单点,由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群。
- 同步阻塞:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。
- 数据一致性,有了补偿机制之后,由业务活动管理器控制一致性。
举个简单的例子:如果你用 100 元买了一瓶水, Try 阶段:你需要向你的钱包检查是否够 100 元并锁住这 100 元,水也是一样的。
如果有一个失败,则进行 Cancel(释放这 100 元和这一瓶水),如果 Cancel 失败不论什么失败都进行重试 Cancel,所以需要保持幂等。
如果都成功,则进行 Confirm,确认这 100 元被扣,和这一瓶水被卖,如果 Confirm 失败无论什么失败则重试(会依靠活动日志进行重试)。
对于 TCC 来说适合一些: 强隔离性,严格一致性要求的活动业务。 执行时间较短的业务。
# 二、Alibaba Seata
# 2.1 Seata是什么
seata github地址 (opens new window)
一个分布式事务的解决方案具有高性能和易用性的微服务架构。
Seata 是一款阿里巴巴开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。
Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
Seata有3个基本组成部分:
- 事务协调器(TC):维护全局事务和分支事务的状态,驱动全局提交或回滚。
- 事务管理器(TM):定义全局事务的范围:开始全局事务,提交或回滚全局事务。
- 资源管理器(RM):管理分支事务正在处理的资源,与TC进行对话以注册分支事务并报告分支事务的状态,并驱动分支事务的提交或回滚。
Seata管理的分布式事务的典型生命周期:
- TM要求TC开始一项新的全球交易。TC生成代表全局交易的XID。
- XID通过微服务的调用链传播。
- RM将本地事务注册为XID到TC的相应全局事务的分支。
- TM要求TC提交或回退XID的相应全局事务。
- TC驱动XID对应的全局事务下的所有分支事务以完成分支提交或回滚。
分布式事务可分为
- 强一致性(刚性事务),保证ACID;——Seata AT模式。
- 弱一致性(柔性事务),BASE理论、最终一致性;——Seata Saga模式。
# 2.2 Seata的4种模式
# 1、AT模式
AT模式实现了强一致性事务。基于支持本地 ACID 事务的关系型数据库。 Java 应用,通过 JDBC 访问数据库。
为解决分布式系统的数据一致性问题出现了两阶段提交协议(2 Phase Commitment Protocol),两阶段提交由协调者和参与者组成,共经过两个阶段和三个操作,部分关系数据库如Oracle、MySQL支持两阶段提交协议。
- TC 事务协调者 维护全局和分支事务的状态,驱动全局事务提交或回滚
- TM 事务管理器(事务发起者) 定义全局事务的范围:开始全局事务、提交或回滚全局事务
- RM 资源管理器(事务参与者) 管理分支事务处理的资源,与 TC 交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚
# 2、TCC模式
由于Seata实现的 TCC需要定义三个接口,prepare、commit、rollback,具有一定侵入性,所以不推荐使用。
# 3、SAGA模式
Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
- 适用场景: 业务流程长、业务流程多 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口
- 优势: 一阶段提交本地事务,无锁,高性能 事件驱动架构,参与者可异步执行,高吞吐 补偿服务易于实现
- 缺点: 不保证隔离性(应对方案见用户文档)
# 4、XA模式
# 2.3 Seata部署
部署 Seata Server
mkdir -p /usr/local/docker/seata
cd /usr/local/docker/seata
vi docker-compose.yml
2
3
我们使用 Docker 部署 Seata Server,Compose 配置文件如下
version: "3"
services:
seata-server:
image: seataio/seata-server
hostname: seata-server
container_name: seata-server
ports:
- "8091:8091"
environment:
- SEATA_PORT=8091
volumes:
- ./config:/root/seata-config
2
3
4
5
6
7
8
9
10
11
12
在 Compose 同级目录下创建配置文件
mkdir -p /usr/local/docker/seata/config
vi /usr/local/docker/seata/config/registry.conf
2
registry {
type = "file"
file {
name = "file.conf"
}
}
config {
type = "file"
file {
name = "file.conf"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在 Compose 同级目录下创建配置文件
vi config/file.conf
service {
# 自定义事务组名称 tx_group,客户端配置需要和服务端一致
vgroup_mapping.tx_group = "default"
default.grouplist = "127.0.0.1:8091"
disableGlobalTransaction = false
}
store {
mode = "file"
file {
dir = "sessionStore"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
启动服务
docker-compose up -d
# 2.4 Seata使用
项目使用(AT模式)
- 主要增加了 Seata 依赖
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
2
3
4
5
- 在 application.yml 配置文件中增加
spring:
cloud:
alibaba:
seata:
# 自定义事务组名称 tx_group,需要与服务端一致
tx-service-group: tx-group
2
3
4
5
6
- 在 resources 目录下创建 registry.conf 配置文件
registry {
type = "file"
file {
name = "file.conf"
}
}
config {
type = "file"
file {
name = "file.conf"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 在 resources 目录下创建 file.conf 配置文件
transport {
type = "TCP"
server = "NIO"
heartbeat = true
enable-client-batch-send-request = true
thread-factory {
boss-thread-prefix = "NettyBoss"
worker-thread-prefix = "NettyServerNIOWorker"
server-executor-thread-prefix = "NettyServerBizHandler"
share-boss-worker = false
client-selector-thread-prefix = "NettyClientSelector"
client-selector-thread-size = 1
client-worker-thread-prefix = "NettyClientWorkerThread"
boss-thread-size = 1
worker-thread-size = 8
}
shutdown {
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
# 自定义事务组名称 tx_group,需要与服务端一致
vgroup_mapping.tx_group = "default"
# 这里配置 Seata Server 的 IP 和 端口
default.grouplist = "192.168.106.121:8091"
enableDegrade = false
disableGlobalTransaction = false
}
client {
rm {
async.commit.buffer.limit = 10000
lock {
retry.internal = 10
retry.times = 30
retry.policy.branch-rollback-on-conflict = true
}
report.retry.count = 5
table.meta.check.enable = false
report.success.enable = true
}
tm {
commit.retry.count = 5
rollback.retry.count = 5
}
undo {
data.validation = true
log.serialization = "jackson"
log.table = "undo_log"
}
log {
exceptionRate = 100
}
support {
spring.datasource.autoproxy = false
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59