转载本文请联系程序员内电史公众号。好久没发帖了,最近实在是太忙了。当爹的第43天,身心俱疲。这是年底,公司按照KPI要求技术部加班到10点,孩子晚上两三个小时就醒一次,基本没机会睡觉。皮疹总是很可怕。写文章有限的时间被无限瓜分了,哎~上班族真的好辛苦。小眼睛的意思不知道写什么,刚好手头有个新项目,尝试用阿里的Seata中间件做分布式事务,就来做个实战分享吧!在介绍Seata之前,让我们简单回顾一下分布式事务概念的基础知识。对于分布式事务的产生,我们先看看百度上分布式事务的定义:分布式事务是指事务的参与者、支持事务的服务器、资源服务器、事务管理器位于不同的节点之间。不同分布式系统的节点。优越的。呃~有点抽象。让我们画一个简单的图来理解它。以订单减库存扣余额为例:当系统规模较小时,单体架构完全可以满足现有业务需求,所有业务共享一个数据库,整个订单流程可能只需要操作同一个事务下的数据库用一种方法。此时很容易提交所有操作或回滚所有操作。随着业务量的不断增长,单一的架构逐渐无法应对庞大的流量。这时候就需要对数据库和表进行数据库和表的处理,拆分应用SOA服务。还创建了订单中心、用户中心、库存中心等,由此产生的问题是业务相互隔离,每个业务维护自己的数据库,数据交换只能通过RPC调用来完成。当用户再次下单时,需要同时对订单库、库存库存储、用户库账号进行操作,但此时我们只能保证自己本地数据的一致性,并且无法保证调用其他服务的操作是否成功,所以为了保证整个下单过程的数据一致性,需要分布式事务的介入。Seata优势实现分布式事务的方案有很多,比如基于XA协议的2PC和3PC,基于业务层的TCC,应用消息队列+消息表实现的最终一致性方案,还有我要说的Seata关于今天的中间件,我们来看看每种方案的优缺点。2PC是基于XA协议实现的分布式事务。XA协议分为两部分:事务管理器和本地资源管理器。其中,本地资源管理器往往由数据库实现,如Oracle、MYSQL等,都实现了XA接口,事务管理器作为全局调度器。两阶段提交(2PC)对业务的侵入性很小。它最大的优点是对用户透明。用户可以像本地事务一样使用基于XA协议的分布式事务,可以严格保证事务的ACID特性。不过2PC的劣势也很明显。是一种强一致性同步阻塞协议,在事务执行过程中需要锁定所有需要的资源,俗称刚性事务。所以比较适合有一定执行时间的短事务,整体性能比较差。一旦事务协调器宕机或网络抖动,参与者将一直处于资源锁定状态或只有部分参与者提交成功,导致数据不一致。因此,在高并发性能至上的场景下,基于XA协议的分布式事务并不是最佳选择。3PC三阶段提交(3PC)是两阶段提交(2PC)的改进版本。为了解决两阶段提交协议的阻塞问题,上文提到的两阶段提交,当协调器崩溃时,参与者无法做出最终选择,资源将一直处于阻塞和锁定状态。在2PC中,只有协调器有超时机制。在3PC中,协调者和参与者都有超时机制。协调者失效后,参与者不会一直阻塞。而且在第一阶段和第二阶段之间插入了一个准备阶段(如下图,看起来有点冗长),保证所有参与节点的状态在最终提交阶段之前是一致的。3PC虽然通过超时机制解决了协调器失效后参与者的阻塞问题,但是同时多了一次网络通信,性能变差了,不推荐。TCC所谓的TCC编程模式也是两阶段提交的一种变体。不同的是TCC在业务层编写代码实现了两阶段提交。TCC分别指Try、Confirm、Cancel。一个业务操作应该对应的写这三个方法。以下单项扣减存货为例。在Try阶段,库存被拿走,在Confirm阶段,库存被实际扣除。如果库存扣减失败,Cancel阶段回滚释放库存。TCC不存在资源阻塞的问题,因为每个方法都是直接提交事务,一旦出现异常,就会使用Cancel进行回滚补偿,也就是常说的补偿事务。本来只有一种方法,现在需要三种方法来支撑。可以看出TCC对业务的侵入性很强,这种模式不能很好的复用,会导致开发量激增。考虑到网络波动等原因,会有重试机制来保证请求送达,所以考虑了接口的幂等性。消息事务(最终一致性)消息事务实际上是基于消息中间件的两阶段提交,将本地事务和发送消息放在同一个事务中,保证本地操作和发送消息同时成功。订单扣库存示意图:订单系统向MQ发送预扣库存消息,MQ保存预扣消息并返回成功ACK。订单系统收到前置消息并执行成功ACK后,执行本地订单操作。为了防止消息发送成功,本地交易失败,订单系统会实现MQ的回调接口,不断检测本地交易是否执行成功。如果失败,它将回滚准备消息;如果成功,它将对消息进行最终提交。库存系统消费库存扣除消息并执行本地交易。若扣分失败,将重发该讯息。一旦超过重试次数,本地表会持久化失败信息,并启动定时任务进行补偿。基于消息中间件的两阶段提交方案通常用于高并发场景,以牺牲数据的强一致性换取性能的大幅提升,但这种方式实现的成本和复杂度都比较高,具体要看实际情况情况商业状况。SeataSeata也是由两阶段提交演化而来的分布式事务解决方案,提供AT、TCC、SAGA、XA等事务模式。在这里,我们将重点介绍AT模式。由于Seata分两个阶段提交,让我们看看它在每个阶段做了什么。下面以订单扣除库存和扣除余额为例。先介绍一下Seata分布式事务的几个角色:TransactionCoordinator(TC):全局事务协调器,用于协调全局事务和各个分支事务(不同服务)的状态,驱动全局事务和各个分支事务的回滚或提交。TransactionManager?:事务管理器,用于业务层开启/提交/回滚一个完整的事务(在调用服务的方法中开启带注解的事务)。ResourceManager(RM):资源管理器,一般指业务数据库代表一个分支事务(BranchTransaction),管理分支事务并配合TC注册分支事务并报告分支事务的状态,驱动分支事务的提交或回滚。Seata实现了分布式事务,设计了一个关键角色UNDO_LOG(回滚日志记录表)。我们在每个应用分布式事务的业务库中创建这张表。这张表的核心功能是存储更新前后的业务数据数据库的数据镜像组织成一个回滚日志,备份在UNDO_LOG表中,以便随时回滚业务异常。比如第一阶段:下面我们更新user表的name字段。updateusersetname='小夫最帅'wherename='程序员里面的东西'首先,seata的JDBC数据源代理解析业务SQL,提取SQL的元数据,即获取SQL类型(UPDATE),表(user)、条件(wherename='somethingintheprogrammer')等相关信息。第一阶段流程图首先查询镜像前的数据,根据分析得到的条件信息生成查询语句定位一条数据。selectnamefromuserwherename='somethinginsidetheprogrammer'前镜像数据然后执行业务SQL,根据前镜像数据的主键查询后镜像数据。业务数据的更新日志和回滚日志在同一个本地事务中提交,分别插入到业务表和UNDO_LOG表中。回滚记录数据格式如下:包括afterImagebeforeimage,beforeImageafterimage,branchIdbranchtransactionID,xidglobaltransactionID{"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注册分支(BranchId),并为要修改的记录申请全局锁。要锁定此数据,请使用SELECTFORUPDATE语句。而如果一直获取不到锁,就需要回滚本地事务。TM启动事务后,会生成一个全局唯一的XID,在被调用的服务之间传递。通过这样的机制,本地事务分支(BranchTransaction)可以在全局事务的第一阶段被提交,并立即释放本地事务锁定的资源。与传统的XA事务在第二阶段释放资源相比,Seata减少了锁定范围以提高效率。即使第二阶段出现异常需要回滚,也能快速从UNDO_LOG表中找到对应的回滚数据,反向解析成SQL。达到回滚补偿。最后提交本地事务,将业务数据的更新连同之前生成的UNDOLOG数据一起提交,并将本地事务提交的结果上报给全局事务协调器TC。第二阶段第二阶段是根据各分支的决议提交或回滚:如果决议是全局提交,此时各分支的事务已经提交并成功,全局事务协调器(TC)将发送第二阶段请求。收到TC的分支提交请求后,将请求放入一个异步任务队列,提交成功的结果会立即返回给TC。异步队列会根据BranchID异步批量查找并删除对应的UNDOLOG回滚记录。如果决定是全局回滚,这个过程比全局提交要麻烦一点。RM服务器收到TC全局协调器的回滚请求,通过XID和BranchID找到对应的回滚日志记录,通过回滚记录生成反向日志记录。并执行updateSQL,完成分支的回滚。注意:这里删除回滚日志记录的操作必须在本地业务事务执行之后。上面说了几种分布式事务的优缺点。下面我们在分布式事务中间实践一下Seata来感受一下。Seata实践Seata是一个需要独立部署的中间件,所以先拿SeataServer来说,这里以最新的seata-server-1.4.0版本为例,下载地址:https://seata.io/en-us/blog/download.html解压后,我们只需要关心\seata\conf目录下的file.conf和registry.conf文件即可。SeataServerfile.conffile.conf文件用于配置持久化事务日志的模式,目前提供三种方式:file、db、redis。file.conf文件配置注意事项:选择db方式后,需要在对应的数据库中创建三张表:globalTable(持久化全局事务)、branchTable(持久化各分支提交的事务)、lockTable(持久化各分支提交的事务)锁定的资源事务)。--thetabletostoreGlobalSessiondata--保持长期化全局事务CREATETABLEIFNOTEXISTS`global_table`(`xid`VARCHAR(128)NOTNULL,`transaction_id`BIGINT,`status`TINYINTNOTNULL,`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,PRIMARYKEY(`xid`),KEY`idx_gmt_modified_status`(`gmt??_modified`,`status`),KEY`idx_transaction_id`(`transaction_id`))ENGINE=InnoDBDEFAULTCHARSET=utf8;--thetabletostoreBranchSessiondata--持久化各提交分支的事CREATETABLEIFNOTEXISTS`branch_table`(`branch_id`BIGINTNOTNULL,`xid`(128VARCHAR)NOTNULL,`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),PRIMARYKEY(`branch_id`),KEY`idx_xid`(`xid`))ENGINE=InnoDBDEFAULTCHARSET=utf8;--thetabletostorelockdata--持久化每个分支锁表事务CREATETABLEIFNOTEXISTS`lock_table`(`row_key`VARCHAR(128)NOTNULL,`xid`VARCHAR(96),`transaction_id`BIGINT,`branch_id`BIGINTNOTNULL,`resource_id`VARCHAR(256),`table_name`VARCHAR(32),`pk`VARCHAR(36),`gmt_create`DATETIME,`gmt_modified`DATETIME,PRIMARYKEY(`row_key`),KEY`idx_branch_id`(`branch_id`))ENGINE=InnoDBDEFAULTCHARSET=utf8;registry.confregistry.conf文件设置注册中心和配置中心:目前注册中心支持nacos、eureka、redis、zk、consul、etcd3、sofa七种,这里我使用eureka作为注册中心;配置中心支持nacos、apollo、zk、consul、etcd3五种方式registry.conf文件配置配置完成后,启动\seata\bin目录下的seata-server,seata服务器就搭建完成了。SeataClientSeataServer环境搭建完成后,我们新建三个服务:order-server(订单服务)、storage-server(库存扣除服务)、account-server(账户金额服务),分别注册到尤里卡分别。各个服务的大致核心配置如下:spring:application:name:storage-servercloud:alibaba:seata:tx-service-group:my_test_tx_groupdatasource:driver-class-name:com.mysql.jdbc.Driverurl:jdbc:mysql://47.93.6.1:3306/seat-storageusername:rootpassword:root#eurekaregistryeureka:client:serviceUrl:defaultZone:http://${eureka.instance.hostname}:8761/eureka/instance:hostname:47.93。6.5prefer-ip-address:true业务大体流程:用户发起订单请求,本地订单订单服务创建订单记录,通过RPC远程调用入库扣库存服务和扣账账户余额服务。只有三个服务同时执行成功。这是一个完整的订单流程。如果一个服务执行失败,所有其他服务都会回滚。Seata对业务代码的侵入性很强,在代码中使用@GlobalTransactional注解开启一个全局事务即可。@Override@GlobalTransactional(name="create-order",rollbackFor=Exception.class)publicvoidcreate(Orderorder){Stringxid=RootContext.getXID();LOGGER.info("-------->交易开始");//本地方法orderDao.create(order);//远程方法减少库存storageApi.decrease(order.getProductId(),order.getCount());//远程方法减少账户余额LOGGER.info("------->扣款账户顺序开始");accountApi.decrease(order.getUserId(),order.getMoney());LOGGER.info("-------->扣款账户结束顺序中");LOGGER.info("------>transactionend");LOGGER.info("全局事务xid:{}",xid);}前面提到了SeataAT模式实现了分布式事务,需要在相关业务数据库中创建undo_log表,用于存放数据回滚日志。表结构如下:--forATmodeyoumusttoinitthissqlforyoubusinessdatabase.theseataservernotneedit.CREATETABLEIFNOTEXISTS`undo_log`(`id`BIGINT(20)NOTNULLAUTO_INCREMENTCOMMENT'incrementid',`branch_id`BIGINT(20)NOTNULLCOMMENT'branchtransactionid',`xid`VARCHAR(100)NOTNULLCOMMENT'globaltransactionid',`context`VARCHAR(128)NOTNULLCOMMENT'undo_logcontext,suchasserialization',`rollback_info`LONGBLOBNOTNULLCOMMENT'rollbackinfo',`log_status`INT(11)NOTNULLCOMMENT'0:normalstatus,1:defensestatus',`log_created`DATETIMENOTNULLCOMMENT'createdatetime',`log_modified`DATETIMENOTNULLCOMMENT'modifydatetime',PRIMARYKEYIYUN(`id`)ux_undo_log`(`xid`,`branch_id`))ENGINE=InnoDBAUTO_INCREMENT=1DEFAULTCHARSET=utf8COMMENT='ATtransactionmodeundotable';本次环境搭建工作结束,完整案例稍后贴出GitHub地址,这里不再占用篇幅在Seata项目中测试服务调用流程如下图所示:服务调用后流程启动各个服务,我们直接请求订单接口看效果。只要订单order表的记录创建成功,storageinventory表中使用的字段数就会增加,accountbalance表中的used字段也会增加。如果数量增加,则表示订单处理成功。原始数据请求后,转发过程没有问题。数据符合预期。订单数据被放置,TM事务管理器order-server服务的控制台也打印出两阶段提交的日志。控制台提交两次。然后再看,如果其中一个服务异常,会不会正常回滚?在account-server服务中模拟超时异常,看能否实现全局事务回滚。全局事务回滚发现没有一条数据执行成功,说明全局事务回滚也成功了。然后查看undo_log回滚记录表的变化。由于Seata删除回滚日志的速度非常快,所以要查看表中的undo_log。滚动日志必须在某项服务上打断点才能看得更清楚。回滚记录总结以上简单介绍了5种分布式事务方案,2PC、3PC、TCC、MQ、Seata,同时对Seata中间件进行了详细的实践。但是无论我们选择哪种解决方案,在项目中应用时都必须谨慎小心。除了特定的数据强一致性场景,我们尽量不要使用它,因为无论它们的性能多么优越,一旦项目实现了分布式事务,整体效率就会下降数倍,尤其是劣势在高并发情况。本案例github地址:https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot-seata-transaction
