Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。
Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
官网地址:Seata。
Seata术语:
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
AT 模式
1.前提
基于支持本地 ACID 事务的关系型数据库。
Java 应用,通过 JDBC 访问数据库。
2.整体机制
两阶段提交协议的演变:
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
二阶段:提交异步化,非常快速地完成。回滚通过一阶段的回滚日志进行反向补偿。
3.流程:
3.1 全局事务开启:TM向TC发起全局事务,TC生成全局事务id(xid),全局事务id可以在调用链中进行传播。
3.2 一阶段提交:
过程:
对于如下sql语句:
update product set name = 'GTS' where name = 'TXC';
(1)解析 SQL:得到 SQL 的类型(UPDATE),表(product),条件(where name = 'TXC')等相关的信息。
(2)查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。
(3)执行业务 SQL:更新这条记录的 name 为 'GTS'。
(4)查询后镜像:根据前镜像的结果,通过 主键 定位数据。
(5)插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中。
(6)提交前,向 TC 注册分支:申请 product 表中,主键值等于 1 的记录的 全局锁 。
(7)本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。
(8)将本地事务提交的结果上报给 TC。
3.3.1 二阶段提交:
收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。
3.3.2 二阶段回滚:
收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理。
根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:
update product set name = 'TXC' where id = 1;
提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。
搭建Seata Server
下载地址:Tags · seata/seata (github.com)
配置conf/file.conf
创建seata数据库:
脚本地址:seata/script/server/db at develop · seata/seata (github.com)
创建seata数据库,并执行如下(MySQL)脚本:
-- -------------------------------- The script used when storeMode is 'db' -------------------------------- -- the table to store GlobalSession data CREATE TABLE IF NOT EXISTS `global_table` ( `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `status` TINYINT NOT NULL, `application_id` VARCHAR(32), `transaction_service_group` VARCHAR(32), `transaction_name` VARCHAR(128), `timeout` INT, `begin_time` BIGINT, `application_data` VARCHAR(2000), `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`xid`), KEY `idx_gmt_modified_status` (`gmt_modified`, `status`), KEY `idx_transaction_id` (`transaction_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; -- the table to store BranchSession data CREATE TABLE IF NOT EXISTS `branch_table` ( `branch_id` BIGINT NOT NULL, `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `resource_group_id` VARCHAR(32), `resource_id` VARCHAR(256), `branch_type` VARCHAR(8), `status` TINYINT, `client_id` VARCHAR(64), `application_data` VARCHAR(2000), `gmt_create` DATETIME(6), `gmt_modified` DATETIME(6), PRIMARY KEY (`branch_id`), KEY `idx_xid` (`xid`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; -- the table to store lock data CREATE TABLE IF NOT EXISTS `lock_table` ( `row_key` VARCHAR(128) NOT NULL, `xid` VARCHAR(128), `transaction_id` BIGINT, `branch_id` BIGINT NOT NULL, `resource_id` VARCHAR(256), `table_name` VARCHAR(32), `pk` VARCHAR(36), `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`row_key`), KEY `idx_branch_id` (`branch_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; CREATE TABLE IF NOT EXISTS `distributed_lock` ( `lock_key` CHAR(20) NOT NULL, `lock_value` VARCHAR(20) NOT NULL, `expire` BIGINT, primary key (`lock_key`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
执行完毕后,见如下四个表:
Seata使用Nacos注册中心,配置文件为conf/registry.conf:
Seata的配置,参考:Seata 参数配置
在Nacos上创建一个配置文件:
启动Seata,在bin目录运行如下命令:
cd bin
seata-server.bat -h 127.0.0.1 -p 8098
启动后,可以在Nacos控制台看到seata服务已经注册成功。
在业务数据库创建UNDO_LOG表,脚本见github:seata/mysql.sql at develop · seata/seata (github.com)
-- for AT mode you must to init this sql for you business database. the seata server not need it. CREATE TABLE IF NOT EXISTS `undo_log` ( `branch_id` BIGINT NOT NULL COMMENT 'branch transaction id', `xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id', `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization', `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info', `log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status', `log_created` DATETIME(6) NOT NULL COMMENT 'create datetime', `log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime', UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
代码示例:
1、在cloud-goods中添加pom依赖:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-seata</artifactId> <version>2.2.0.RELEASE</version> <exclusions> <exclusion> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.4.2</version> </dependency>
2、添加配置:
spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 123456 url: jdbc:mysql://localhost:3306/goods?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8 mybatis-plus: configuration: map-underscore-to-camel-case: false seata: enabled: true tx-service-group: fengmi_tx_group enable-auto-data-source-proxy: true service: vgroupMapping: my_test_tx_group: default config: type: nacos nacos: server-addr: 127.0.0.1:8848 group: DEFAULT_GROUP namespace: public username: nacos password: nacos data-id: seataServer.properties registry: #从Nacos中发现seata server服务 type: nacos nacos: application: seata-server server-addr: 127.0.0.1:8848 namespace: public group: DEFAULT_GROUP username: nacos password: nacos
在代码中可以使用@GlobalTransitional注解来实现分布式事务。
package com.yas.service; import com.yas.Goods; import com.yas.mapper.GoodsMapper; import io.seata.spring.annotation.GlobalTransactional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class GoodsService implements IGoodsService { @Autowired GoodsMapper goodsMapper; @Override @GlobalTransactional public String buy(){ Integer result = goodsMapper.insert(new Goods("苹果",3999)); System.out.println(result); int x = 1 / 0; System.out.println(x); return result+""; } }