一、引言
1、事务简介
事务(Transaction)是访问并可能更新数据中各种数据项的一个程序执行单元(unit)。在关系数据库中,一个事务由一组SQL语向组成。事务应该具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。
- 原子性(atomicity): 事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。
- 一致性(consistency): 事务必须是使数据库从一个一致性状态变到另一个一致性状态,事务的中间状态不能被观察到的。
- 隔离性(isolation): 一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。隔离性又分为四个级: 读未提交(read uncommited)、读已提交(read commited,解决读)、可重复读(repeatable read,解决虚读)、串行化(serializable,解决幻读)。
- 持久性(durabilty): 持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响任何。
事务机制在实现时,都应该考虑事务的ACID特性,包括:本地事务、分布式事务,及时不能都很好的满足,也要考虑支持到什么程度。
2、本地事务
@Transational
大多数场景下,我们的应用都只需要操作单一的数据库,这种情况下的事务称之为本地事务(Local Transacton。本地事务的ACID特性是数据库直接提供支持。
3、分布式事务
多个数据库时,服务与服务之间,数据库与数据库之间互相调用时,不属于同一个进程,用@Transational本地事务不能解决,得使用分布式事务。
4、常见的分布式事务解决方案
- seata
- 消息队列
- saga
- XA
他们共同点:都是“两阶段(2PC)”,指完成整个分布式事务,划分成两个步骤完成。两阶段后面会详细说。
二、什么是seata
Seata 是一款阿里巴巴开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。AT模式是阿里首推的模式。
官网:http://seata.io/zh-cn/index.html
源码:https://github.com/seata/seata
官方demo:https://github.com/seata/seata-samples
1.1 seata的三大角色
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
其中TC为单独部署的server服务器,TM和RM为嵌入到应用中的Client客户端。
1.2 分布式事务的生命周期
在Seata 中,一个分布式事务的生命周期如下:
- TM 请求 TC 开启一个全局事务。TC 会生成一个 XID 作为该全局事务的编号。XID会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起。
- RM 请求 TC 将本地事务注册为全局事务的分支事务,通过全局事务的XID 进行关联。
- TM 请求 TC 告诉 XID 对应的全局事务是进行提交还是回滚。
- TC 驱动 RM 们将 XID 对应的自己的本地事务进行提交还是回滚。
1.3 Seata 基本运行原理
通过XID作为全局事务的编号,存入数据库中。事务完成后会清楚表中对应数据。对应这上面的生命周期。
- global_table 表存储全局事务信息,xid主键。每一个@GlobalTransactional就会生成一个xid。
- branch_table 表存储事务分支信息,branch_id为主键,xid为父级主键。每执行一次数据库操作就会生成一条分支信息。
- lock_table 锁表信息,锁住当前事务对应的数据条,完成事务与事务之间的隔离。记录全局锁,当前持有全局事务ID、分支事务ID、数据库主键。
- undo_log会存储每次分支运行前后的数据,存储了branch_id和xid,以及rollback_info运行前后的数据信息(beforeImage执行前数据,afterImage为执行的数据,数据信息是sqlType以及值,可以拼接成SQL的数据信息,根据该表完成回滚需执行的SQL)。
- distributed_lock Seata 0.7.1 及以上版本才有 。
distributed_lock
和lock_table
都是 Seata 分布式事务框架中用于协调分布式事务的表,但是它们的作用不同。distributed_lock
表是 Seata 分布式事务框架中用于实现分布式锁的表,主要用于实现分布式事务的并发控制,保证分布式事务的原子性和一致性。当一个事务需要锁定一个资源时,会在distributed_lock
表中插入一条记录,当事务释放资源时,会删除对应的记录。
lock_table
表则是用于记录分布式事务的全局锁定信息,用于实现分布式事务的一致性。当一个分布式事务在执行时,需要锁定某个资源时,会在lock_table
表中插入一条记录,并将对应资源的状态设置为锁定状态。当分布式事务完成时,会释放所有相关资源,同时将lock_table
表中对应的记录删除,将资源状态设置为可用状态。
总之,distributed_lock
表用于分布式事务的并发控制,lock_table
表则用于分布式事务的全局锁定信息记录和管理。
默认的表如下:
三、seata二阶段提交协议
一个分布式的全局事务,整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:
- 一阶段 prepare 行为。
- 二阶段 commit 或 rollback 行为。
全阶段由一个协调者进行总控制,其他为参与者,一个协调者负责控制与其他参与者的通信。
一阶段流程(预处理阶段):
- 询问。 协调者向所有参与者发送事务请求,询问是否可执行事务操作,然后等待各个参与者的响应。
- 执行。各个参与者接收到协调者事务请求后,执行事务操作(例如更新一个关系型数据库表中的记录),并将 Undo 和 Redo 信息记录事务日志中。
- 响应。 如果参与者成功执行了事务写入 Undo 和 Redo 信息,则向协调者返回 ack:YES 响应,否则返回 NO 响应。当然,参与者也可能宕机,从而不会返回响应。
二阶段(提交/回滚阶段):
- commit 请求。协调者向所有参与者发送 Commit 请求。
- 事务提交。参与者收到 Commit 请求后,执行事务提交,提交完成后释放事务执行期占用的所有资源。
- 反馈结果。参与者执行事务提交后向协者发送Ack 响应。
- 完成事务。接收到所有参与者的Ack 响应后,完成事务提交中断事务。
在执行 Prepare 步骤过程中,如果某些参与者执行事务失败、岩机或与协调者之间的网络中断,那么协调者就无法收到所有参与者的ack:YES 响应,或者某个参与者返回了 NO 响应此时,协调者就会进入回退流程,对事务进行回退。(将 Commt 请求替换为红色的 Rollback 请求)。
- rollback 请求。协调者向所有参与者发送 Rollback 请求。
- 事务回滚。参与者收到 Rollback请求后,使用 Prepare阶段的Undo日志执行事务回滚,完成后释放事务执行期占用的所有资源。
- 反馈结果。参与者执行事务提交后向协者发送Ack 响应。
- 完成事务。接收到所有参与者的Ack 响应后,完成事务提交中断事务。
2PC的问题:
主要出在一个协调者和多个参与者机制的问题。
- 同步阻塞。 参与者在等待协调者的指令时,其实是在等待其他参与者的响应,在此过程中,参与者是无法进行其他操作的,也就是阻塞了其运行。倘若参与者与协调者之间网络异常导致参与者一直收不到协调者信息,那么会导致参与者一直阻塞下去。
- 协调者宕机风险。单点在 2PC 中,一切请求都来自协调者,所以协调者的地位是至关重要的,如果协调者宕机,那么就会使参与者一直阻塞并一直占用事务资源。如果协调者也是分布式,使用选主方式提供服务,那么在一个协调者挂掉后,可以选取另一个协调者继续后续的服务,可以解决单点问题,但是,新协调者无法知道上一个事务的全部状态信息(例如已等待 Prepare 响应的时长等),所以也无法顺利处理上一个事务。
- 数据不一致。 Commit 事务过程中 Commit 请求/ Rollback 请求可能因为协调者宕机或协调者与参与者网络问题丢失,那么就导致了部分参与者没有收到 Commit /Rollback 请求,而其他参与者则正常收到执行了 Commit/Rollback 操作,没有收到请求的参与者则继续阻塞。这时,参与者之间的数据就不再一致了。
当参与者执行 Commit / Rollback 后会协调者发送Ack,然而协调者不论是否收到所有的参与者的 Ack,该事务也不会再有其他补救措施了,协调者能做的也就是等待超时后向事务发起者返回一个“我不确定该事务是否成功”。二阶段提交中极微小出现。
4.环境可靠性依赖。 协调者 Prepare 请求发出后,等待响应,然而如果有参与者宕机或与协调者之间的网络中断,都会导致协调者无法收到所有参与者的响应,那么在 2PC中,协调者会等待一定时间,然后超时后,会触发事务中断,在这个过程中,协调者和所有其他参与者都是出于阻塞的,这种机制对网络问题常见的现实环境来说太苛刻了。
四、AT模式
AT(auto transcation)——自动的事务,是一种无侵入的分布式事务解决方案。
seata框架实现AT模式,也是AT模式的一个典型的代表。
在AT 模式下,用户只需关注自己的“业务 SQL",用户的“业务 SQL"作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。
一阶段:
业务数据和回滚日志记录在同一事务中提交,释放本地锁和连接资源。核心在于对业务SQL进行解析,转换成undo log,并入库。
在一阶段,Seata 会拦截”业务 SQL”,首先解析 SQL 语义,找到业务 SQL"要更新的业务数据,在业务数据被更新前,将其保存成"before image",然后执行“业务 SQL"更新业务数据,在业务数据更新之后,再将其保存成”after image",最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
二阶段提交:
分布式事务操作成功,则TC通知RM异步删除undo log。
二阶段如果是提交的话,因为“业务 SQL"在一阶段已经提交至数据库,所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
**二阶段回滚:**分布式事务操作失败,TM会向TC发起回滚请求,RM收到协调器TC发来的回滚请求,通过XID和Branch ID找到相应的回滚日志记录,通过回滚记录反向更新SQL并执行,以完成分支的回滚。
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的业务 SQL还原业务数据。回滚方式便是用 “before image” 还原业务数据但在还原前要首先要校验脏写,对比“数据库当前业务数据和"ahter image",如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。
五、TCC模式
TCC模式需要用户根据自己的业务场景实现Try、Comfirm和Cancel三个操作;事务发起方在一阶段执行Try方式,在二阶段提交执行Confirm方法,二阶段回滚执行Cancel方法。
- 侵入性比较强,并且得自己实现相关事务控制的逻辑。
- 在整个过程中基本没有锁,性能更强。
六、seata server快速开始
seata db + nacos方式部署高可用集群模式。
6.1 部署指南
官网:http://seata.io/zh-cn/docs/ops/deploy-guide-beginner.html
Seata分TC、TM和RM三个角色,TC(Server端)为单独服务端部署,TM和RM(Client端)由业务系统集成。
server端存储模式支持三种:
- file:(默认)单机模式,全局事务会话信息内存中读写并持久化本地文件root.data,性能较高。路径seata\bin\sessionStore\root.data,默认方式启动时会创建该文件。
- db:(5.7+)高性能模式,全是事务会话信息通过db共享,性能相应差一些。微服务共享,使用该模式。
- redis:seata-server 1.3及以上版本支持,性能较高,存在事务信息丢失风险。
6.2 服务端TC搭建
下载地址:https://github.com/seata/seata/releases/tag
资源目录:
https://github.com/seata/seata/tree/master/script
- client:存放client端sql脚本 (包含 undo_log表) ,参数配置。
- config-center:各个配置中心参数导入脚本,config.txt(包含server和client,原名nacos-config.txt)为通用参数文件。
- server:server端数据库脚本 (包含 lock_table、branch_table 与 global_table) 及各个容器配置
配置db模式:
(1)修改配置文件信息。
路径:seata\conf\file.conf
store {
## store mode: file、db、redis
### 修改为db模式
mode = "db"
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
## 修改数据库信息
url = "jdbc:mysql://127.0.0.1:3306/seata_test"
user = "root"
password = "123456"
minConn = 5
maxConn = 30
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
}
(2)创建数据库seata_test并执行对应SQL
数据库表脚本在资源目录地址中:
https://github.com/seata/seata/blob/master/script/server/db/mysql.sql
在对应的库执行一下里面的SQL。
一共有4个表,根据版本不一样,有的只有3个表。
我们将https://github.com/seata/seata/blob/master/script的资源列表下载下来,放入到seata-server文件下,后续会用到。
6.3 更改注册中心和配置中心
将服务端TC注册到nacos,达到高可用模式。
配置seata\conf\registry.conf文件配置成nacos。
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
# 注册中心。配置成naocs,默认file
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = "nacos"
password = "nacos"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
# 配置中心也修改为nacos
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
}
}
6.4 修改资源列表script\config-center\conf.txt文件信息
# 配置成db
store.mode=db
#修改DB相关配置
store.db.url=jdbc:mysql://127.0.0.1:3306/seata_test?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=123456
#service.vgroupMapping.default_tx_group=default
#事务分组,异地机房停电容错
#default_tx_group可以自定义,比如(gaunghzou、shanghai),对应的#client也要去设置
#1、客户端需要去设置seata.service.vgroup-#mapping.my_test_tx_group=guangzhou,指定对应的分组。
#根据guangzhou不可用,可以快速切换成shanghai
#2、default必须等于注册中心
即(seata\conf\registry.conf中registry配置的cluster)cluster = "default"的值
service.vgroupMapping.guangzhou=default
#If you use a registry, you can ignore it
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
6.5 将修改后的信息推送到nacos
使用seata\script\config-center\nacos文件下的脚本进行推送到nacos配置中心。
.sh在windows下不能运行,安装git即可。
我使用的是后面nacos-config-interactive.sh的方式。
也可以在启动时指定相关信息使用第一种标本推送。
sh $(SEATAPATH)/script/config-center/nacos/nacos-config.sh -h localhost -p 8848 -g SEATA GROUP-t 5a3c7d6c-f497-4d68-a71a-2e5e3340b3ca
参数说明:
-h: host,默认值localhost
-p: port,默认值8848
-g:配置分组,默认值为SEATA GROUP
-t:租户信息,对应Nacos 的命名空间ID字段,默认值为空
推送成功后如图:
6.6 启动Seata server
源码启动: 执行server模块下ioseata.server.Server,java的main方法
命令启动: bin/seata-server.sh -h 127.0.0.1 -p 8091 -m db -n 1 -e test
参数说明:
linux下集群部署启动seata server,指定端口:
bin/seata-server.sh -p 8092
本地windows直接运行seata\bin\seata-server.bat,默认8091,启动成功后,就能在nacos界面看到seata服务。
七、seata client端快速开始
学习地址:
http://seata.io/zh-cn/docs/user/quickstart.html
7.1 添加pom依赖
<!-- seata,需放入用到的所有微服务中-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
7.2 各微服务对应数据库中添加undo_log表
用于回滚的日志表。
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
7.3 配置文件配置
seata:
#配置 tx-service-group事务分组
tx-service-group: guangzhou
#配置seata的注册中心(nacos模式),告诉Seata client 怎么去访问seata server (TC)
registry:
type: nacos
nacos:
application: seata-server #seata server服务名
server-addr: 127.0.0.1:8848 #seata server所在的服务地址
username: nacos
password: nacos
group: SEATA_GROUP
#配置seata的配置中心(nacos模式)
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848 #seata server所在的服务地址
username: nacos
password: nacos
group: SEATA_GROUP
此处和server端一直,即6.4中说明的。
#1、客户端需要去设置seata.service.vgroup- guangzhou#mapping.my_test_tx_group=guangzhou,指定对应的分组。
7.4 在接口中使用@GlobalTransactional注解即可
@GlobalTransactional
public String add(Order order){
//入库 order 数据库+1
orderMapper.insert(order);
//库存 stock 数据库-1
String reduce = stockFeignService.reduce();
// int a = 1/0; 进行异常测试两个库中数据的回滚
return "openFeign远程调用:"+reduce + "---"+product;
}