Seata(Simple Extensible Autonomous Transaction Architecture)是一个开源的分布式事务解决方案,旨在帮助应用程序分布式事务管理的挑战。Seata提供了一套全面的工具和框架,可用于实现跨多个数据库和服务的一致性事务管理。本报告将深入探讨Seata的核心概念、架构、特性以及使用场景。
Seata的架构包括三个核心组件:
首先Seata的JDBC数据源代理通过对业务SQL解析,提取SQL的元数据,也就是得到SQL的类型(UPDATE),表(user),条件(where name = “天青色”)等相关信息。
{
"branchId":641789253,
"xid":"xid:xxx",
"undoItems":[
{
"afterImage":{
"rows":[
{
"fields":[
{
"name":"id",
"type":4,
"value":1
}
]
}
],
"tableName":"product"
},
"beforeImage":{
"rows":[
{
"fields":[
{
"name":"id",
"type":4,
"value":1
}
]
}
],
"tableName":"product"
},
"sqlType":"UPDATE"
}
]
}
这样可以保证任何提交的业务数据的更新一定有相应的回滚日志。
在本地事务提交前,各分支事务需要向TC注册分支(Branch Id),为要修改的记录申请全局锁,要为这条数据加锁,利用Select for update语句。而如果一直拿不到锁那就需要回滚本地事务。TM开启事务后会生成全局唯一的XID,会在各个调用的服务间进行传递。
有了这样的机制,本地事务分支便可以在全局事务的第一阶段提交,并马上释放本地事务锁定的资源。相比传统的XA事务在第二阶段释放资源,Seata降低了锁范围提高效率。即使第二阶段发生异常需要回滚,也可以快速从UNDO_LOG表中找到对应回滚数据并反解析成SQL来达到回滚补偿。
最后本地事务提交,业务数据的更新和前面胜澈功能的UNDO_LOG数据一并条,并将本地事务提交的结果上报给全局事务协调者TC。
第二阶段是根据各分支的决议作出提交或者回滚:
如果决议是全局提交,此时各分支事务已提交并成功,这是全局事务协调者(TC)会向分支发送第二阶段的请求。收到TC的分支提交请求,该请求会被放入一个异步任务队列中,并马上返回提交结果返回给TC。异步队列中会异步和批量的根据BranchID查找并删除相应的UNDO_LOG回滚记录。
如果决议是全局回滚,RM服务放收到TC全局协调者发来的回滚请求,通过XID和Branch ID找到相应的回滚日志记录,通过回滚记录生成反向的更新SQL并执行,以完成分支的回滚。
举例:两个全局事务tx1和tx2,分别对应a表的m字段更新操作,m的初始值1000。
tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。 tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁 。
tx1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。
如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。
此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。
因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。
在数据库本地事务管理级别读已提交或以上的基础上,Seata AT模式的默认全局隔离级别是读未提交。如果在特定的场景下,必须要求全局的读已提交,seata的实现方式是通过SELECT FOR UPDATE语句代理。
SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。
file.conf文件用于配制持久化事务日志的模式,目前提供file、db、redis三种方式。在选择db方式后,需要在对应的数据库中闯将gloableTable(持久化全局事务)、branchTable(持久化各提交分支的事务)、lockTable(持久化各分支锁定资源事务)三张表。
-- the table to store GlobalSession data
-- 持久化全局事务
CREATE TABLE IF NOT EXISTS `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`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store BranchSession data
-- 持久化各提交分支的事务
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store lock data
-- 持久化每个分支锁表事务
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(96),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
搭建服务,核心配置如下
spring:
application:
name: storage-server
cloud:
alibaba:
seata:
tx-service-group: my_test_tx_group
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://47.93.6.1:3306/seat-storage
username: root
password: root
业务大致流程:用户发起下单请求,本地 order 订单服务创建订单记录,并通过 RPC 远程调用 storage 扣减库存服务和 account 扣账户余额服务,只有三个服务同时执行成功,才是一个完整的下单流程。如果某个服执行失败,则其他服务全部回滚。
Seata 对业务代码的侵入性非常小,代码中使用只需用 @GlobalTransactional 注解开启一个全局事务即可。
@Override
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public void create(Order order) {
String xid = RootContext.getXID();
LOGGER.info("------->交易开始");
//本地方法
orderDao.create(order);
//远程方法 扣减库存
storageApi.decrease(order.getProductId(), order.getCount());
//远程方法 扣减账户余额
LOGGER.info("------->扣减账户开始order中");
accountApi.decrease(order.getUserId(), order.getMoney());
LOGGER.info("------->扣减账户结束order中");
LOGGER.info("------->交易结束");
LOGGER.info("全局事务 xid: {}", xid);
}
在相关的业务库中创建undo_log表来存数据回滚日志,表结构如下:
-- 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`
(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'increment id',
`branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(100) 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 NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME NOT NULL COMMENT 'modify datetime',
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
Seata是一个强大的分布式事务解决方案,具备高性能和强一致性的特点,适用于各种需要数据一致性的分布式应用场景。它的架构和设计使得分布式事务管理变得更加容易,有助于解决分布式系统中的一致性问题。对于需要构建高可用性和高性能的分布式应用程序的团队来说,Seata是一个值得考虑的工具和框架。
https://www.cnblogs.com/chengxy-nds/p/14046856.html
https://seata.io/zh-cn/docs/next/dev/mode/at-mode