seata分布式事务
本文档默认您已经具备如下环境:
- Nacos
- mysql
一、环境准备
1.1 资源
名称 | 地址 | |
---|---|---|
Nacos | https://github.com/alibaba/nacos/tags | 注册中心、配置中心 |
seata1.4.0 | http://seata.io/zh-cn/blog/download.html | 分布式事务 |
项目示例 | https://github.com/HLDBanana/seata_demo | seata示例项目 |
1.2 seata介绍
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
AT可以看作时由Seata社区进行全方面优化,自研的XA模式。最大特点就是解决了XA模式的性能差的问题。
1.2.1 AT模式
前提
- 基于支持本地 ACID 事务的关系型数据库。
- Java 应用,通过 JDBC 访问数据库。
整体机制
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:
提交异步化,非常快速地完成。
回滚通过一阶段的回滚日志进行反向补偿。
写隔离
- 一阶段本地事务提交前,需要确保先拿到 全局锁 。
- 拿不到 全局锁 ,不能提交本地事务。
- 拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
以一个示例来说明:
两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。
tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。 tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁 。
tx1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。
如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。
此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。
因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。
工作机制
一阶段
- 解析 SQL得到前镜像(原始数据):
- 执行业务 SQL:更新数据
- 查询后镜像(更新后的数据)
- 把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中。
- 提交前,向 TC 注册分支:该条数据的 全局锁 。
- 本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。
- 将本地事务提交的结果上报给 TC。
二阶段-回滚
收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
- 通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
- 根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:
update product set name = ‘TXC’ where id = 1; - 提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。
二阶段-提交
- 收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
- 异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。
1.2.2 TCC模式
TCC 模式,不依赖于底层数据资源的事务支持:
- 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
- 二阶段 commit 行为:调用 自定义 的 commit 逻辑。
- 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。
1.2.3 SAGA模式
Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
- 适用场景:
业务流程长、业务流程多
二、seata AT模式整合
2.1 创建业务表、undo_log回滚日志表、事务锁信息库表
新版本seata-server包内不含 undo_log、事务表建表sql,需要下载seata-server-0.9.0 版本获取响应sql文件:
seata-server-0.9.0.zip
- 创建file.conf对应的事务锁信息表
#全局事务表
CREATE TABLE `branch_table` (
`branch_id` bigint(20) NOT NULL,
`xid` varchar(128) NOT NULL,
`transaction_id` bigint(20) DEFAULT NULL,
`resource_group_id` varchar(32) DEFAULT NULL,
`resource_id` varchar(256) DEFAULT NULL,
`lock_key` varchar(128) DEFAULT NULL,
`branch_type` varchar(8) DEFAULT NULL,
`status` tinyint(4) DEFAULT NULL,
`client_id` varchar(64) DEFAULT NULL,
`application_data` varchar(2000) DEFAULT NULL,
`gmt_create` datetime DEFAULT NULL,
`gmt_modified` datetime DEFAULT NULL,
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='全局事务表';
#分支事务表
CREATE TABLE `global_table` (
`xid` varchar(128) NOT NULL,
`transaction_id` bigint(20) DEFAULT NULL,
`status` tinyint(4) NOT NULL,
`application_id` varchar(32) DEFAULT NULL,
`transaction_service_group` varchar(32) DEFAULT NULL,
`transaction_name` varchar(128) DEFAULT NULL,
`timeout` int(11) DEFAULT NULL,
`begin_time` bigint(20) DEFAULT NULL,
`application_data` varchar(2000) DEFAULT NULL,
`gmt_create` datetime DEFAULT NULL,
`gmt_modified` datetime DEFAULT NULL,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`,`status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='分支事务表';
#表数据锁信息(具体加锁的表数据id)
CREATE TABLE `lock_table` (
`row_key` varchar(128) NOT NULL,
`xid` varchar(96) DEFAULT NULL,
`transaction_id` mediumtext,
`branch_id` mediumtext,
`resource_id` varchar(256) DEFAULT NULL,
`table_name` varchar(32) DEFAULT NULL,
`pk` varchar(36) DEFAULT NULL,
`gmt_create` datetime DEFAULT NULL,
`gmt_modified` datetime DEFAULT NULL,
PRIMARY KEY (`row_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='存储锁表';
如下:
- 创建业务表单
创建seata-account、seata-order、seata-storage业务库,分别创建t_account、t_ordre、t_storage业务表单
# 账户表
CREATE TABLE seata-account.t_account (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`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 NULL COMMENT '剩余可用额度',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
#订单表
CREATE TABLE seata-order.t_order (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`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:已完结',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
#库存表
CREATE TABLE seata-storage.t_storage (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`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 '剩余库存',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
- 业务库创建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 COMMENT='AT transaction mode undo table';;
创建完成如下:
2.2 seata-server配置
下载seata-server: http://seata.io/zh-cn/blog/download.html
- 配置seata-seaver 注册中心
修改seata/conf/registry.conf 配置文件
只需修改选中配置中心类型对应配置即可,如下:
- seata-server 事务日志信息
修改seata/conf/file.conf 配置文件
只需修改选中配置中心类型对应配置即可,如下:
2.3 将配置导入到nacos
seata-server-0.9.0版本包中有上传nacos配置中心的脚本以及配置模板:seata-server-0.9.0.zip
如图:
- 修改nacos-config.txt配置
- 上传配置到nacos
上传配置到nacos seata命名空间,cmd进入nacos-config.sh所在路径执行:
#-t 命名空间id,如果需要的话请在nacos上自行创建命名空间并替换命名空间ID
#-h nacos地址
#-g nacos分组
sh nacos-config.sh -h 182.92.219.202 -p 8848 -g SEATA_GROUP -u nacos -w nacos -t 51915a62-d2d6-43d4-8f45-86b159eb90f5
如图:
将配置上传到了seata分支
三、项目配置
3.1 pom依赖
- 父工程依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
- 子工程依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
3.2 子项目集成seata配置
spring:
cloud:
nacos:
discovery:
server-addr: 182.92.219.202:8848 #nacos地址
username: nacos #nacos账号密码
password: nacos
group: SEATA_GROUP #分组
# namespace: 6c990727-93b2-4081-a8c6-6b015c56eda2 #不指定命名空间。默认public
seata:
tx-service-group: my_test_tx_group #这里要特别注意和nacos中配置的要保持一直
registry:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
username: ${spring.cloud.nacos.discovery.username}
password: ${spring.cloud.nacos.discovery.password}
group: ${spring.cloud.nacos.discovery.group}
# namespace: 6c990727-93b2-4081-a8c6-6b015c56eda2
config:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
username: ${spring.cloud.nacos.discovery.username}
password: ${spring.cloud.nacos.discovery.password}
group: ${spring.cloud.nacos.discovery.group}
namespace: 51915a62-d2d6-43d4-8f45-86b159eb90f5 #2.2中配置所在命名空间ID,入未配置 默认public空间
service:
vgroup-mapping:
my_test_tx_group: default # 这里要特别注意和nacos中配置的要保持一直
四、编写业务代码
示例项目:https://github.com/HLDBanana/seata_demo
业务逻辑图如下:
最终项目结构如下:
- 仓储服务
@PostMapping(value = "/decrease")
public CommonResult decrease(@RequestParam("productId") @Valid @NotNull Long productId,
@RequestParam("count") Integer count) {
log.info("开始调用扣减库存");
int suc = storageService.decrease(productId,count);
log.info("扣减成功:" + suc);
return new CommonResult(200, "扣减库存成功");
}
- 账户服务
@PostMapping("/decrease")
public CommonResult decrease(@RequestParam("userId") Long userId,
@RequestParam("money") BigDecimal money) {
log.info("开始调用扣减账户余额");
int res = accountService.decrease(userId, money);
log.info("扣减金额成功:" + res);
return new CommonResult(res, "扣减账户余额成功");
}
- 订单服务
订单服务
public interface OrderService {
/**
* 创建订单
*/
void create(Order order);
}
- 主要调用逻辑
在需要开启分布式事务的方法上添加@GlobalTransactional注解,开启分布式事务。
注意:被调用服务不需要做任何调整,只需要在调用方开启分布式事务就可以了
@Override
@GlobalTransactional(name = "seata-create-order", rollbackFor = Exception.class)
public void create(Order order) {
log.info("--------->开始新建订单");
//1 新建订单
orderDao.create(order);
//2 扣减库存
log.info("------------->订单微服务开始调用库存,做扣减Count");
CommonResult st = storageService.decrease(order.getProductId(), order.getCount());
if (st.getCode() != 200){
throw new RuntimeException("扣减库存失败");
}
log.info("------------->订单微服务开始调用库存,做扣减end");
//3 扣减账户
log.info("------------->订单微服务开始调用账户,做扣减Money");
CommonResult ac = accountService.decrease(order.getUserId(), order.getMoney());
if (ac.getCode() != 200){
throw new RuntimeException("扣减账户失败");
}
log.info("------------->订单微服务开始调用账户,做扣减end");
//4 修改订单状态
log.info("------------->修改订单状态开始");
orderDao.update(order.getUserId(),0);
log.info("------------->修改订单状态结束");
log.info("------------->下订单结束了");
}
五、验证
5.1 二阶段-提交验证
调用接口:http://localhost:2001/order/create 创建订单扣减库存和账户金额
结果如下:
5.2 二阶段-回滚
- 修改扣减账户余额实现方法,模拟超时
@Override
public int decrease(Long userId, BigDecimal money) {
try {
// 模拟超时异常
TimeUnit.SECONDS.sleep(30000);
} catch (Exception e) {
e.printStackTrace();
}
//int a = 1/0;
return accountDao.decrease(userId, money);
}
- 调用 http://localhost:2001/order/create 接口创建订单扣减库存和账户金额
- 查看数据库订单、库存本地事务是否已提交
订单本地事务已提交
库存扣减成功
- 查看全局锁、分支锁、表数据锁
查看seata库表锁状态信息
-
查看order库、storage库 undo_log回滚日志
-
放开断点,让代码逻辑执行完成,查看回滚情况。
代码执行结果:账户余额扣减超时,事务失败。
回滚完成之后 undo_log,锁信息全部清除
至此,验证完成。