的一些概念注:本文大部分内容摘自seata官网。写这篇文章的目的是总结seata官网的部分内容,以备日后查阅。一、什么是seataSeata是一个开源的分布式事务解决方案,致力于提供高性能、易用的分布式事务服务。Seata将为用户提供AT、TCC、SAGA和XA交易模式,为用户打造一站式的分布式解决方案。其中AT模式是Seata主推的模式,基于两阶段提交实现。术语:TC(TransactionCoordinator)事务协调器维护全局和分支事务的状态,驱动全局事务的提交或回滚。TM(TransactionManager)——事务管理器定义了一个全局事务的范围:启动一个全局事务,提交或回滚一个全局事务。RM(ResourceManager)-资源管理器管理分支事务的资源,与TC对话以注册分支事务并报告分支事务的状态,并驱动分支事务提交或回滚。2、AT模式介绍AT模式需要保证每个业务数据库都有一个undo_log表,里面存放的是业务数据执行前后的镜像数据。1、前提是基于支持本地ACID事务的关系型数据库。Java应用程序,通过JDBC访问数据库。2.整体机制演进两阶段提交协议:第一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。阶段2:提交是异步的并且完成得非常快。回滚通过单阶段回滚日志进行反向补偿。三、读写隔离的实现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.读隔离基于ReadCommitted(已提交读)或以上的数据库本地事务隔离级别,Seata(AT模式)默认的全局隔离级别是未提交读(ReadUncommitted)。如果应用在特定场景下使用,则必须要求全局读提交。目前的Seata方法是通过SELECTFORUPDATE语句的代理。SELECTFORUPDATE语句的执行会申请一个全局锁。如果全局锁被另一个事务持有,则释放本地锁(回滚本地执行SELECTFORUPDATE语句)并重试。在这个过程中,查询会一直阻塞,直到获取到全局锁,也就是提交了读取的相关数据,才不会返回。出于整体性能的考虑,Seata目前的方案并没有代理所有的SELECT语句,只代理FORUPDATESELECT语句。三、交易分组1、什么是交易分组?事务分组是seata的资源逻辑,类似于服务实例。file.conf中的my_test_tx_group是一个事务分组。可以将不同的微服务注册到不同的组。2、如何通过事务分组找到后端集群?首先,程序配置事务分组(GlobalTransactionScanner构造方法的txServiceGroup参数)。程序会通过用户配置的配置中心搜索service.vgroupMapping.[事务分组配置项],从配置项中获取的值为TC集群的名称。获取集群名程序通过一定的后缀+集群名构造服务名,每个配置中心的服务名都不一样。获取服务名去对应的注册中心拉取对应服务名的服务列表,获取后台真实的TC服务列表3、为什么要这样设计,而不是直接拿服务名?还有一个额外的层来获取事务分组的配置以映射集群。经过这样的设计,事务组就可以作为资源的逻辑隔离单元。当某个集群出现故障时,可以快速故障转移,只切换对应的组,可以将故障降低到服务级别,但前提是你有足够的服务器集群。4.事物分组示例1.TC异地多机房容灾假设TC集群部署在两个机房:广州机房(主)和上海机房(备),各有两个实例。一套完整的微服务架构项目:projectA和projectA有Services:serviceA、serviceB、serviceC和serviceD其中projectA的所有微服务的事务组tx-transaction-group设置为:projectA,projectA正常使用TC集群(master)ofguangzhou,那么一般情况下客户端的配置如下Show:seata.tx-service-group=projectAseata.service.vgroup-mapping.projectA=Guangzhou如果此时guangzhou集群组完全宕机时间,或者projectA由于网络原因暂时无法与广州机房互通,那么我们将配置广州集群的分组改为上海,如下:seata.service.vgroup-mapping.projectA=Shanghai并推送到每个微服务,从而完成整个项目一个项目的TC集群的动态切换。2.单一环境多应用接入假设在开发环境(或预发布/生产环境)有一套完整的seata集群,seata集群服务于不同的微服务架构项目projectA、projectB、projectC、projectA、projectB、和projectC是相对独立的。将seata集群中的6个实例成对分组,分别为projectA、projectB、projectC服务,那么seata-server的配置如下(以nacos注册中心为例):registry{type="nacos"loadBalance="RandomLoadBalance"loadBalanceVirtualNodes=10nacos{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-groupcluster="project-a-group"username="username"password="password"}}客户端配置如下:seata.tx-service-group=projectA#同理,projectB和projectC配置project-b-group/project-c-groupseata.service.vgroup-mapping.projectA=project-a-group配置启动后,事务组对应的TC为其服务单独申请。整体部署图如下:3.客户端精细化控制假设现在有一个seata集群,广州机房实例运行在性能较高的机器上,上海集群运行在性能较差的机器上。现有的微服务架构项目projectA和projectA有微服务ServiceA、ServiceB、ServiceC和ServiceD,其中ServiceD的流量较小,其他微服务的流量较大。所以此时,我们可以将ServiceD微服务引流到上海集群,将高性能的服务器交给其他流量大的微服务(反之亦然,如果有某个微服务流量特别大,我们也可以将其分离出来为这个微服务创建一个更高性能的集群,并将客户端的虚拟组指向集群,最终目的是保证流量高峰时服务的可用性)4.Seata的预发布和生产隔离在大多数情况下,预发布环境和生产环境会使用同一套数据库基于这个条件,预发布TC集群和生产TC集群必须使用同一个数据库来保证全局事务的有效性(即生产TC集群和预发布TC集群使用相同的锁表并使用不同的branch_table和global_table)。我们记录生产中使用的分支表和全局表分别是:global_table和branch_table;pre-release为global_table_pre,branch_table_prepre-release和production共享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="用户名"password="密码"minConn=5maxConn=100globalTable="global_table"---->pre-issueedas"global_table_pre"branchTable="branch_table"---->pre-issuedas"branch_table_pre"lockTable="lock_table"queryLimit=100maxWait=5000}}seata-serverregistry.conf配置如下如下(以nacos为例)registry{type="nacos"loadBalance="RandomLoadBalance"loadBalanceVirtualNodes=10nacos{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"}}其部署图如下:不仅如此,还可以结合以上四种bestPractice4.API支持这里记录下底层API的使用1.传播远程调用事务上下文远程调用前获取当前XID:Stringxid=RootContext.getXID();远程调用过程传递XID到服务提供者,并在执行服务提供者的业务逻辑之前将XID绑定到当前应用程序的运行时:RootContext.bind(rpcXid);2.在全局事务中暂停和恢复事务,如果某些业务逻辑需要不在全局事务的管辖范围内,在调用前解绑XID:StringunbindXid=RootContext.unbind();相关业务逻辑执行完成后,绑定回XID,实现全局事务恢复:RootContext.bind(解绑Xid);五、可能遇到的问题1、undo_log表中log_status=1的记录是什么?场景:分支事务a注册TC后,a的本地事务未提交全局事务回滚后果:全局事务回滚成功,资源被占用,出现资源挂起问题。反挂措施:a回滚时,发现回滚undo还没有插入,插入了一条log_status=1的undo记录。一个本地事务(业务写操作sql和对应的undo是一个本地事务)在提交的时候会因为undo表的唯一索引冲突导致提交失败2.如何保证事物的隔离性?由于在seata的第一阶段已经提交了本地事务,为了防止其他事务的脏读和脏写需要增强隔离。脏读select语句添加更新,代理方法添加@GlobalLock+@Transactional或@GlobalTransaction。脏写必须使用@GlobalTransaction注:如果你查询的业务的接口没有GlobalTransactional包,即在这个方法上根本不需要分布式事务。这时候可以在方法上打上@GlobalLock+@Transactional注解,在查询语句中添加forupdate。如果你查询的接口在事务链接的外层有GlobalTransactional注解,那么你只需要在你查询的语句中添加forupdate即可。之所以设计这个注解是因为在这个注解之前需要查询分布式事务来读取提交的数据,但是业务本身不需要分布式事务。如果使用GlobalTransactional注解,会增加一些无用的额外rpc开销,比如begin返回xid,提交事务等。GlobalLock简化了rpc过程,以达到更高的性能。3.dirtydataroll失败如何处理?脏数据需要人工处理。根据日志提示修改数据或删除对应的undo(可以自定义FailureHandler用于邮件通知等),回滚时关闭undo镜像验证。不建议使用此解决方案。注意:建议提前做好隔离,保证没有脏数据4、使用AT模式有哪些注意事项?必须使用代理数据源,代理数据源有3种方式:依赖seata-spring-boot-starter时,代理数据源自动代理,无需额外处理。依赖seata-all时,使用@EnableAutoDataSourceProxy(since1.1.0)注解,注解参数可以选择jdk代理或者cglib代理。依赖seata-all时,也可以手动使用DatasourceProxy封装DataSource。配置GlobalTransactionScanner,使用seata-all时需要手动配置,使用seata-spring-boot-starter时不需要额外处理。业务表必须包含单列主键。如果有复合主键,请参考问题13。每个业务库必须包含一个undo_log表。如果与分库分表组件配合使用,则分库不分表。跨微服务链路的事务需要支持相应的RPC框架,seata-all目前支持的有:ApacheDubbo、AlibabaDubbo、sofa-RPC、Motan、gRpc、httpClient,SpringCloud支持请参考spring-cloud-alibaba-seata。其他自研框架、异步模型、消息消费事务模型请自行组合API支持。目前AT模式支持的数据库有:MySQL、Oracle、PostgreSQL、TiDB。在使用注解开启分布式事务时,如果默认的服务提供者端加入了事务的消费者端,则提供者不需要标注注解。但是provider也需要相应的依赖和配置,注解只能省略。在使用注解启动分布式事务时,如果需要回滚事务,则必须向事务的发起者抛出异常,由事务发起者的@GlobalTransactional注解感知。Provide直接抛出异常或者定义一个错误码,让消费者在抛出异常前判断。5、Spring@Transactional注解使用AT模式需要注意什么?@Transactional可以与DataSourceTransactionManager和JTATransactionManager结合使用,分别表示本地事务和XA分布式事务。它通常用于与本地交易相结合。与本地事务结合时,@Transactional和@GlobalTransaction一起使用。@Transactional只能位于与@GlobalTransaction标记相同的方法层或位于@GlobalTransaction标记方法的内层。这里分布式事务的概念大于本地事务。如果在外层标记@Transactional,则分布式事务将被提交为空。当提交@Transactional对应的connection时,会报全局事务正在提交或者全局事务的xid不存在。6、SpringCloudxid不能下发?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:false8.脏数据无法回滚是因为数据库自动更新时间戳?由于业务提交,seata记录当前镜像后,数据库再次更新了时间戳,导致镜像验证失败。解决方案一:关闭数据库的时间戳自动更新。数据的时间戳更新,比如修改和创建时间,是在代码层面维护的。比如MybatisPlus可以做自动填充。方案二:update语句不把没有更新的字段放到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,那么每次Abeginandcommittest都会在2秒内进行一次。如果失败,则记录连续失败的次数。如果成功,则清除连续失败的次数。连续的错误由用户界面和自检线程累积,直到连续失败的次数达到用户的阈值。然后关闭Seata分布式事务,避免用户自己的业务长时间不可用。反之,如果当前分布式事务关闭,自检线程会继续每2秒自检一次,直到连续成功次数达到用户设置的阈值,此时Seata分布式事务将恢复使用11。在store.mode=db中设置mysql的连接参数rewriteBatchedStatements=true,因为seata是通过jdbc的executeBatch批量插入全局锁的,按照mysql官网的说法,连接参数中的rewriteBatchedStatements为true时执行executeBatch且操作类型为insert时,jdbc驱动会将相应的SQL优化成insertinto()values(),()的形式,提高批量插入的性能。根据实际测试,该参数设置为true后,对应的批量插入性能是原来的10倍以上。因此,当数据源为MySQL时,建议将该参数设置为true。6.参考资料1.seatafaq一些常见问题解决2.seata参数配置3.seata事务分组介绍4.seata事务分组示例5.支持的SQL限制6.seata部署指南
