1. 分布式事务介绍
传统单体应用场景下,系统的数据保存在一个数据库实例中,通常场景的关系数据库都能自动提供事务保证,并且这种情况下的事务称为本地事务,能保证原子性、一致性、隔离性、持久性(ACID
特性)。
本地事务
@Transational
大多数场景下,我们的应用都只需要操作单一的数据库,这种情况下的事务称之为本地事务 (Local Transaction)。本地事务的ACID特性是数据库直接提供支持。
分布式事务典型场景
当下互联网发展如火如荼,绝大部分公司都进行了数据库拆分和服务化
(SOA)
。在这种情况下,完成某一个业务功能可能需要横跨多个服务,操作多个数据库。这就涉及到到了分布式事务,用需要操作的资源位于多个资源服务器上,而应用需要保证对于多个资源服务器的数据的操作,要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同资源服务器的数据一致性。
跨库事务
跨库事务指的是,一个应用某个功能需要操作多个库,不同的库中存储不同的业务数据。一个服务同时操作2个库的情况:
分库分表
通常一个库数据量比较大或者预期未来的数据量比较大,都会进行水平拆分,也就是分库分表。如
下图,将数据库
B
拆分成了2个库:
对于分库分表的情况,一般开发人员都会使用一些数据库中间件来降低
sql
操作的复杂性。如,对
于
sql
:
insert into user(id,name) values (1,"
张三
"),(2,"
李四
")
。这条
sql
是操作单库的语法,单库情
况下,可以保证事务的一致性。
但是由于现在进行了分库分表,开发人员希望将
1
号记录插入分库
1
,
2
号记录插入分库
2
。所以数
据库中间件要将其改写为
2
条
sql
,分别插入两个不同的分库,此时要保证两个库要不都成功,要不都失败,因此基本上所有的数据库中间件都面临着分布式事务的问题
服务化
Service A
完成某个功能需要直接操作数据库,同时需要调用
Service B
和
Service C
,而
Service B
又同时操作了2
个数据库,
Service C
也操作了一个库。需要保证这些跨服务的对多个数据库的操作要不都成功,要不都失败,实际上这可能是最典型的分布式事务场景。
常见分布式事务解决方案
1
、
seata
阿里分布式事务框架
2
、消息队列
3
、
saga
4
、
XA
他们有一个共同点,都是
“
两阶段
(2PC)
”
。
“
两阶段
”
是指完成整个分布式事务,划分成两个步骤完成。
实际上,这四种常见的分布式事务解决方案,分别对应着分布式事务的四种模式:
AT
、
TCC
、
Saga
、
XA
;
2PC两阶段提交协议:
2PC(
两阶段提交,
Two-Phase Commit)
顾名思义,分为两个阶段:
Prepare
预处理阶段 和
Commit
提交阶段
Prepare
:提交事务请求
基本流程如下图:
1. rollback
请求 协调者向所有参与者发送
Rollback
请求。
2.
事务回滚 参与者收到
Rollback
后,使用
Prepare
阶段的
Undo
日志执行事务回滚,完成后释放事务执行期占用的所有资源。
3.
反馈结果 参与者执行事务回滚后向协调者发送
Ack
响应。
4.
中断事务 接收到所有参与者的
Ack
响应后,完成事务中断
2PC问题
1.
同步阻塞 参与者在等待协调者的指令时,其实是在等待其他参与者的响应,在此过程中,参与者是无法进行其他操作的,也就是阻塞了其运行。 倘若参与者与协调者之间网络异常导致参与者一直收不到协调者信息,那么会导致参与者一直阻塞下去。
2.
单点 在
2PC
中,一切请求都来自协调者,所以协调者的地位是至关重要的,如果协调者宕机,那么就会使参与者一直阻塞并一直占用事务资源。
如果协调者也是分布式,使用选主方式提供服务,那么在一个协调者挂掉后,可以选取另一个协调者继续后续的服务,可以解决单点问题。但是,新协调者无法知道上一个事务的全部状态信息(
例如已等待
Prepare
响应的时长
等
)
,所以也无法顺利处理上一个事务。
3.
数据不一致
Commit
事务过程中
Commit
请求
/Rollback
请求可能因为协调者宕机或协调者与参与者网络问题丢失,那么就导致了部分参与者没有收到 Commit/Rollback
请求,而其他参与者则正常收到执行了
Commit/Rollback
操作,没有收到请求的参与者则继续阻塞。这时,参与者之间的数据就不再一致了。
当参与者执行
Commit/Rollback
后会向协调者发送
Ack
,然而协调者不论是否收到所有的参与者的
Ack
,该事务也不会再有其他补救措施了,协调者能做的也就是等待超时后像事务发起者返回一个“
我不确定该事务是否成功”
。
4.
环境可靠性依赖 协调者
Prepare
请求发出后,等待响应,然而如果有参与者宕机或与协调者之间的网络中断,都会导致协调者无法收到所有参与者的响应,那么在 2PC
中,协调者会等待一定时间,然后超时后,会触发事务中断,在这个过程中,协调者和所有其他参与者都是出于阻塞的。这种机制对网络问题常见的现实环境来说太苛刻了。
2. AT 模式介绍
AT
模式是一种无侵入的分布式事务解决方案。阿里
Seata
框架,实现了该模式。在
AT
模式下,用户只 需关注自己的“
业务
SQL”
,用户的
“
业务
SQL”
作为一阶段,
Seata
框架会自动生成事务的二阶段提交和回滚操作。
一阶段:
Seata
会拦截
"
业务
SQL"
,解析
SQL
语义
查询
“
业务
SQL”
要更新的业务数据,在业务数据被更新前,将其保存成
“before image”
执行
“
业务
SQL”
,更新业务数据
查询更新后的数据,将其保存成
“after image”
将
before image
和
after image
保存至
Undo Log
表中
生成行锁
以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
二阶段(提交):
- 因为 “业务SQL” 在一阶段已经提交至数据库,所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
二阶段(回滚):
- 首先要校验脏写,对比“数据库当前业务数据”和 “after image”
- 如果两份数据完全一致就说明没有脏写,可以还原业务数据。
- 如果不一致就说明有脏写,出现脏写就需要转人工处理。
- 用“before image”还原业务数据
- 删除快照数据和行锁
3. Seata是什么?
Seata
是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。
Seata
将为用户提供了 AT
、
TCC
、
SAGA
和
XA
事务模式,为用户打造一站式的分布式解决方案。
AT
模式是阿里首推的模式。
Seata的三大角色
在
Seata
的架构中,一共有三个角色:
TC (Transaction Coordinator) -
事务协调者
:维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) -
事务管理器
:定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) -
资源管理器
:管理分支事务处理的资源,与
TC
交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
其中,
TC
为单独部署的
Server
服务端,
TM
和
RM
为嵌入到应用中的
Client
客户端。
在 Seata 中,一个分布式事务的生命周期如下:
1.TM
请求
TC
开启一个全局事务。
TC
会生成一个
XID
作为该全局事务的编号。
XID
,会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起。 当一进入事务方法中就会生成XID
,global_table 就是存储的全局事务信息 ,
2.RM
请求
TC
将本地事务注册为全局事务的分支事务,通过全局事务的
XID
进行关联。 当运行数据库操作方法,branch_table
存储事务参与者
3.TM
请求
TC
告诉
XID
对应的全局事务是进行提交还是回滚。
4.TC
驱动
RM
们将
XID
对应的自己的本地事务进行提交还是回滚。
4. Seata快速开始
https://seata.io/zh-cn/docs/ops/deploy-guide-beginner.html 官网文档
Seata Server(TC)环境搭建
下载安装包
https://github.com/seata/seata/releases
下载
1.3.0
版本
Server
端存储模式(
store.mode
)支持三种:
- file:(默认)单机模式,全局事务会话信息内存中读写并持久化本地文件root.data,性能较高 (默认)
- db:(5.7+)高可用模式,全局事务会话信息通过db共享,相应性能差些
- redis: Seata-Server 1.3及以上版本支持,性能较高,存在事务信息丢失风险,请提前配置适合当前场景的redis持久化配置
使用db模式进行seata-server存储
打开
config/file.conf
修改
mode="db"
修改数据库连接信息(
URL\USERNAME\PASSWORD)
创建数据库
seata_server
新建表:
可以去
seata
提供的资源信息中下载
https://github.com/seata/seata/tree/master/script/server/db
Nacos(注册&配置中心)配置
将
Seata Server
注册到
Nacos
,并配置到配置中心
打开
conf/registry.conf
,修改
conf
目录下的
registry.conf
配置:
下载整个 script 文件夹,复制到 bin 同级目录下
修改 /seata/script/config-center/config.txt 文件
打开D:\seata-server-1.3.0\seata\script\config-center\nacos 目前,修改nacos-config.sh
注意:mysql5.7url可以添加useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true
mysql8.0url都要添加
双击启动nacos-config.sh文件(必须安装git)
目的:将seata配置信息加载到Nacos配置中心(太多了,不太好创建)
启动seata-server
在
bin
目录下,双击
seata-server.bat
,默认端口
8091
搭建分布式事务微服务场景
Seata Client
创建两个数据库
seata_stock
与
seata_order
分别在
seata_stock
下创建
stock_tab
与
seata_order
下
order_tab
# 库存表
CREATE TABLE `stock_tab` (
`id` int(11) NOT NULL AUTO_INCREMENT, #库存ID
`product_id` int(11) NULL DEFAULT NULL, #商品ID
`count` int(11) NULL DEFAULT 0, #商品数量
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE =
utf8_general_ci ROW_FORMAT = Compact;
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,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
# 订单表
CREATE TABLE `order_tab` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`product_id` int(11) NULL DEFAULT 0 COMMENT '商品id',
`total_amount` int(11) NULL DEFAULT 0 COMMENT '总金额',
`status` int(255) NULL DEFAULT NULL COMMENT '0->待付款;1->待发货',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE =
utf8_general_ci ROW_FORMAT = Compact;
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,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
业务场景:
用户下单,整个业务逻辑由三个微服务构成:
订单服务:根据采购需求创建订单。
库存服务:对给定的商品扣除库存数量
运行代码发现:使用本地事务,订单插入失败,库存却成功更新。。。。。。
AT模式 seata
在客户端项目pom中引入seata
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
配置客户端(
order
与
stock)application.yml
tx-service-group: 事务组名称 需要与设置的 config.text 中service.vgroupMapping.my_test_tx_group=default 保持一致
在
service
中添加
@GlobalTransactional
注解(
TM
方)
运行测试。。。。。。
如果报错:请在添加订单与修改库存的业务中打印
xid
查询是否保持一致