共计 8163 个字符,预计需要花费 21 分钟才能阅读完成。
留神:此篇文章大部分内容都是摘抄自 seata 的官网,写此篇文章的目标是对 seata 官网局部内容总结,不便日后温习。
一、什么是 seata
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简略易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。其中 AT
模式是 Seata
主推的模式,是基于二阶段提交来实现的。
术语:
-
TC (Transaction Coordinator) 事务协调者
保护全局和分支事务的状态,驱动全局事务提交或回滚。
-
TM (Transaction Manager) – 事务管理器
定义全局事务的范畴:开始全局事务、提交或回滚全局事务。
-
RM (Resource Manager) – 资源管理器
治理分支事务处理的资源,与 TC 交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
二、AT 模式的介绍
AT 模式须要保障每个业务库,都有一张 undo_log
表,保留着业务数据执行前和执行后的镜像数据。
1、前提条件
- 基于反对本地 ACID 事务的关系型数据库。
- Java 利用,通过 JDBC 拜访数据库。
2、整体机制
两阶段提交协定的演变:
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,开释本地锁和连贯资源。
-
二阶段:
- 提交异步化,十分疾速地实现。
- 回滚通过一阶段的回滚日志进行反向弥补。
3、读写隔离的实现
1、写隔离
- 一阶段本地事务提交前,须要确保先拿到 全局锁。
- 拿不到 全局锁,不能提交本地事务。
- 拿 全局锁 的尝试被限度在肯定范畴内,超出范围将放弃,并回滚本地事务,开释本地锁。
以一个示例来阐明:
两个全局事务 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 持有的,所以不会产生 脏写 的问题。
2、读隔离
在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的根底上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted)。
如果利用在特定场景下,必须要求全局的 读已提交,目前 Seata 的形式是通过 SELECT FOR UPDATE 语句的代理。
SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其余事务持有,则开释本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查问是被 block 住的,直到 全局锁 拿到,即读取的相干数据是 已提交 的,才返回。
出于总体性能上的思考,Seata 目前的计划并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。
三、事务分组
1、事务分组是什么?
事务分组是 seata 的资源逻辑,相似于服务实例。在 file.conf 中的 my_test_tx_group 就是一个事务分组。能够实现让不同的微服务注册到不同的组上。
2、通过事务分组如何找到后端集群?
- 首先程序中配置了事务分组(GlobalTransactionScanner 构造方法的 txServiceGroup 参数)
- 程序会通过用户配置的配置核心去寻找 service.vgroupMapping .[事务分组配置项],获得配置项的值就是 TC 集群的名称
- 拿到集群名称程序通过肯定的前后缀 + 集群名称去结构服务名,各配置核心的服务名实现不同
- 拿到服务名去相应的注册核心去拉取相应服务名的服务列表,取得后端实在的 TC 服务列表
3、为什么这么设计,不间接取服务名?
这里多了一层获取事务分组到映射集群的配置。这样设计后,事务分组能够作为资源的逻辑隔离单位,呈现某集群故障时能够疾速 failover,只切换对应分组,能够把故障缩减到服务级别,但前提也是你有足够 server 集群。
4、事物分组的例子
1、TC 的异地多机房容灾
- 假设 TC 集群部署在两个机房:guangzhou 机房(主)和 shanghai 机房(备)各两个实例
- 一整套微服务架构我的项目:projectA
- projectA 内有微服务:serviceA、serviceB、serviceC 和 serviceD
其中,projectA 所有微服务的事务分组 tx-transaction-group 设置为:projectA,projectA 失常状况下应用 guangzhou 的 TC 集群(主)
那么失常状况下,client 端的配置如下所示:
seata.tx-service-group=projectA
seata.service.vgroup-mapping.projectA=Guangzhou
如果此时 guangzhou 集群分组整个 down 掉,或者因为网络起因 projectA 临时无奈与 Guangzhou 机房通信,那么咱们将配置核心中的 Guangzhou 集群分组改为 Shanghai,如下:
seata.service.vgroup-mapping.projectA=Shanghai
并推送到各个微服务,便实现了对整个 projectA 我的项目的 TC 集群动静切换。
2、繁多环境多利用接入
- 假如当初开发环境(或预发 / 生产)中存在一整套 seata 集群
- seata 集群要服务于不同的微服务架构我的项目 projectA、projectB、projectC
- projectA、projectB、projectC 之间绝对独立
咱们将 seata 集群中的六个实例两两分组,使其别离服务于 projectA、projectB 与 projectC,那么此时 seata-server 端的配置如下(以 nacos 注册核心为例):
registry {
type = "nacos"
loadBalance = "RandomLoadBalance"
loadBalanceVirtualNodes = 10
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "DEFAULT_GROUP"
namespace = "8f11aeb1-5042-461b-b88b-d47a7f7e01c0"
#同理在其余几个分组 seata-server 实例配置 project-b-group / project-c-group
cluster = "project-a-group"
username = "username"
password = "password"
}
}
client 端的配置如下:
seata.tx-service-group=projectA
#同理,projectB 与 projectC 配置 project-b-group / project-c-group
seata.service.vgroup-mapping.projectA=project-a-group
实现配置启动后,对应事务分组的 TC 独自为其应用服务,整体部署图如下:
3、client 的精细化管制
- 假设当初存在 seata 集群,Guangzhou 机房实例运行在性能较高的机器上,Shanghai 集群运行在性能较差的机器上
- 现存微服务架构我的项目 projectA、projectA 中有微服务 ServiceA、ServiceB、ServiceC 与 ServiceD
- 其中 ServiceD 的流量较小,其余微服务流量较大
那么此时,咱们能够将 ServiceD 微服务引流到 Shanghai 集群中去,将高性能的服务器让给其余流量较大的微服务(反之亦然,若存在某一个微服务流量特地大,咱们也能够独自为此微服务开拓一个更高性能的集群,并将该 client 的 virtual group 指向该集群,其最终目标都是保障在流量洪峰时服务的可用)
4、Seata 的预发与生产隔离
- 大多数状况下,预发环境与生产环境会应用同一套数据库。基于这个条件,预发 TC 集群与生产 TC 集群必须应用同一个数据库保障全局事务的失效(即生产 TC 集群与预发 TC 集群应用同一个 lock 表,并应用不同的 branch_table 与 global_table 的状况)
- 咱们记生产应用的 branch 表与 global 表别离为:global_table 与 branch_table;预发为 global_table_pre,branch_table_pre
- 预发与生产共用 lock_table
此时,seata-server 的 file.conf 配置如下
store {
mode = "db"
db {
datasource = "druid"
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "username"
password = "password"
minConn = 5
maxConn = 100
globalTable = "global_table" ----> 预发为 "global_table_pre"
branchTable = "branch_table" ----> 预发为 "branch_table_pre"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
}
seata-server 的 registry.conf 配置如下(以 nacos 为例)
registry {
type = "nacos"
loadBalance = "RandomLoadBalance"
loadBalanceVirtualNodes = 10
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "DEFAULT_GROUP"
namespace = "8f11aeb1-5042-461b-b88b-d47a7f7e01c0"
cluster = "pre-product" --> 同理生产为 "product"
username = "username"
password = "password"
}
}
其部署图如下所示:
不仅如此,你还能够将以上四个最佳实际根据你的理论生产状况组合搭配应用
四、api 反对
此处记录一下低级别 api 的应用
1. 近程调用事务上下文的流传
近程调用前获取以后 XID:
String xid = RootContext.getXID();
近程调用过程把 XID 也传递到服务提供方,在执行服务提供方的业务逻辑前,把 XID 绑定到以后利用的运行时:
RootContext.bind(rpcXid);
2. 事务的暂停和复原
在一个全局事务中,如果须要某些业务逻辑不在全局事务的管辖范畴内,则在调用前,把 XID 解绑:
String unbindXid = RootContext.unbind();
待相干业务逻辑执行实现,再把 XID 绑定回去,即可实现全局事务的复原:
RootContext.bind(unbindXid);
五、可能遇到的问题
1、undo_log 表 log_status= 1 的记录是做什么用的?
- 场景:分支事务 a 注册 TC 后,a 的本地事务提交前产生了全局事务回滚
- 结果:全局事务回滚胜利,a 资源被占用掉,产生了资源悬挂问题
- 防悬挂措施:a 回滚时发现回滚 undo 还未插入,则插入一条 log_status= 1 的 undo 记录,a 本地事务(业务写操作 sql 和对应 undo 为一个本地事务)提交时会因为 undo 表惟一索引抵触而提交失败。
2、如何保障事物的隔离性?
因 seata 一阶段本地事务已提交,为避免其余事务脏读脏写须要增强隔离。
- 脏读 select 语句加 for update,代理办法减少 @GlobalLock+@Transactional 或 @GlobalTransaction
- 脏写 必须应用 @GlobalTransaction
注:如果你查问的业务的接口没有 GlobalTransactional 包裹,也就是这个办法上压根没有分布式事务的需要,这时你能够在办法上标注 @GlobalLock+@Transactional 注解,并且在查问语句上加 for update。如果你查问的接口在事务链路上外层有 GlobalTransactional 注解,那么你查问的语句只有加 for update 就行。设计这个注解的起因是在没有这个注解之前,须要查问分布式事务读已提交的数据,但业务自身不须要分布式事务。若应用 GlobalTransactional 注解就会减少一些没用的额定的 rpc 开销比方 begin 返回 xid,提交事务等。GlobalLock 简化了 rpc 过程,使其做到更高的性能。
3、脏数据会滚失败如何解决?
- 脏数据需手动解决,依据日志提醒修改数据或者将对应 undo 删除(可自定义实现 FailureHandler 做邮件告诉或其余)
- 敞开回滚时 undo 镜像校验,不举荐该计划。
注:倡议事先做好隔离保障无脏数据
4、应用 AT 模式须要的注意事项有哪些?
- 必须应用代理数据源,有 3 种模式能够代理数据源:
- 依赖 seata-spring-boot-starter 时,主动代理数据源,无需额定解决。
- 依赖 seata-all 时,应用 @EnableAutoDataSourceProxy (since 1.1.0) 注解,注解参数可抉择 jdk 代理或者 cglib 代理。
- 依赖 seata-all 时,也能够手动应用 DatasourceProxy 来包装 DataSource。
- 配置 GlobalTransactionScanner,应用 seata-all 时须要手动配置,应用 seata-spring-boot-starter 时无需额定解决。
- 业务表中必须蕴含单列主键,若存在复合主键,请参考问题 13。
- 每个业务库中必须蕴含 undo_log 表,若与分库分表组件联用,分库不分表。
- 跨微服务链路的事务须要对相应 RPC 框架反对,目前 seata-all 中曾经反对:Apache Dubbo、Alibaba Dubbo、sofa-RPC、Motan、gRpc、httpClient,对于 Spring Cloud 的反对,请大家援用 spring-cloud-alibaba-seata。其余自研框架、异步模型、音讯生产事务模型请联合 API 自行反对。
- 目前 AT 模式反对的数据库有:MySQL、Oracle、PostgreSQL 和 TiDB。
- 应用注解开启分布式事务时,若默认服务 provider 端退出 consumer 端的事务,provider 可不标注注解。然而,provider 同样须要相应的依赖和配置,仅可省略注解。
- 应用注解开启分布式事务时,若要求事务回滚,必须将异样抛出到事务的发起方,被事务发起方的 @GlobalTransactional 注解感知到。provide 间接抛出异样 或 定义错误码由 consumer 判断再抛出异样。
5、AT 模式和 Spring @Transactional 注解连用时须要留神什么?
@Transactional 可与 DataSourceTransactionManager 和 JTATransactionManager 连用别离示意本地事务和 XA 分布式事务,大家罕用的是与本地事务联合。当与本地事务联合时,@Transactional 和 @GlobalTransaction 连用,@Transactional 只能位于标注在 @GlobalTransaction 的同一办法档次或者位于 @GlobalTransaction 标注办法的内层。这里分布式事务的概念要大于本地事务,若将 @Transactional 标注在外层会导致分布式事务空提交,当 @Transactional 对应的 connection 提交时会报全局事务正在提交或者全局事务的 xid 不存在。
6、SpringCloud xid 无奈传递?
1. 首先确保你引入了 spring-cloud-alibaba-seata 的依赖.
2. 如果 xid 还无奈传递, 请确认你是否实现了 WebMvcConfigurer, 如果是, 请参考 com.alibaba.cloud.seata.web.SeataHandlerInterceptorConfiguration#addInterceptors 的办法. 把 SeataHandlerInterceptor 退出到你的拦挡链路中.
7、应用 mybatis-plus 动静数据源组件后 undolog 无奈删除?
dynamic-datasource-spring-boot-starter 组件外部开启 seata 后会主动应用 DataSourceProxy 来包装 DataSource, 所以须要以下形式来放弃兼容
1. 如果你引入的是 seata-all, 请不要应用 @EnableAutoDataSourceProxy 注解.
2. 如果你引入的是 seata-spring-boot-starter 请敞开主动代理
seata:
enable-auto-data-source-proxy: false
8、数据库开启自动更新工夫戳导致脏数据无奈回滚?
因为业务提交,seata 记录以后镜像后,数据库又进行了一次工夫戳的更新,导致镜像校验不通过。
解决方案 1: 敞开数据库的工夫戳自动更新。数据的工夫戳更新,如批改、创立工夫由代码层面去保护,比方 MybatisPlus 就能做主动填充。
解决方案 2: update 语句别把没更新的字段也放入更新语句。
9、Seata 应用注册核心注册的地址有什么限度?
Seata 注册核心不能注册 0.0.0.0 或 127.0.0.1 的地址,当主动注册为上述地址时能够通过启动参数 -h 或容器环境变量 SEATA_IP 来指定。当和业务服务处于不同的网络时注册地址能够指定为 NAT_IP 或公网 IP,但须要保障注册核心的健康检查探活是通顺的。
10、seata 服务自检
首先通过读取 client.tm.degradeCheck 是否为 true, 决定是否开启自检线程. 随后读取 degradeCheckAllowTimes 和 degradeCheckPeriod, 确认阈值与自检周期. 假如 degradeCheckAllowTimes=10,degradeCheckPeriod=2000 那么每 2 秒钟会进行一个 begin,commit 的测试, 如果失败, 则记录间断失败数, 如果胜利则清空间断失败数. 间断谬误由用户接口及自检线程进行累计, 直到间断失败次数达到用户的阈值, 则敞开 Seata 分布式事务, 防止用户本身业务长时间不可用. 反之, 如果以后分布式事务敞开, 那么自检线程持续依照 2 秒一次的自检, 直到间断胜利数达到用户设置的阈值, 那么 Seata 分布式事务将复原应用
11、设置 mysql 的连贯参数 rewriteBatchedStatements=true
在 store.mode=db,因为 seata 是通过 jdbc 的 executeBatch 来批量插入全局锁的,依据 MySQL 官网的阐明,连贯参数中的 rewriteBatchedStatements 为 true 时,在执行 executeBatch,并且操作类型为 insert 时,jdbc 驱动会把对应的 SQL 优化成 insert into () values (), ()
的模式来晋升批量插入的性能。依据理论的测试,该参数设置为 true 后,对应的批量插入性能为原来的 10 倍多,因而在数据源为 MySQL 时,倡议把该参数设置为 true。
六、参考资料
1、seata faq 一些常见的问题解决
2、seata 参数配置
3、seata 事务分组介绍
4、seata 事务分组的例子
5、可反对的 SQL 限度
6、seata 部署指南