(一)seata1.2 AT及XA模式实例演示

欢迎关注本人公众号

在这里插入图片描述

概述

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

本文先将官方实例跑起来,看看运行效果,值后在对其原理和源码进行分析。

下载源码

进入seata的GitHub主页,下载seata和seata-samples两个项目。下载下来后可以用idea打开。
(一)seata1.2 AT及XA模式实例演示_第1张图片
下载完成后,idea导入seata-samples文件夹下的seata-xa和seata文件夹下的 server两个项目。
(一)seata1.2 AT及XA模式实例演示_第2张图片
(一)seata1.2 AT及XA模式实例演示_第3张图片

AT模式演示

官方文档写的是下载seata-server-xxx.zip解压运行,我这里不适用这种方法,因为后续还要阅读源码,所以直接运行上一步下载的seata源码运行。

准备工作

本地安装mysql8

安装步骤:mysql8.0.20安装教程

配置修改

两个项目均需要修改为我们自己的mysql,并且设置使用AT模式。
先将server切换为1.2.0版本
在这里插入图片描述
seata-server修改内容:
(一)seata1.2 AT及XA模式实例演示_第4张图片
同时将pom文件中的connection版本改为8:

<dependency>
    <groupId>mysqlgroupId>
    <artifactId>mysql-connector-javaartifactId>
    <version>8.0.16version>
dependency>

然后修改seata-xa项目的配置。需要修改以下内容:

(一)seata1.2 AT及XA模式实例演示_第5张图片
application.properties修改内容都是Mysql的地址和用户名密码。
pom文件修改的是MySQL驱动的版本,同上。

剩下的几个DataSourceConfiguration文件都是修改为AT模式。
(一)seata1.2 AT及XA模式实例演示_第6张图片

建表

我们这里测试的是AT模式,需建4个表

DROP TABLE IF EXISTS `storage_tbl`;
CREATE TABLE `storage_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `commodity_code` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT 0,
  PRIMARY KEY (`id`),
  UNIQUE KEY (`commodity_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `order_tbl`;
CREATE TABLE `order_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) DEFAULT NULL,
  `commodity_code` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT 0,
  `money` int(11) DEFAULT 0,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `account_tbl`;
CREATE TABLE `account_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) DEFAULT NULL,
  `money` int(11) DEFAULT 0,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
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;

运行官方实例

先启动server端服务,直接运行io.seata.server.Server的main方法即可。启动成功输出

2020-07-06 11:14:19.108 INFO [main]io.seata.config.FileConfiguration.:121 -The configuration file used is registry.conf
2020-07-06 11:14:19.145 INFO [main]io.seata.config.FileConfiguration.:121 -The configuration file used is file.conf
2020-07-06 11:14:20.765 INFO [main]io.seata.core.rpc.netty.RpcServerBootstrap.start:155 -Server started ... 

依次启动account/order/storage/business 4个服务。
(一)seata1.2 AT及XA模式实例演示_第7张图片
启动成功后,会在server端注册。这里seata是使用的springCloud Feign。


