写在前面
本地事务很好保证要么所有操作都成功要么都失败,但是随着业务越来越复杂,单机版已经满足不了我们的需求,就需要项目从单体应用演变成分布式应用,然而随之也带来了一个问题,那就是如何保证多个微服务对DB的操作要么一起成功要么一起失败的问题,也就是分布式事务的问题。
网上有一大堆分布式事务的解决方案的理论,转化落地的有Seata这么的框架。
官网首页对它的一句话描述是,Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
Seata提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。其中,AT 模式是一种无侵入的分布式事务解决方案,比较好上手,实现起来简单,应用的比较广泛,本文将介绍AT模式的实现。
AT模式前提
- 基于支持本地 ACID 事务的关系型数据库。
- Java 应用,通过 JDBC 访问数据库。
三大模块
Seata 中有三⼤模块,分别是 TM、RM 和 TC。其中 TM 和 RM 是作为 Seata 的客户端与业务系统集成在⼀起,TC 作为 Seata 的服务端独⽴部署。
TC (Transaction Coordinator) - 事务协调者
维护全局和分⽀事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器
管理分⽀事务处理的资源,与TC交谈以注册分⽀事务和报告分⽀事务的状态,并驱动分⽀事务提交或回滚。
在 Seata 中,分布式事务的执⾏流程:
- TM 开启分布式事务,TM会 向 TC 注册全局事务记录;
- 操作具体业务模块的数据库操作之前,RM 会向 TC 注册分⽀事务;
- 当业务操作完事后,TM会通知 TC 提交/回滚分布式事务;
- TC 汇总事务信息,决定分布式事务是提交还是回滚;
- TC 通知所有 RM 提交/回滚 资源,事务⼆阶段结束。
整体机制
两阶段提交协议的演变:
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:
- 提交异步化,非常快速地完成。
- 回滚通过一阶段的回滚日志进行反向补偿。
Seata Server(TC)安装
本文使用的是v1.4.0
-
配置registry.conf
Seata Server要向注册中⼼进⾏注册,这样,其他服务就可以通过注册中⼼去发现 Seata Server,与Seata Server进⾏通信。Seata ⽀持多款注册中⼼服务:nacos 、eureka、redis、zk、consul、etcd3、sofa。我们项⽬中要使⽤ nacos注册中⼼,nacos服务的连接地址、注册的服务名,这需要在D:seataconf egistry.conf⽂件中进⾏配置:
registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "nacos" loadBalance = "RandomLoadBalance" loadBalanceVirtualNodes = 10 nacos { application = "seata-server" serverAddr = "127.0.0.1:8848" group = "SEATA_GROUP" namespace = "b8107264-b340-4939-8d60-df77d18057dc" cluster = "default" username = "nacos" password = "nacos" } } config { # file、nacos 、apollo、zk、consul、etcd3 type = "nacos" nacos { serverAddr = "127.0.0.1:8848" namespace = "b8107264-b340-4939-8d60-df77d18057dc" group = "SEATA_GROUP" username = "nacos" password = "nacos" } }
-
向nacos中添加配置信息
-
下载配置config.txt
Seata参数配置中针对每一个配置项有介绍。
-
将config.txt⽂件放⼊seata⽬录下⾯
-
修改config.txt信息
Server端存储的模式(store.mode)现有file,db,redis三种。主要存储全局事务会话信息,分⽀事务信息, 锁记录表信息,Seata server默认是file模式。file只能⽀持单机模式, 如果想要⾼可⽤模式的话可以切换db或者redis,本文采⽤db数据库模式。
注意这里我改了事务分组,默认是my_test_tx_group,我改成了my_tx_group。
service.vgroupMapping.my_tx_group=default
store.mode=db store.db.datasource=druid store.db.dbType=mysql store.db.driverClassName=com.mysql.jdbc.Driver store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true store.db.user=root store.db.password=123456 store.db.minConn=5 store.db.maxConn=30 store.db.globalTable=global_table store.db.branchTable=branch_table store.db.queryLimit=100 store.db.lockTable=lock_table store.db.maxWait=5000
-
新建seata数据库,并创建global_table,branch_table,lock_table三张表,点击这里下载。
-- -------------------------------- 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(96), `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;
-
使⽤nacos-config.sh ⽤于向 Nacos 中添加配置
#!/usr/bin/env bash # Copyright 1999-2019 Seata.io Group. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at、 # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. while getopts ":h:p:g:t:u:w:" opt do case $opt in h) host=$OPTARG ;; p) port=$OPTARG ;; g) group=$OPTARG ;; t) tenant=$OPTARG ;; u) username=$OPTARG ;; w) password=$OPTARG ;; ?) echo " USAGE OPTION: $0 [-h host] [-p port] [-g group] [-t tenant] [-u username] [-w password] " exit 1 ;; esac done if [[ -z ${host} ]]; then host=localhost fi if [[ -z ${port} ]]; then port=8848 fi if [[ -z ${group} ]]; then group="SEATA_GROUP" fi if [[ -z ${tenant} ]]; then tenant="" fi if [[ -z ${username} ]]; then username="" fi if [[ -z ${password} ]]; then password="" fi nacosAddr=$host:$port contentType="content-type:application/json;charset=UTF-8" echo "set nacosAddr=$nacosAddr" echo "set group=$group" failCount=0 tempLog=$(mktemp -u) function addConfig() { curl -X POST -H "${contentType}" "http://$nacosAddr/nacos/v1/cs/configs?dataId=$1&group=$group&content=$2&tenant=$tenant&username=$username&password=$password" >"${tempLog}" 2>/dev/null if [[ -z $(cat "${tempLog}") ]]; then echo " Please check the cluster status. " exit 1 fi if [[ $(cat "${tempLog}") =~ "true" ]]; then echo "Set $1=$2 successfully " else echo "Set $1=$2 failure " (( failCount++ )) fi } count=0 for line in $(cat $(dirname "$PWD")/config.txt | sed s/[[:space:]]//g); do (( count++ )) key=${line%%=*} value=${line#*=} addConfig "${key}" "${value}" done echo "=========================================================================" echo " Complete initialization parameters, total-count:$count , failure-count:$failCount " echo "=========================================================================" if [[ ${failCount} -eq 0 ]]; then echo " Init nacos config finished, please start seata-server. " else echo " init nacos config fail. " fi
-
将nacos-config.sh放在D:seataconf⽂件夹中
-
打开git bash here 执⾏nacos-config.sh,需要提前将nacos启动
sh nacos-config.sh -h 127.0.0.1 -p 8848 -g SEATA_GROUP -t b8107264-b340-4939-8d60-df77d18057dc
看下nacos中配置列表
-
-
启动Seata Server
查看控制台中打印信息,启动成功!
观察nacos服务列表
TM/RM整合Seata
-
数据库环境准备
新建工程study-seata,分为业务聚合(TM)study-seata-business,订单服务(RM)study-seata-order,积分服务(RM)study-seata-point,库存服务(RM)study-seata-storage。
新建seata_order数据库,新建表t_order:
CREATE TABLE `t_order` ( `id` bigint(20) NOT NULL COMMENT '订单id', `goods_Id` int(11) NULL DEFAULT NULL COMMENT '商品ID', `num` int(11) NULL DEFAULT NULL COMMENT '商品数量', `money` decimal(10, 0) NULL DEFAULT NULL COMMENT '商品总金额', `create_time` datetime(0) NULL DEFAULT NULL COMMENT '订单创建时间', `username` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '用户名称', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8;
新建seata_point数据库,新建表t_point:
CREATE TABLE `t_point` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '积分ID', `username` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '用户名', `point` int(11) NULL DEFAULT NULL COMMENT '用户积分', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8;
新建seata_storage数据库,新建表t_storage:
CREATE TABLE `t_storage` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '库存ID', `goods_id` int(11) NULL DEFAULT NULL COMMENT '商品ID', `storage` int(11) NULL DEFAULT NULL COMMENT '库存量', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8; INSERT INTO `t_storage` VALUES (1, 1, 100);
AT 模式在RM端需要 UNDO_LOG 表,来记录每个RM的事务信息,主要包含数据修改前后的相关信息,⽤于回滚处理,所以在所有数据库中分别执⾏:
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;
最终数据库环境搭建好了,如下:
-
项目依赖配置
版本选择:
工具/服务组件 版本 JDK 1.8 SpringBoot 2.3.2.RELEASE SpringCloud Hoxton.SR8 Spring-Cloud-alibaba 2.2.5.RELEASE Seata 1.4.0 Mysql 5.7.28 父pom.xml中添加Seata依赖管理,⽤于Seata的版本锁定。
<dependencyManagement> <dependencies> <!--Spring Cloud--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR8</version> <type>pom</type> <scope>import</scope> </dependency> <!--Spring Cloud Alibaba--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.2.5.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <!--mysql --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <!-- seata-spring-boot-starter --> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.4.0</version> </dependency> </dependencies> </dependencyManagement>
在TM/RM端pom.xml中引入Seata依赖:
<!--seata依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <!--排除低版本seata依赖--> <exclusions> <exclusion> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> </exclusion> </exclusions> </dependency> <!--添加⾼版本seata依赖--> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.4.0</version> </dependency>
关于依赖引入请参考版本说明
-
微服务配置
study-seata-business的application.yml配置如下:
server: port: 8080 spring: application: name: study-seata-business # nacos配置 cloud: nacos: # 服务发现 discovery: server-addr: 127.0.0.1:8848 namespace: b8107264-b340-4939-8d60-df77d18057dc username: nacos password: nacos # seata配置 seata: tx-service-group: my_tx_group registry: type: Nacos nacos: application: seata-server server-addr: 127.0.0.1:8848 group: SEATA_GROUP namespace: b8107264-b340-4939-8d60-df77d18057dc username: nacos password: nacos config: type: Nacos nacos: server-addr: 127.0.0.1:8848 group: SEATA_GROUP namespace: b8107264-b340-4939-8d60-df77d18057dc username: nacos password: nacos # ribbon配置 ribbon: ReadTimeout: 500000 ConnectTimeout: 50000
study-seata-service-order的application.yml配置如下:
server: port: 8081 spring: application: name: study-seata-service-order # 数据源配置 datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/seata_order?useSSL=false username: root password: 123456 # nacos配置 cloud: nacos: # 服务发现 discovery: server-addr: 127.0.0.1:8848 namespace: b8107264-b340-4939-8d60-df77d18057dc username: nacos password: nacos # seata配置 seata: tx-service-group: my_tx_group registry: type: Nacos nacos: application: seata-server server-addr: 127.0.0.1:8848 group: SEATA_GROUP namespace: b8107264-b340-4939-8d60-df77d18057dc username: nacos password: nacos config: type: Nacos nacos: server-addr: 127.0.0.1:8848 group: SEATA_GROUP namespace: b8107264-b340-4939-8d60-df77d18057dc username: nacos password: nacos
study-seata-service-point的application.yml配置如下:
server: port: 8082 spring: application: name: study-seata-service-point # 数据源配置 datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/seata_point?useSSL=false username: root password: 123456 # nacos配置 cloud: nacos: # 服务发现 discovery: server-addr: 127.0.0.1:8848 namespace: b8107264-b340-4939-8d60-df77d18057dc username: nacos password: nacos # seata配置 seata: tx-service-group: my_tx_group registry: type: Nacos nacos: application: seata-server server-addr: 127.0.0.1:8848 group: SEATA_GROUP namespace: b8107264-b340-4939-8d60-df77d18057dc username: nacos password: nacos config: type: Nacos nacos: server-addr: 127.0.0.1:8848 group: SEATA_GROUP namespace: b8107264-b340-4939-8d60-df77d18057dc username: nacos password: nacos
study-seata-service-storage的application.yml配置如下:
server: port: 8083 spring: application: name: study-seata-service-storage # 数据源配置 datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/seata_storage?useSSL=false username: root password: 123456 # nacos配置 cloud: nacos: # 服务发现 discovery: server-addr: 127.0.0.1:8848 namespace: b8107264-b340-4939-8d60-df77d18057dc username: nacos password: nacos # seata配置 seata: tx-service-group: my_tx_group registry: type: Nacos nacos: application: seata-server server-addr: 127.0.0.1:8848 group: SEATA_GROUP namespace: b8107264-b340-4939-8d60-df77d18057dc username: nacos password: nacos config: type: Nacos nacos: server-addr: 127.0.0.1:8848 group: SEATA_GROUP namespace: b8107264-b340-4939-8d60-df77d18057dc username: nacos password: nacos
-
添加注解@GlobalTransactional
AT模式实现分布式事务十分简单,在发起全局事务的TM端方法上增加@GlobalTransactional就可以了。
先看看不加此注解时各服务的操作DB的情况:
此时,并未加注解@GlobalTransactional,在碰到异常时,订单表插入了一行,积分增加了,但是库存并没有减少:
现在,加上注解@GlobalTransactional,再次观察一下这三个表中的变化,最好打一下断点,你会发现,程序在执行完异常代码处,会回滚掉插入的订单和被修改的积分值,说明保证了分布式事务中各分支事务要么都成功提交要么发生异常都回滚。
总结
AT 模式是⼀种⽆侵⼊的分布式事务解决⽅案。在 AT 模式下,⽤户只需关注⾃⼰的“业务 SQL”,⽤户的 “业务 SQL” 作为⼀阶段,Seata 框架会⾃动⽣成事务的⼆阶段提交和回滚操作。AT模式基于数据源代理实现,对ORM框架十分友好,集成难度低,学习成本低,十分推荐使用。
最后,附上源码