Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
本文先将官方实例跑起来,看看运行效果,值后在对其原理和源码进行分析。
进入seata的GitHub主页,下载seata和seata-samples两个项目。下载下来后可以用idea打开。
下载完成后,idea导入seata-samples文件夹下的seata-xa
和seata文件夹下的 server
两个项目。
官方文档写的是下载seata-server-xxx.zip
解压运行,我这里不适用这种方法,因为后续还要阅读源码,所以直接运行上一步下载的seata源码运行。
安装步骤:mysql8.0.20安装教程
两个项目均需要修改为我们自己的mysql,并且设置使用AT模式。
先将server切换为1.2.0版本
seata-server修改内容:
同时将pom文件中的connection版本改为8:
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.16version>
dependency>
然后修改seata-xa项目的配置。需要修改以下内容:
application.properties修改内容都是Mysql的地址和用户名密码。
pom文件修改的是MySQL驱动的版本,同上。
剩下的几个DataSourceConfiguration
文件都是修改为AT模式。
我们这里测试的是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个服务。
启动成功后,会在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中的结果不会有任何变化。
看一下undoLog表的内容,读者可以在运行过程中添加断点查看该表日志,因为如果等程序运行完成,该表的日志会被删除。
选一条日志的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事务时,我发现 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
表用不到。