留神:此篇文章大部分内容都是摘抄自 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=projectAseata.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-groupseata.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部署指南