2020-07-06 11:14:46.080 INFO [ServerHandlerThread_1_500]io.seata.core.rpc.DefaultServerMessageListenerImpl.onRegRmMessage:127 -RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://rm-2zetd9474ydd1g5955o.mysql.rds.aliyuncs.com:3306/fescar', applicationId='account-xa', transactionServiceGroup='my_test_tx_group'},channel:[id: 0xaa9af4c7, L:/127.0.0.1:8091 - R:/127.0.0.1:50222]
2020-07-06 11:15:00.620 INFO [ServerHandlerThread_1_500]io.seata.core.rpc.DefaultServerMessageListenerImpl.onRegRmMessage:127 -RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://127.0.0.1:3306/test', applicationId='order-xa', transactionServiceGroup='my_test_tx_group'},channel:[id: 0xaef9d3dd, L:/127.0.0.1:8091 - R:/127.0.0.1:50259]
2020-07-06 11:15:07.131 INFO [ServerHandlerThread_1_500]io.seata.core.rpc.DefaultServerMessageListenerImpl.onRegRmMessage:127 -RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://127.0.0.1:3306/test', applicationId='storage-xa', transactionServiceGroup='my_test_tx_group'},channel:[id: 0xf7f0d0cd, L:/127.0.0.1:8091 - R:/127.0.0.1:50281]
2020-07-06 11:15:43.133 INFO [NettyServerNIOWorker_1_16]io.seata.core.rpc.DefaultServerMessageListenerImpl.onRegTmMessage:153 -TM register success,message:RegisterTMRequest{applicationId='account-xa', transactionServiceGroup='my_test_tx_group'},channel:[id: 0xedc81195, L:/127.0.0.1:8091 - R:/127.0.0.1:50341]
2020-07-06 11:15:56.732 INFO [NettyServerNIOWorker_1_16]io.seata.core.rpc.DefaultServerMessageListenerImpl.onRegTmMessage:153 -TM register success,message:RegisterTMRequest{applicationId='order-xa', transactionServiceGroup='my_test_tx_group'},channel:[id: 0x90901ea5, L:/127.0.0.1:8091 - R:/127.0.0.1:50360]
2020-07-06 11:16:04.010 INFO [NettyServerNIOWorker_1_16]io.seata.core.rpc.DefaultServerMessageListenerImpl.onRegTmMessage:153 -TM register success,message:RegisterTMRequest{applicationId='storage-xa', transactionServiceGroup='my_test_tx_group'},channel:[id: 0x7c1fd552, L:/127.0.0.1:8091 - R:/127.0.0.1:50369]

再business服务启动后,会进行数据初始化:账户余额10000,库存100

@PostConstruct
    public void initData() {
        jdbcTemplate.update("delete from account_tbl");
        jdbcTemplate.update("delete from order_tbl");
        jdbcTemplate.update("delete from storage_tbl");
        jdbcTemplate.update("insert into account_tbl(user_id,money) values('" + TestDatas.USER_ID + "','10000') ");
        jdbcTemplate.update("insert into storage_tbl(commodity_code,count) values('" + TestDatas.COMMODITY_CODE + "','100') ");
    }

总共有4个服务。账户服务,订单服务,库存服务,采购业务服务。是目前主流的微服务架构,本文演示的也是微服务架构下分布式事务问题。

访问http://127.0.0.1:8084/purchase调用服务。
基于初始化数据,和默认的调用逻辑,purchase 将可以被成功调用 3 次。
每次账户余额扣减 3000,由最初的 10000 减少到 1000。
第 4 次调用,因为账户余额不足,purchase 调用将失败。相应的:库存、订单、账户都回滚。

调用一次以后,数据库中余额变化,扣了3000块,30个库存,多了一条订单记录:

mysql> select * from account_tbl;
+----+---------+-------+
| id | user_id | money |
+----+---------+-------+
|  2 | U100000 |  7000 |
+----+---------+-------+
1 row in set (0.01 sec)

mysql> select * from order_tbl;
+----+---------+----------------+-------+-------+
| id | user_id | commodity_code | count | money |
+----+---------+----------------+-------+-------+
|  3 | U100000 | C100000        |    30 |  3000 |
+----+---------+----------------+-------+-------+
1 row in set (0.00 sec)

mysql> select * from storage_tbl;
+----+----------------+-------+
| id | commodity_code | count |
+----+----------------+-------+
|  2 | C100000        |    70 |
+----+----------------+-------+
1 row in set (0.00 sec)

mysql> select * from undo_log;
Empty set (0.00 sec)

当调用第4此时,账户余额不足,账户服务扣减余额失败;
下单服务失败,相应的库存订单都需要回滚。
读者可以自行验证,自第四次调用开始,都不会成功。DB中的结果不会有任何变化。

(一)seata1.2 AT及XA模式实例演示_第8张图片

AT模式原理初步分析

看一下undoLog表的内容,读者可以在运行过程中添加断点查看该表日志,因为如果等程序运行完成,该表的日志会被删除。
(一)seata1.2 AT及XA模式实例演示_第9张图片
选一条日志的rollback_info看看

