阿里技术(如何选择分布式事务解决方案?) https://mp.weixin.qq.com/s/2AL3uJ5BG2X3Y2Vxg0XqnQ## 起初看了这个分布式相关的信息,对于seata 有点想了解一下实现的精髓,之前公司内部也有大佬分享过seata 还不止一次的分享过 内部也实现了一个简单的分布式事务,借此机会了解一下这个鬼东西。 Seata (Simple Extensible Autonomous Transaction Architecture) AT (Automatic Transaction) 模式
官方的文档: https://seata.io/zh-cn/docs/overview/what-is-seata.html
一、AT (Automatic Transaction) 模式
AT 模式 完全是根据 上面 https://mp.weixin.qq.com/s/2AL3uJ5BG2X3Y2Vxg0XqnQ## 这篇博客copy 下来的,感觉比官方文档说明更加的清楚,两阶段提交的变异品,总体来说还是十分不错。Seata支持很多种模式 AT、TCC、XA、SAGA 但是AT具有创新性,我们就了解这个吧。
AT 模式的运行机制
下面我们重点说一下 AT 模式的运行机制:
- 全局事务依然是基于各个分支事务来完成。Seata Server 协调各个分支事务要么一起提交,要么一起回滚。
- 各个分支事务在运行时,Seata Client 通过对 SQL 执行的代理和拦截,通过解析 SQL 定位到行记录,记录下 SQL 执行前后的行数据快照,beforeImage 和 afterImage 共同构成了回滚日志,回滚日志记录在独立的表中。回滚日志的写入和业务数据的更改在在同一个本地事务中提交。
- 分支事务完成后,立即释放对本地资源的锁,然后给 Seata 协调器上报事务执行的结果。
- Seata 协调器汇总各个分支事务的完成情况**,生成事务提交或者回滚的决议,将决议下发给 Seata Client**。
- 如果决议是提交事务,则 Seata Client 异步清理回滚日志;如果决议是回滚事务,则 Seata Client 根据回滚日志进行补偿操作,补偿前会对比当前数据快照和 afterImage 是否一致,如果不一致则回滚失败,需要人工介入。
AT模式限制
AT 模式通过自动生成回滚日志的方式,使得业务方接入成本低,对业务入侵度很低,但是应用 AT 模式也有一些限制:
- AT 模式只支持基于 ACID 事务的关系数据库。
- AT 模式是通过对 SQL 解析来完成的,对 SQL 语法的支持有限,使用复杂 SQL 时需要考虑兼容性。
- 目前不支持复合主键,业务表在设计时注意添加自增主键。
- 全局事务默认的隔离级别是读未提交,但是通过 SELECT…FOR UPDATE 等语句,可以实现读已提交的隔离级别。通过全局排它写锁,可以做到的隔离级别介于读未提交和读已提交之间。
二、实践
2.1 准备工作
-
java 环境变量
-
mysql 本地安装一台
-
nacos 安装,下载 nacos-1.2.1
https://github.com/alibaba/nacos/releases
启动脚本 ./bin startup.sh -m standalone
访问地址: http://127.0.0.1:8848/nacos/#
-
seata server 安装
https://github.com/seata/seata/releases
参考这个文档: https://seata.io/zh-cn/docs/ops/deploy-guide-beginner.html
本地启动seata-server 使用db-server sql 初始化脚本
https://github.com/seata/seata/blob/1.2.0/script/server/db/mysql.sql
修改server的配置文件 模式修改为db,修改数据库密码
启动脚本 sh ./bin/seata-server.sh -p 8091 -h 127.0.0.1 -m db
at 模式客户端也需要undo_log 数据库创建,github seata example 目录下 也有sql
https://github.com/seata/seata/blob/1.2.0/script/client/at/db/mysql.sql -
下载github seata example
https://github.com/seata/seata-samples/tree/master/springboot-dubbo-seata/
然后根据官方的文档: https://seata.io/zh-cn/docs/user/quickstart.html 即可体验一下子。
这里 客户端的服务端都使用一个数据库拉~
2.2 实践
此时,我们已经下载好了 seata example 、启动好了 seata server、nacos等等。
官方的体验文档: https://seata.io/zh-cn/docs/user/quickstart.html 这里的前提是修改好了 数据库密码
依次启动服务
然后去nacos 可以看到在线的服务
模拟买买买
然后去查看数据库,水杯的数据的变化
http://localhost:8104/business/dubbo/buy
{
"userId": "1",
"commodityCode": "C201901140001",
"name": "demoData",
"count": 1,
"amount": 1
}
买买买的拦截Aop
从seata的逻辑流程图中可以看到一丝的痕迹,拦截要做一些的处理,传递context 信息,为后续业务接口的调用传递上下文信息。eg xid
/**
* init global transaction scanner
*
* @Return: GlobalTransactionScanner
*/
@Bean
public GlobalTransactionScanner globalTransactionScanner(){
return new GlobalTransactionScanner("dubbo-gts-seata-example", "my_test_tx_group");
}
连接池的代理
io.seata.rm.datasource.DataSourceProxy,为啥要代理连接池?这个是执行sql的地方啊!从AT 模式: https://seata.io/zh-cn/docs/overview/what-is-seata.html 这个实践的原理中有过介绍 ,业务发起方会将分布式事务的上下文进行传递,如果当前执行事务有分布式的上下文信息,那么就会解析sql,查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据,执行业务 SQL:更新这条记录,询后镜像:根据前镜像的结果,通过 主键 定位数据。
这里对于sql的解析, 兼容性真的需要水平。(AT 模式是通过对 SQL 解析来完成的,对 SQL 语法的支持有限,使用复杂 SQL 时需要考虑兼容性。目前不支持复合主键,业务表在设计时注意添加自增主键。)
/**
* init datasource proxy
* @Param: druidDataSource datasource bean instance
* @Return: DataSourceProxy datasource proxy
*/
@Bean
public DataSourceProxy dataSourceProxy(DruidDataSource druidDataSource){
return new DataSourceProxy(druidDataSource);
}
@Bean
public DataSourceTransactionManager transactionManager(DataSourceProxy dataSourceProxy) {
return new DataSourceTransactionManager(dataSourceProxy);
}
断点看看 undo_log
{
"@class": "io.seata.rm.datasource.undo.BranchUndoLog",
"xid": "192.168.0.104:8091:2012950778",
"branchId": 2012950785,
"sqlUndoLogs": [
"java.util.ArrayList",
[
{
"@class": "io.seata.rm.datasource.undo.SQLUndoLog",
"sqlType": "UPDATE",
"tableName": "t_storage",
"beforeImage": {
"@class": "io.seata.rm.datasource.sql.struct.TableRecords",
"tableName": "t_storage",
"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": 1
},
{
"@class": "io.seata.rm.datasource.sql.struct.Field",
"name": "count",
"keyType": "NULL",
"type": 4,
"value": 999
}
]
]
}
]
]
},
"afterImage": {
"@class": "io.seata.rm.datasource.sql.struct.TableRecords",
"tableName": "t_storage",
"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": 1
},
{
"@class": "io.seata.rm.datasource.sql.struct.Field",
"name": "count",
"keyType": "NULL",
"type": 4,
"value": 998
}
]
]
}
]
]
}
}
]
]
}
注释打开看看回滚的效果
三、总结
从程序的角度实践,简单的了解了一下seata的使用,了解了AT 模式对于二阶段提交的改进的使用。动手实践一下还是不错的,GlobalTransactionScanner 拦截全局事务,注册TM client、RM client 通过netty活server端通信,其中还有动态从配置中读出配置等等接入,简单的看看一下源代码,学习了一波。结合 https://mp.weixin.qq.com/s/2AL3uJ5BG2X3Y2Vxg0XqnQ## 这篇文章,理论和实践都玩了一下,seata example 中还玩了一下TCC 模式嗯。