微服务·数据一致-seata
概述
Seata(Simple Extensible Autonomous Transaction Architecture)是一个开源的分布式事务解决方案,旨在帮助应用程序分布式事务管理的挑战。Seata提供了一套全面的工具和框架,可用于实现跨多个数据库和服务的一致性事务管理。本报告将深入探讨Seata的核心概念、架构、特性以及使用场景。
核心概念
- 全局事务:Seata引入了全局事务的概念,将一组分支事务(通常是数据库操作)组织在一起,以确保它们要么全部成功,要么全部回滚,以维护数据的一致性。
- 分支事务:分支事务是全局事务中的单个数据库操作或者服务调用,它们遵循全局事务的指导并协作以实现全局事务的一致性。
- 全局事务协调器:全局事务协调器(TC,Transaction Coordinator)负责群居事务的协调和管理,确保所有分支事务按照事务的隔离级别执行。
架构
Seata的架构包括三个核心组件:
- 全局事务协调器(TC,Transaction Coordinator):负责全局事务的协调和管理。它记录了全局事务的状态,并根据分支事务的状态来确保全局事务的最终结果。
- 分支事务参与者(TM, Transaction Manager):负责管理和执行分支事务、包括尝试、确认和回滚分支事务(在调用服务的方法中用注解启动事务)。
- 分支事务资源管理器(RM,Resource Manager):负责管理分支事务所涉及的资源,入数据库、消息队列、缓存等。
Seata实现分布式事务,设计了一个关键角色UNDO_LOG(回滚日志记录表),在每个应用分布式事务的业务库中创建了这张表,这个表的的核心作用就是将业务数据在更新前后的数据镜像组成回滚日志,备份在UNDO_LOG表中,以便业务异常能随时回滚。
特性
- 强一致性:Seata确保全局事务的强一致性,即要么全部提交成功,要么全部回滚。
- 高性能:Seata的设计注重性能,可以处理高并发的分布式事务。
- 分布式事务隔离:Seata支持多种隔离级别,包括串行化、可重复读等,以适应不同的业务需求。
- 全局事务追踪:Seata提供全局事务的追踪和监控能力,有助于故障排查和性能优化。
- 分布式事务补偿:Seata支持Saga模式,可依通过补偿事务来处理部分失败的分布式事务。
Seata AT模式的详解
以下单扣库存、扣余额举例:
第一阶段
首先Seata的JDBC数据源代理通过对业务SQL解析,提取SQL的元数据,也就是得到SQL的类型(UPDATE),表(user),条件(where name = “天青色”)等相关信息。
- 先查询数据更改前的信息(前镜像),根据解析得到的条件信息,生成查询语句,定位一条数据。
- 紧接着执行业务SQL,根据前镜像数据之间查询出后镜像数据
- 把业务数据在更新前后的数据镜像组织成回滚日志,将业务数据的更新和回滚日志在同一个本地事务中提交,分别插入到业务表和UNDO_LOG表中。
UNDO_LOG数据格式如下,包括afterImage前镜像、beforeImage后镜像、branchId分支事务ID,xid全局事务ID
{
"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 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。
XID的传递过程
- TM注册到TC中,要发起全局事务式,先向TC发送一个通知,然后TC就会生成一个唯一的ID返还给TM,这个ID就是xid。
- TM收到xid后放入当前线程的ThreadLocal中存储,在业务逻辑中使用OpenFeign调用其他服务的接口时,Seata重写了feign客户端;如果是Rest Template的方式,Seata也写了请求拦截器,将当前ThreadLocal中的xid放入hreader中进行传递。
- RM收到请求后,首先在拦截器中尝试获取header中的xid,如果获取成功,就将xid再放入当前ThreadLocal中。
- RM通知TC自己是该xid的事务参与者,也就是注册该分支服务。
实践
Seata Server
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;
Seata Client
搭建服务,核心配置如下
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