一.分布式事务问题
在一系列微服务系统当中,假如不存在分布式事务,会发生什么呢?
正常情况下:页面发起一个请求,假设流程中对服务A操作的同时,还需要调用服务B,服务C进行操做,那么这里对于服务A及服务B的操作都是在自己的服务中进行的,那么事务也就是在自己的服务中控制的,一切都没问题的时候,服务A,B,C完成自己本地的事务操作后提交,数据没有问题。
异常情况:
假设:子流程服务B出现事务回滚,而主流程及子流程服务C均已提交事务,那么就出现事务不一致问题了,数据就发生错误。
解决方案:
- 两阶段提交方案/XA方案
- TCC (Try、Confirm、Cancel)方案
- 本地消息表
- 可靠消息最终一致性方案
- 最大努力通知方案
- ..............
以上方案都需要自己结合实际业务,配合中间件,写回滚逻辑,需要有丰富的经验,一般公司可以使用阿里提供的Seata框架解决
二.Seata简介
官网:https://seata.io/zh-cn/index.html
GitHub:https://github.com/seata/seata
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双11,对各BU业务进行了有力的支撑。经过多年沉淀与积累,商业化产品先后在阿里云、金融云进行售卖。2019.1 为了打造更加完善的技术生态和普惠技术成果,Seata 正式宣布对外开源,未来 Seata 将以社区共建的形式帮助其技术更加可靠与完备。
术语:
- 全局事务ID(Transaction ID XID):同一事务管理即为一个ID
- 事务协调者TC (Transaction Coordinator) :维护全局和分支事务的状态,驱动全局事务提交或回滚。seata服务
- 事务管理器TM (Transaction Manager) :定义全局事务的范围:开始全局事务、提交或回滚全局事务。@GlobalTransactional注解,事务发起方
- 资源管理器RM (Resource Manager) :管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。就是各个服务连接的数据库
执行流程:
- TM向TC申请开启一个全局事务,全局事务创建成功并生成XID
- XID在服务间调用传播
- RM向TM注册分支事务,将其交给XID对应的全局事务管理
- TM向TC发起针对XID全局事务的回滚或提交
- TC调起XID对应的全部分支事务回滚或提交
三.初步使用
1.下载nacos:https://github.com/alibaba/nacos/releases/tag/1.3.2
2.下载Seata:https://github.com/seata/seata/releases/tag/v1.3.0
3.源码下载:https://github.com/seata/seata或者https://gitee.com/seata-io/seata
4.解压启动nacos:sh bin/startup.sh -m standalone
浏览器访问127.0.0.1:8848/nacos
5.修改seata/conf/registry.conf 文件
6.拉取seata源代码,修改源代码
7.将seata配置提交到nacos保存:sh nacos-config.sh 127.0.0.1
执行源代码的nacos-config.sh脚本,提示Init nacos config finished, please start seata-server.表示成功
8.创建seata库,并在库中建表:seata 0.9.0之前会提供sql脚本,之后版本不提供
-- the table to store GlobalSession data
drop table if exists `global_table`;
create table `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`)
);
-- the table to store BranchSession data
drop table if exists `branch_table`;
create table `branch_table` (
`branch_id` bigint not null,
`xid` varchar(128) not null,
`transaction_id` bigint ,
`resource_group_id` varchar(32),
`resource_id` varchar(256) ,
`lock_key` varchar(128) ,
`branch_type` varchar(8) ,
`status` tinyint,
`client_id` varchar(64),
`application_data` varchar(2000),
`gmt_create` datetime,
`gmt_modified` datetime,
primary key (`branch_id`),
key `idx_xid` (`xid`)
);
-- the table to store lock data
drop table if exists `lock_table`;
create table `lock_table` (
`row_key` varchar(128) not null,
`xid` varchar(96),
`transaction_id` long ,
`branch_id` long,
`resource_id` varchar(256) ,
`table_name` varchar(32) ,
`pk` varchar(36) ,
`gmt_create` datetime ,
`gmt_modified` datetime,
primary key(`row_key`)
);
9.启动seata server:./bin/seata-server.sh
10.业务数据库准备(以官网订单业务逻辑为案例)订单->库存->账户,创建三个数据库seata_order,seata_account,seata_storage
分别在三个库下执行对应建表语句
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,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
11.导入项目seata_test:https://github.com/pengdakun/seata_test.git
注意事项:application.yml配置
全局事务注解:
12.启动三个服务,请求接口http://localhost:2001/order/create,需要在库存,账户表各加一条数据
{
"userId":1,
"commodityCode":1,
"count":2,
"money":10
}
四.原理解析
1.在看TC/TM/RM执行流程
- TM开启分布式事务(TM向TC注册全局事务)
- RM向TC汇报准备情况
- TM结束分布式事务,事务一阶段结束(TM通知TC提交或回滚事务)
- TC汇总事务信息,决定回滚或提交分布式事务
- TC通知所有RM提交或回滚,事务二阶段结束
2.一阶段:seata会拦截业务SQL
- 解析SQL语句,找到业务SQL要更新的业务数据,在业务数据要被更新前保存为before image
- 执行业务SQL语句,在业务数据更新之后,保存after image,并生成行锁
3.二阶段提交数据:如果一阶段一切正常,二阶段提交只需要删除一阶段创建的before image,after image,行锁即可
4.二阶段回滚数据:seata需要回滚一阶段已经执行的SQL数据,主要是使用before image进行数据还原,但是在还原之前需要将当前数据与after image进行对比,如果数据一致则直接还原,否则需要人工处理。之后删除一阶段创建的before image,after image,行锁
5.Debug查看表数据:
seata库:
order库:通过select CONVERT(rollback_info USING utf8mb4) AS body from undo_log;查询rollback_info
{
"@class":"io.seata.rm.datasource.undo.BranchUndoLog",
"xid":"192.168.1.5:8091:63635087974993920",
"branchId":63635088042102785,
"sqlUndoLogs":[
"java.util.ArrayList",
[
{
"@class":"io.seata.rm.datasource.undo.SQLUndoLog",
"sqlType":"INSERT",
"tableName":"order_tbl",
"beforeImage":{
"@class":"io.seata.rm.datasource.sql.struct.TableRecords$EmptyTableRecords",
"tableName":"order_tbl",
"rows":[
"java.util.ArrayList",
[
]
]
},
"afterImage":{
"@class":"io.seata.rm.datasource.sql.struct.TableRecords",
"tableName":"order_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":19
},
{
"@class":"io.seata.rm.datasource.sql.struct.Field",
"name":"user_id",
"keyType":"NULL",
"type":12,
"value":"1"
},
{
"@class":"io.seata.rm.datasource.sql.struct.Field",
"name":"commodity_code",
"keyType":"NULL",
"type":12,
"value":"1"
},
{
"@class":"io.seata.rm.datasource.sql.struct.Field",
"name":"count",
"keyType":"NULL",
"type":4,
"value":2
},
{
"@class":"io.seata.rm.datasource.sql.struct.Field",
"name":"money",
"keyType":"NULL",
"type":4,
"value":10
}
]
]
}
]
]
}
}
]
]
}
JSON中有beforeImage用于保存更新前的数据(库存,账户中undo_log均有记录),通过befoae_image和after_image全局事务的提交与回滚就是通过befoae_image和after_image进行事务补偿的,之后在删除全部非业务表数据。