1. 分布式事务问题
**分布式前**
:单机单库没这个问题,从1:1 -> 1:N -> N:N**分布式之后**
:单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。**一句话**
:一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题
2. Seata简介
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。支持模式:AT(默认,实际类似TCC)、TCC、Sega、XA 官网:http://seata.io/zh-cn/ 下载地址:https://github.com/seata/seata/releases
3. Seata处理分布式事务过程
详细参考:7. 分布式事务的执行流程
- TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;
- XID 在微服务调用链路的上下文中传播;
- RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;
- TM 向 TC 发起针对 XID 的全局提交或回滚决议;
- TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
3.1 一个ID+三个组件
3.1.1 Transaction ID XID
全局唯一的事务ID
3.1.2 Transaction Coordinator (TC)
Seata服务器;事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚;
3.1.3 Transaction Manager (TM)
@GlobalTransactional注解标注的方法;控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议;
3.1.4 Resource Manager (RM)
每一个数据库;控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚
4. @GlobalTransactional
5. 安装
- 下载的是seata-server-0.9.0.zip
- seata-server-0.9.0.zip解压到指定目录并修改conf目录下的file.conf配置文件
- 先备份原始file.conf文件
- 主要修改:自定义事务组名称+事务日志存储模式为db+数据库连接信息,见下方配置↓
- service模块
- store模块
- mysql5.7数据库新建库seata
- 在seata库里建表,建表db_store.sql在\seata-server-0.9.0\seata\conf目录里面
db_store.sql
- 修改
seata-server-0.9.0\seata\conf\registry.conf
配置文件- 先启动Nacos端口号8848,
softs\nacos-server-1.1.4\nacos\bin\startup.cmd
- 再启动seata-server,
softs\seata-server-0.9.0\seata\bin\seata-server.bat
- file.conf修改内容如下:
service {
######################修改begin######################
# #修改自定义事务组名称
vgroup_mapping.my_test_tx_group = "fsp_tx_group"
######################修改end########################
default.grouplist = "127.0.0.1:8091"
enableDegrade = false
disable = false
max.commit.retry.timeout = "-1"
max.rollback.retry.timeout = "-1"
}
## transaction log store
store {
## store mode: file、db,可以存成文件,也可以存在db
######################修改begin######################
mode = "db"
######################修改end########################
## file store
file {
dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
max-branch-session-size = 16384
# globe session size , if exceeded throws exceptions
max-global-session-size = 512
# file buffer size , if exceeded allocate new buffer
file-write-buffer-cache-size = 16384
# when recover batch read size
session.reload.read_size = 100
# async, sync
flush-disk-mode = async
}
## database store
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "dbcp"
## mysql/oracle/h2/oceanbase etc.
db-type = "mysql"
driver-class-name = "com.mysql.jdbc.Driver"
######################修改begin######################
url = "jdbc:mysql://*阿里云*:3306/seata"
user = "root"
password = "你自己密码"
######################修改end######################
min-conn = 1
max-conn = 3
global.table = "global_table"
branch.table = "branch_table"
lock-table = "lock_table"
query-limit = 100
}
}
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
serverAddr = "localhost:8848"
namespace = ""
cluster = "default"
}
# 目的是:指明注册中心为nacos,及修改nacos连接信息
6. 案例
以下演示都需要先启动Nacos后启动Seata,保证两个都OK,Seata没启动报错no available server to connect
6.1 业务说明
这里会创建三个服务,一个订单服务,一个库存服务,一个账户服务。
当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存, 再通过远程调用账户服务来扣减用户账户里面的余额, 最后在订单服务中修改订单状态为已完成。
该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。
下订单—->扣库存—->减账户(余额)
6.2 创建数据库
6.2.1 seata_order库下建t_order表
存储订单的数据库
CREATE TABLE t_order (
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
`count` INT(11) DEFAULT NULL COMMENT '数量',
`money` DECIMAL(11,0) DEFAULT NULL COMMENT '金额',
`status` INT(1) DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完结'
) ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
SELECT * FROM t_order;
6.2.2 seata_storage库下建t_storage 表
CREATE TABLE t_storage (
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
`total` INT(11) DEFAULT NULL COMMENT '总库存',
`used` INT(11) DEFAULT NULL COMMENT '已用库存',
`residue` INT(11) DEFAULT NULL COMMENT '剩余库存'
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO seata_storage.t_storage(`id`, `product_id`, `total`, `used`, `residue`)
VALUES ('1', '1', '100', '0', '100');
SELECT * FROM t_storage;
6.2.3 seata_account库下建t_account 表
CREATE TABLE t_account (
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
`total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
`used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用余额',
`residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度'
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO seata_account.t_account(`id`, `user_id`, `total`, `used`, `residue`) VALUES ('1', '1', '1000', '0', '1000');
SELECT * FROM t_account;
6.2.4 按照上述3库分别建对应的回滚日志表
订单-库存-账户3个库下都需要建各自的回滚日志表,
\seata-server-0.9.0\seata\conf\db_undo_log.sql
6.3 依赖
<!--nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
6.4 微服务
三个微服务见码云:cloud2020项目,
seata-order-service2001
,seata-storage-service2002
,seata-account-service2003
6.5 测试
下订单->减库存->扣余额->改(订单)状态
6.5.1 数据库初始情况
SELECT * FROM
seata_order.
t_order<br /> ![image.png](https://cdn.nlark.com/yuque/0/2021/png/668367/1617721008615-8ba1301c-2aa7-4b70-91fa-805f18a7bf34.png#crop=0&crop=0&crop=1&crop=1&height=75&id=tgCp0&margin=%5Bobject%20Object%5D&name=image.png&originHeight=100&originWidth=627&originalType=binary&ratio=1&rotation=0&showTitle=false&size=12399&status=done&style=none&title=&width=470)<br />`SELECT * FROM `seata_storage`.`t_storage
SELECT * FROM
seata_account.
t_account;
6.5.2 超时异常,没加@GlobalTransactional
- AccountServiceImpl添加超时代码
- 数据库情况
故障情况
OrderServiceImpl.create()添加@GlobalTransactional
@GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class) public void create(Order order) { 。。。。。。 }
-
7. 分布式事务的执行流程
TM 开启分布式事务(TM 向 TC 注册全局事务记录);
- 按业务场景,编排数据库、服务等事务内资源(RM 向 TC 汇报资源准备状态 );
- TM 结束分布式事务,事务一阶段结束(TM 通知 TC 提交/回滚分布式事务);
- TC 汇总事务信息,决定分布式事务是提交还是回滚;
- TC 通知所有 RM 提交/回滚 资源,事务二阶段结束。
7.1 一阶段加载
- 在一阶段,Seata 会拦截“业务 SQL”
- 解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”
- 执行“业务 SQL”更新业务数据,在业务数据更新之后
- 其保存成“after image”,最后生成行锁
- 以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性
- 如下图,在seata库中的branch_table表生成了所有RM相关的记录
- 如下图,在业务库的undo_log表生成了日志,其中最重要的就是rollback_info,里面存储的就是“before image”和“after image”,里面实际存储的就是业务表中所有字段在执行业务sql发生前的值和发生后的值
7.2 二阶段提交
二阶段如是顺利提交的话
:
- 因为“业务 SQL”在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
7.3 二阶段回滚
二阶段回滚
:
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。
回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”
如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写(before和after中间有其他操作修改了数据),出现脏写就需要转人工处理。