{
	"@class": "io.seata.rm.datasource.undo.BranchUndoLog",
	"xid": "192.168.252.1:8091:2016197280",
	"branchId": 2016197281,
	"sqlUndoLogs": [
		"java.util.ArrayList",
		[
			{
				"@class": "io.seata.rm.datasource.undo.SQLUndoLog",
				"sqlType": "UPDATE",
				"tableName": "storage_tbl",
				"beforeImage": {
					"@class": "io.seata.rm.datasource.sql.struct.TableRecords",
					"tableName": "storage_tbl",
					"rows": [
						"java.util.ArrayList",
						[
							{
								"@class": "io.seata.rm.datasource.sql.struct.Row",
								"fields": [
									"java.util.ArrayList",
									[
										{
											"@class": "io.seata.rm.datasource.sql.struct.Field",
											"name": "id",
											"keyType": "PRIMARY_KEY",
											"type": 4,
											"value": 2
										},
										{
											"@class": "io.seata.rm.datasource.sql.struct.Field",
											"name": "count",
											"keyType": "NULL",
											"type": 4,
											"value": 10
										}
									]
								]
							}
						]
					]
				},
				"afterImage": {
					"@class": "io.seata.rm.datasource.sql.struct.TableRecords",
					"tableName": "storage_tbl",
					"rows": [
						"java.util.ArrayList",
						[
							{
								"@class": "io.seata.rm.datasource.sql.struct.Row",
								"fields": [
									"java.util.ArrayList",
									[
										{
											"@class": "io.seata.rm.datasource.sql.struct.Field",
											"name": "id",
											"keyType": "PRIMARY_KEY",
											"type": 4,
											"value": 2
										},
										{
											"@class": "io.seata.rm.datasource.sql.struct.Field",
											"name": "count",
											"keyType": "NULL",
											"type": 4,
											"value": -20
										}
									]
								]
							}
						]
					]
				}
			}
		]
	]
}

看了这个回复日志,就很明显了。undolog中记录了两个快照:beforeImage和afterImage。分别记录了修改前后的字段值,在需要回滚时,就使用beforeImage中记录的值来回复原始数据即可。

这跟MySQL本身的undolog有异曲同工之妙。

当然实际分布式处理比这复杂的多,上面只是将最核心的原理介绍一下,接下来文章会详细分析seata的原理

XA事务实例演示

在使用XA事务时,我发现 mysql-connector-java 还不能改为8.X 版本。否则会报错, 无法创建XAConnection:

Caused by: java.sql.SQLFeatureNotSupportedException
	at com.alibaba.druid.util.MySqlUtils.createXAConnection(MySqlUtils.java:165)
	at io.seata.rm.datasource.util.XAUtils.createXAConnection(XAUtils.java:62)
	at io.seata.rm.datasource.util.XAUtils.createXAConnection(XAUtils.java:41)
	at io.seata.rm.datasource.xa.DataSourceProxyXA.getConnectionProxy(DataSourceProxyXA.java:63)
	at io.seata.rm.datasource.xa.DataSourceProxyXA.getConnection(DataSourceProxyXA.java:49)
	at org.springframework.jdbc.datasource.DataSourceUtils.fetchConnection(DataSourceUtils.java:151)
	at org.springframework.jdbc.datasource.DataSourceUtils.doGetConnection(DataSourceUtils.java:115)
	at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:78)
	... 63 more

所以将mysql-connector-java的版本还原回去:5.1.48

将三个项目中的数据源全部改为DataSourceProxyXA实现

@Bean("dataSourceProxy")
public DataSource dataSource(DruidDataSource druidDataSource) {
    // DataSourceProxy for AT mode
    // return new DataSourceProxy(druidDataSource);

    // DataSourceProxyXA for XA mode
    return new DataSourceProxyXA(druidDataSource);
}

启动服务,依旧访问http://127.0.0.1:8084/purchase即可。操作与上面AT一样,读者可以自行验证。

此时由于使用的时XA事务,所以undo_log表用不到。

你可能感兴趣的:(分布式+高并发)