目录
一、Seata介绍
Seata基本概念
更多原理和过程分析
二、无分布式事务控制的情况
三、Seata概念
事务分组概念释疑
五、Seata服务端安装配置
六、Seata客户端(AT模式)
七、Seata服务端调整配置
附录A:Seata需要的SQL等官方脚本
seata全称是:simple extensiable autonomous transaction architecture,中文直译就是:简单的、可扩展的、自治的事务架构。
Seata作为跨数据库的分布式事务管理和解决办法,提供了AT、TCC、Saga、XA四种事务模式解决方案。 其中AT模式对业务代码几乎零修改零侵入。相关介绍请见官网。
Seata分为服务端(Seata Server)和客户端(即各个微服务)。服务端即TC全局协调中心,客户端即TM和RM。
在 Seata 的架构中,一共有三个角色:
其中,TC 为单独部署的Seata-Server 服务端,TM 和 RM 为嵌入到各个微服务应用中的 Client 客户端。
在 Seata 中,分布式事务的生命周期示意图如下(绿色箭头为服务间调用链路,传递过程中自动携带全局事务xid的传递):
注:全局事务的整体提交还是回滚,是由全局事务的发起方来决定的。 发起方决定后,告诉TC,并由TC通知各服务进行branch事务在各服务本地的commit/rollback操作。
可参见文章:[图文] Seata AT 模式分布式事务源码分析_旧时光 | YoungChen's 博客-CSDN博客
在之前的Java工程(4.SpringCloud:Sentinel服务流控与服务降级_zyplanke的专栏-CSDN博客)已经有支付服务、订单服务。在其工程基础上,对其进行修改(主要是增加对数据库的访问)
1、对pom.xml增加spring-boot-starter-data-jpa、mysql-connector-java、druid-spring-boot-starter三个依赖。内容如下:
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.7.10
payment
order
com.example
myproject-global-pom
1.0
pom
This is my project global pom config
1.8
${java.version}
${java.version}
3.1.6
2021.0.4.0
UTF-8
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-web
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
${springcloudalibaba.version}
com.alibaba.nacos
nacos-client
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
${springcloudalibaba.version}
com.alibaba.nacos
nacos-client
org.springframework.cloud
spring-cloud-starter-bootstrap
${springcloud.version}
com.alibaba.nacos
nacos-client
2.1.1
org.springframework.cloud
spring-cloud-starter-bootstrap
${springcloud.version}
org.springframework.cloud
spring-cloud-starter-openfeign
${springcloud.version}
org.springframework.cloud
spring-cloud-starter-loadbalancer
${springcloud.version}
com.alibaba.cloud
spring-cloud-starter-alibaba-sentinel
${springcloudalibaba.version}
true
com.alibaba.csp
sentinel-datasource-nacos
1.8.0
org.springframework.boot
spring-boot-starter-data-jpa
mysql
mysql-connector-java
8.0.22
com.alibaba
druid-spring-boot-starter
1.2.8
org.springframework.boot
spring-boot-maven-plugin
2、支付服务:定义支付服务数据库表InnoDB engine(并插入一条数据)
mysql> create schema payment;
mysql> create table payment.T_PAY(order_id int, amount int, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY `PK_T_PAY` (order_id) ) ENGINE = InnoDB;
Query OK, 0 rows affected (0.02 sec)
mysql> desc payment.T_PAY;
+-------------+----------+------+-----+-------------------+-------------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+----------+------+-----+-------------------+-------------------+
| order_id | int | YES | | NULL | |
| amount | int | YES | | NULL | |
| create_time | datetime | YES | | CURRENT_TIMESTAMP | DEFAULT_GENERATED |
+-------------+----------+------+-----+-------------------+-------------------+
3 rows in set (0.00 sec)
3、支付服务:修改支付服务的PaymentController.java,内容如下 (增加数据库SQL,URL参数改为userid )
@RestController
public class PaymentController {
@Value("${server.port}")
private int myport;
@Autowired
private JdbcTemplate jdbcTemplate;
Logger logger = LoggerFactory.getLogger(PaymentController.class);
@GetMapping("/dopay/{orderid}")
public ResponseEntity paylogic(@PathVariable("orderid") Long orderid) {
int insert_rows = jdbcTemplate.update("insert into T_PAY (order_id, amount) values (" + orderid +", 999)");
logger.info("支付服务successful! orderid=" + orderid + ", 支付成功。 支付服务的端口为port=" + myport);
return ResponseEntity.ok("支付服务successful! orderid=" + orderid + ", 支付成功。 支付服务的端口为port=" + myport);
}
}
4、支付服务:由于该服务使用Nacos作为配置中心,因此在Nacos中 修改Data ID=paymentService.properties的配置,增加数据库连接信息配置如下:
5、订单服务:定义订单服务数据库表(InnoDB engine)
mysql> create schema orders;
mysql> create table orders.T_Order (order_id int, address varchar(128), remark varchar(256), create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY `PK_T_Order` (order_id) ) ENGINE = InnoDB ;
Query OK, 0 rows affected (0.02 sec)
mysql> desc orders.T_Order;
+-------------+--------------+------+-----+-------------------+-------------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+--------------+------+-----+-------------------+-------------------+
| order_id | int | YES | | NULL | |
| address | varchar(128) | YES | | NULL | |
| remark | varchar(256) | YES | | NULL | |
| create_time | datetime | YES | | CURRENT_TIMESTAMP | DEFAULT_GENERATED |
+-------------+--------------+------+-----+-------------------+-------------------+
4 rows in set (0.00 sec)
6、订单服务:修改订单服务的OrderController.java。先调用支付服务,然后模拟异常。内容如下
@RestController
public class OrderController {
@Autowired
private IPaymentServiceClient paymentServiceClient;
@Autowired
private JdbcTemplate jdbcTemplate;
Logger logger = LoggerFactory.getLogger(OrderController.class);
boolean forcefail = true;
@GetMapping("/consumer/{orderid}")
public ResponseEntity consumerFeign(@PathVariable("orderid") Long orderid) {
String result = paymentServiceClient.dodopay(orderid).getBody(); // 调用支付服务进行支付
logger.info("[paymentService result]: {}", result);
jdbcTemplate.update("insert T_Order(order_id, address, remark) values (" + orderid + ", 'dizhi', '"+result+"')");
//模拟在支付完成后,本订单事务后续处理过程中发生异常后,全局回滚功能
if (forcefail) {
throw new RuntimeException("模拟在支付完成后,本订单事务后续处理过程中发生异常!");
}
return ResponseEntity.ok ("调用订单服务完成。 订单orderid:" + orderid + ", 调用支付返回的Body:" + result);
}
}
6、订单服务:由于该服务使用Nacos作为配置中心,因此在Nacos中 修改Data ID=orderService.properties的配置,增加数据库连接信息配置如下
7、测试。curl http://localhost:8888/consumer/66
8、结论:先支付后订单入库的流程中,在支付完成后,本订单事务后续处理过程中发生异常,本应该整体回归。然而实际上支付表中的记录仍然存在,并未整体回滚,未能实现跨库事务一致性。
因此需要借用Seata等分布式事务组件来解决此类问题。
请先阅读官网文档(必读知识包括):
其中事务分组及相关的几个概念参数,网上大多文章有误导。正确的理解为:
tx-service-group事务分组 | 事务分组是seata的资源逻辑概念。 即可以按微服务的需要对事务进行逻辑上分组,每组取一个名字 |
在客户端(微服务)配置。 在Spring的配置参数形式如下: seata.tx-service-group=mytest-tx-group 或者用spring.cloud.alibaba.seata.tx-service-group配置 如以上不指定,则逻辑事务分组名默认规则为:spring.application.name值+"-seata-service-group" |
vgroup-mapping | 即seata-server服务端节点组成的集群cluster名。 需要在客户端(微服务)中,对每个逻辑事务分组指定映射使用的vgroup-mapping(即服务端的cluster)。 通过此映射关系调整,即可切换到不同服务端cluster。 |
在客户端(微服务)配置。 指定逻辑事务分组映射到vgroup-mapping(vgroup-mapping的值需要与seata-server中registry.conf中的cluster保持一致)。 在Spring的配置参数形式如下: seata.service.vgroup-mapping.mytest-tx-group=beijing 其中:红色为事务分组名 等号右侧值无需引号;且与seata-server的cluster名相同 |
grouplist | 与上面不是同级别概念。 它的用途是:直接在客户端对vgroup-mapping(即服务端cluster)配置各个seata-server服务端节点IP和端口信息。 |
在客户端(微服务)配置。 推荐使用seata.registry.type=nacos或其他类型(微服务客户端和seata-server服务端应同时指定为nacos),因为使用Nacos,客户端通过Nacos即可自动获得seata-server服务端节点的IP和端口信息。 如果不是nacos等注册中心,则需要在客户端(微服务)配置中,通过以下两个参数直接指定seata-server服务端节点信息: 在Spring的配置参数形式如下(官方不推荐): seata.registry.type=file seata.service.grouplist.beijing=seata-serverIP:Port 其中:紫色为vgroup-mapping名(即seata-server的cluster名) |
网上文章常见的错误为:
四、环境介绍
1、下载并解压seata-server
wget https://github.com/seata/seata/releases/download/v1.4.2/seata-server-1.4.2.zip
unzip seata-server-1.4.2.zip
解压后,目录结果为:
├──bin ├──conf ├──log └──lib
2、配置seata-server。Seata服务端的conf目录可以看到配置文件:
这里为简单起见,本文将:
- 在registry.conf文件中将registry.type=nacos(并将nacos中serverAddr指向Nacos服务端,cluster改为beijing);config.type=file。
- 在file.conf文件中将store.mode=file。
3、启动seata-server服务端
./seata-server.sh -h 39.100.80.168 &
还可以加启动选项:
--help 显示帮助
-h: 本Seata服务向Nacos等注册时,以哪个IP信息作为Seata服务对外暴露IP(本选项不是指定本地侦听IP)。
对于服务器是阿里云ESC等有NAT转换特别有用
-p: The port to listen. Default: 8091
-m: 服务端undo log store mode : file、db. Default: file
-n: Server node,多个Server时,需区分各自节点,用于生成不同区间的transactionId,以免冲突
-e: 多环境配置参考 http://seata.io/en-us/docs/ops/multi-configuration-isolation.html
4、查看注册结果。由于在registry.conf文件中将registry.type=nacos。所以在Nacos中可以看到注册的服务。
点看详情,注意查看集群信息(下图中集群名源于registry.conf下的cluster设置):
1、应要使用Seata,在前例工程Maven中增加依赖。pom.xml如下:
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
${springcloudalibaba.version}
- 以上通过spring-cloud-starter-alibaba-seata:2021.1版本,通过seata-spring-boot-starter:1.3.0,而实际依赖为seata:1.3.0版本。
- 若使用MySQL的版本≥8.0.23,则会报错“i.s.r.d.u.parser.JacksonUndoLogParser : json decode exception, Cannot construct instance of `java.time.LocalDateTime` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)”。 Seata官方说在1.4.2中已修复此问题,但实际测试发现Seata1.4.2还没有修复到位。因此MySQL数据库版本因为小于等于8.0.22才能避免此错误。具体见https://github.com/seata/seata/issues/3620#
另:
2、各个微服务本地数据库中,确保参与分布式事务的业务表都具有PK主键(否则Seata会报错“Could not found any index in the table” )。 Seata目前只支持单列主键,不支持复合主键。 具体见官方《Seata常见问题 第13》http://seata.io/zh-cn/docs/overview/faq.html
3、由于使用Seata AT模式,需要在各个微服务使用的应用数据库中创建undo_log表(这里不是Seata服务端的数据库,而是客户端各个微服务的业务数据库)。
创建表的SQL语句脚本来源参见本文附录A说明。这里使用AT模式,以MySQL为例(https://github.com/seata/seata/blob/develop/script/client/at/db/mysql.sql)在payment、order两个服务所使用的数据库分别创建下表:
-- 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';
注:表名`undo_log`可以修改。若修改,需要同时修改建表SQL语句和微服务配置seata.client.undo.log-table=值
4、在各个微服务中分别配置Seata相关信息。由于payment、order服务本身由Nacos配置中心进行管理,而且两个服务的配置内容是相同的,因此在配置中心共同通用commonshare.properties增加以下Seata相关配置信息
#================Seata Client Config============
#是否启用Seata(默认启用)
seata.enabled=true
# 定义本服务的事务组(可以每个应用独立取名,也可以不同的应用使用相同的名字)
seata.tx-service-group=mytest-tx-group
# 指定逻辑事务组映射到vgroup-mapping(等号左侧为事务分组名,vgroup-mapping的值需要与seata-server中registry.conf中的cluster保持一致)
seata.service.vgroup-mapping.mytest-tx-group=beijing
seata.registry.type=nacos
seata.registry.nacos.server-addr=39.100.80.168:8848
seata.registry.nacos.application=seata-server
seata.registry.nacos.group=SEATA_GROUP
5、在需要分布式事务的整个业务流程入口增加 @GlobalTransactional 注解。这里order订单服务是整个分布式事务逻辑的入口,因此注解增加在OrderController.java上,内容如下:
@RestController
public class OrderController {
@Autowired
private IPaymentServiceClient paymentServiceClient;
@Autowired
private JdbcTemplate jdbcTemplate;
Logger logger = LoggerFactory.getLogger(OrderController.class);
boolean forcefail = true;
@GetMapping("/consumer/{orderid}")
@GlobalTransactional(timeoutMills = 28000, name = "gts-seata-example")
public ResponseEntity consumerFeign(@PathVariable("orderid") Long orderid) {
logger.info("全局事务XID=[{}], 事务BranchType=[{}] ", RootContext.getXID(), RootContext.getBranchType());
String result = paymentServiceClient.dodopay(orderid).getBody(); // 调用支付服务进行支付
logger.info("[paymentService result]: {}", result);
jdbcTemplate.update("insert T_Order(order_id, address, remark) values (" + orderid + ", 'dizhi', '"+result+"')");
//模拟在支付完成后,本订单事务后续处理过程中发生异常后,全局回滚功能
if (forcefail) {
throw new RuntimeException("模拟在支付完成后,本订单事务后续处理过程中发生异常!");
}
return ResponseEntity.ok ("调用订单服务完成。 订单orderid:" + orderid + ", 调用支付返回的Body:" + result);
}
}
注解选项:timeoutMills是超时时间,name是事务的名字, rollbackFor指定函数抛出哪些异类class会进行全局回滚(不指定默认为RuntimeException)。noRollbackFor指定函数抛出哪些异类class不会进行全局回滚。
注:全局事务的整体提交还是回滚,是由全局事务的发起方(即 @GlobalTransactional 所在的服务)来决定的。即若全局应commt时,则由发起方告诉TC(seata-server),再由TC通知各个branch完成本地提交。 若全局应rollback时,则由发起方告诉TC(seata-server),再由TC通知各个branch完成本地回滚。
因此在服务调用链路中,必须将异常抛出至全局事务的发起方,从而被全局事务发起方的 @GlobalTransactional 注解感知到。
6、(可选)为了更好的观察XID的传递情况。在PaymentController.java中增加日志
@RestController
public class PaymentController {
@Value("${server.port}")
private int myport;
@Autowired
private JdbcTemplate jdbcTemplate;
Logger logger = LoggerFactory.getLogger(PaymentController.class);
@GetMapping("/dopay/{orderid}")
public ResponseEntity paylogic(@PathVariable("orderid") Long orderid) {
logger.info("全局事务XID=[{}], 事务BranchType=[{}] ", RootContext.getXID(), RootContext.getBranchType());
int insert_rows = jdbcTemplate.update("insert into T_PAY (order_id, amount) values (" + orderid +", 999)");
logger.info("支付服务successful! orderid=" + orderid + ", 支付成功。 支付服务的端口为port=" + myport);
return ResponseEntity.ok("支付服务successful! orderid=" + orderid + ", 支付成功。 支付服务的端口为port=" + myport);
}
}
7、分别运行payment、order服务。测试。curl http://localhost:8888/consumer/66
可以发现两个服务的业务数据库要么同时生效有数据,要么同时没有数据,实现了分布式事务一致性。
上面在seata-server使用了config.type=file,store.mode=file。这两者都不适合生产环境。因此需要进行调整:即将config.type=file改为Nacos配置中心、store.mode=file改为由数据库db存放。
1、获取Seata服务端数据库建表SQL脚本。 脚本来源参见本文附录A说明 (即https://github.com/seata/seata/tree/develop/script/config-center)
2、执行建表SQL脚本。 先在MySQL数据库创建名为“seata”的schema。然后在该schema下,执行以上SQL脚本(四张表:`global_table`、`branch_table`、`lock_table`、`distributed_lock`)
3、进入服务端seata软件安装目录下conf目录,编辑registry.conf文件,如下:
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "39.100.80.168:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "beijing"
username = ""
password = ""
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "39.100.80.168:8848"
namespace = ""
group = "SEATA_GROUP"
username = ""
password = ""
dataId = "seataServer.properties"
}
}
4、获取配置中心的配置项内容模板。 配置内容来源参见本文附录A说明 (即https://github.com/seata/seata/tree/develop/script/config-center)
5、以获取“config.txt”文件内容作为模板参考 (各配置项的含义说明参见:https://seata.io/zh-cn/docs/user/configurations.html)。
因为“config.txt”文件原始内容比较多,需要从中选择(其他配置项不必显示设置,保持默认值即可)。另外“config.txt”即有对Seata服务端的配置,也有对Seata客户端(即各微服务)的定义。这里只选择与服务端有关且比较常用配置,选取修改后配置内容如下:
#========= Seata Server Config:Server端连接数据库信息
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=Pwd_1234
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.distributedLockTable=distributed_lock
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
人工在Nacos配置中心界面上,将以上内容配置拷贝到Nacos,如下图。配置中心的dataId=seataServer.properties,group=SEATA_GROUP等必须与registry.conf文件中定义的一致。 (网上文章大多通过nacos-config.sh的Shell脚本将以上内容推送到Nacos中。不过建议最好还是人工配置,比较方便且过程自主可控)
6、重启seata-server端。./seata-server.sh -h 39.100.80.168 &
7、测试可以发现:若全局事务整体成功提交或成功回滚,在seata-server的三张Tables中不会留下数据(实际SQL动作过程是先insert、再update、最后delete)。只有未完成的全局事务,三张Tables才会留有数据。
见安装目录seata/conf/README-zh.md.
该文件内如如下:
# 脚本说明
## [client](https://github.com/seata/seata/tree/develop/script/client)
> 存放用于客户端的配置和SQL
- at: AT模式下的 `undo_log` 建表语句
- conf: 客户端的配置文件
- saga: SAGA 模式下所需表的建表语句
- spring: SpringBoot 应用支持的配置文件
## [server](https://github.com/seata/seata/tree/develop/script/server)
> 存放server侧所需SQL和部署脚本
- db: server 侧的保存模式为 `db` 时所需表的建表语句
- docker-compose: server 侧通过 docker-compose 部署的脚本
- helm: server 侧通过 Helm 部署的脚本
- kubernetes: server 侧通过 Kubernetes 部署的脚本
## [config-center](https://github.com/seata/seata/tree/develop/script/config-center)
> 用于存放各种配置中心配置项模板和脚本,执行时都会读取 `config.txt`配置文件,并写入配置中心
- nacos: 用于向 Nacos 中添加配置
- zk: 用于向 Zookeeper 中添加配置,脚本依赖 Zookeeper 的相关脚本,需要手动下载;ZooKeeper相关的配置可以写在 `zk-params.txt` 中,也可以在执行的时候输入
- apollo: 向 Apollo 中添加配置,Apollo 的地址端口等可以写在 `apollo-params.txt`,也可以在执行的时候输入
- etcd3: 用于向 Etcd3 中添加配置
- consul: 用于向 consul 中添加配置