SpringCloud进阶--Seata与分布式事务
Seata与分布式事务先回顾下数据库事务吧原子性一个事务中的所有操作要么全部完成要么全部不完成不会结束在中间某个环节。事务在执行过程中发生错误会被回滚到事务开始前的状态就像这个事务从来没有执行过一样。一致性事务开始之前和事务结束之后数据的完整性没有被破坏。隔离性数据库允许多个并发事务同时对其数据进行读写操作隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别包括读未提交、读已提交、可重复读和串行化。持久性事务处理结束后对数据的修改就是永久的即使系统故障也不会丢失。在分布式环境下可能会出现这样的问题比如下单购物先调用库存服务减掉库存数量-》订单服务开始下单-》账户服务进行扣款。如果订单服务出现问题就会导致库存扣了但是没有生成订单用户也没付款导致货物丢失因此必须实现分布式事务SPring Cloud Alibaba提供了分布式事务组件SeataSeata时一款开源的分布式事务解决方案Seata将为用户提供AT、TCC、SAGA、XA事务模式。实际上就是多了一个中间人TC来协调所有服务的事务。分布式事务的解决方案XA分布式事务协议-2PC两阶段提交实现这里的PC指的时Prepare和Commit也就是说它分为两个阶段一个时准备阶段一个时提交阶段整个过程的参与者一共有两个角色一个是事务的执行者一个是事务的协调者实际上整个分布式事务的运作都需要依靠协调者来维持准备阶段一个分布式事务是由协调者来开启的首先协调者会向所有的事务执行者发送事务内容等待所有的事务执行者答复各个事务执行者开始执行事务操作但不提交并将undo和redo信息记录到事务日志中如果事务执行者执行事务成功那么告诉协调者yes否则告诉协调者失败No,不能提交事务。提交阶段所有执行者都反馈完成后进入第二阶段协调者检查各个执行者反馈的内容如果都反馈成功那么告诉所有执行者可以提交事务了最后再释放锁资源如果至少有一个执行者返回失败或者超时那么就让所有执行者回滚分布式事务执行失败。这种方式看起来比较简单但是存在以下几个问题事务协调者是核心角色一旦出现问题整个分布式事务都不能正常运行。如果提交阶段发生网络问题导致某些事务执行者没有收到协调者发来的提交命令。将导致这些执行者有些提交有些没提交这样会发生错误XA分布式事务协议-3PC三阶段提交实现三阶段提交是在二阶段提交基础上的改进版本主要加入了超时机制同时在协调者和执行者中都引入了超时机制三个阶段分别为CanCommit阶段协调者向执行者发送CanCommit请求询问是否可以执行事务提交操作然后等待执行者响应执行者接收到请求后如果其自身认为可以顺利执行事务则返回yes并进入预备状态否则返回NoPreCommit阶段协调者根据执行者的反应情况来决定是否可以进入第二阶段PreCommit 如果所有执行者都返回yes则协调者向所有执行者发送PreCommit请求并进入PreCommit阶段执行者收到请求后会执行事务操作。并将undo和redo信息记录到事务日志中如果执行成功则返回成功响应 如果至少有一个执行者返回No,则协调者向所有执行者发送abort请求所有执行者在收到请求或超过一段时间没收到任何请求时会直接中断事务。DoCommit阶段该阶段进行真正的事务提交。协调者收到所有执行者发送的成功响应那么就进入DoCommit状态并向所有执行者发送DoCommit请求执行者收到DoCommit请求后开始提交事务并在完成事务提交后释放所有事务资源并向协调者发送确认响应协调者接收到所有执行者的确认之后完成事务。如果因为网络问题没收到DoCommit请求执行者会在超时之后直接提交事务虽然执行者只是猜测协调者返回的是DoCommit请求但是因为前面的阶段都正常执行所有能够在一定程度上认为本次事务是成功的因此会直接提交协调者只要接收到一个失败的响应或者响应超时就会执行中断事务协调者向所有执行者发送abort请求执行者收到abort请求后利用其在PreCommit阶段记录的undo信息来执行事务回滚在回滚完成后释放所有的事务资源并向协调者发送确认信息协调者接收到反馈后执行事务中断。相比两阶段提交三阶段的优势显而易见但是也有缺点3PC在2PC的第一阶段和第二阶段中插入一个准备阶段保证了在最后提交阶段之前各参与节点的状态一致。一旦执行者无法及时收到协调者的提交信息会默认执行commit。这样就不会因为协调者单方面故障导致全局出现问题但是超时之后的commit决策本质上是一个赌注如果此时协调者发送的是abort请求但是超时未接收那么会导致数据一致性问题TCC补偿事务TCC(Try Confirm Cancel)对业务有侵入性一共分为三个阶段Try阶段比如在借书时将书籍库存-1用户剩余借阅量-1这个操作除了直接对库存和剩余借阅量进行修改之外还要将减去的值单独存放到冻结表中但是此时不会创建借阅信息它只是预先把关键的信息处理了预留业务资源出来。Confirm阶段如果Try阶段成功则进入Confirm阶段。此时开始创建借阅信息只能使用Try阶预留的业务资源如果创建成功那么就对Try阶段冻结的值进行解冻这个流程就执行完了如果失败了就进入Cancel阶段Cancel阶段此时把冻结的值返还回去这个借阅没有成功跟XA协议相比TCC没有协调者角色而是自主执行上一阶段的执行情况来确保正常充分利用了集群优势性能有很大提升。但是缺点也很明显它与业务有关联性需要开发者编写更多的补偿代码并不适合所有的业务流程Seata 机制RM(Resource Manager)资源管理器用于直接执行本地事务的提交和回滚。管理分支事务处理的资源与TC交谈以注册分支事务和报告分支事务的状态并驱动分支事务提交或回滚。TMTransaction Manager事务管理器分布式事务的核心管理者。比如现在需要在借阅服务中开启全局事务来让自身、图书服务、用户服务都参与进来也就是说TM一般是全局事务发起者。定义全局事务的范围开始全局事务、提交或回滚全局事务。TC(Transaction Coordinator)事务协调者Seata服务器用于全局控制比如在XA模式下就是一个协调者角色而一个分布式事务的启动就是由TM向TC发起请求TC再来与其他RM进行协调操作。维护全局和分支事务的状态驱动全局事务提交或回滚。TM请求TC开启一个全局事务----TC会生成一个XID作为该全局事务的编号XID会在服务调用链路中传播保证将多个微服务的子事务关联在一起RM请求TC将本地事务注册未全局事务的分支事务通过全局事务的XID进行关联TM询问TCXID对应的全局事务应该提交还是回滚TC驱动RM将XID对应的本地事务进行提交或回滚。Seata支持4种事务模式AT本质上是2PC的升级版在AT模式下用户只关心自己的“业务sql”一阶段Seata 会拦截 业务SQL解析SQL 语义查询 “业务SQL” 要更新的业务数据在业务数据被更新前将其保存成 “before image”执行 “业务SQL” 更新业务数据查询更新后的数据将其保存成 “after image”将 before image 和 after image 保存至 Undo Log 表中生成行锁以上操作全部在一个数据库事务内完成这样保证了一阶段操作的原子性。二阶段提交因为 “业务SQL” 在一阶段已经提交至数据库所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉完成数据清理即可。二阶段回滚首先要校验脏写对比“数据库当前业务数据”和 “after image”如果两份数据完全一致就说明没有脏写可以还原业务数据。如果不一致就说明有脏写出现脏写就需要转人工处理。用“before image”还原业务数据删除快照数据和行锁TCC同上XA同上但是要求数据库本身支持这种模式才可以Saga用于处理长事务每个执行者需要实现事务的正向操作和补偿操作以AT模式为例如何才能做到不侵入业务的情况下完成分布式事务Seata客户端是通过对数据源进行代理实现的使用的是DataSourceProxy类。我们只需要将对应的代理类注册为Bean即可。使用file模式部署先下载Seata下载地址Releases · apache/incubator-seata · GitHub。Seata支持本地部署和基于注册中心部署比如Nacos、Eureka这里以本地部署为例不需要对Seata的配置文件做任何修改。Seata存在事务分组机制:事务分组seata的资源逻辑可以按照微服务需要在应用程序客户端对事务进行分组并对分组命名集群Seata-Server服务端一个或多个节点组成的集群cluster。应用程序客户端需要指定事务分组和Seata服务端集群默认default的映射关系。为什么要通过事务分组映射到集群为什么不直接指定集群呢这样设计后事务分组可以作为资源的逻辑隔离单位集群出现故障时可以快速failover只切换对应分组就可以把故障减到服务级别前提是有足够多的server集群节点。为各个微服务引入依赖dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-seata/artifactId /dependency添加配置seata: service: vgroup-mapping: #对事务组做映射默认分组为应用名称-seata-service-group将其映射到default集群 borrow-service-seata-service-group: default grouplist: default: localhost:8868然后启动服务这时只是单纯的连接上Seata。但是没开启分布式事务。要开启分布式事务。首先在启动类加注解EnableAutoDataSourceProxy。此注解会添加一个后置处理器将数据源封装为支持分布式事务的代理源SpringBootApplication EnableAutoDataSourceProxy public class AppBorrow { public static void main( String[] args ) { SpringApplication.run(AppBorrow.class, args); } }然后在业务方法上添加GlobalTransactional注解开启分布式事务GlobalTransactional Override public Boolean doBorrow(int uid, int bid) { //1.判断图书和用户是否都可以借阅 if (bookClient.bookRemain(bid)1){ throw new RuntimeException(图书数量不足); } if (userClient.userRemain(uid)1){ throw new RuntimeException(用户剩余借阅量不足); } // 2. 先将图书数量-1 if (!bookClient.bookBorrow(bid)){ throw new RuntimeException(借阅图书时出错); } // 3. 添加借阅信息 if (borrowMapper.getBorrow(uid,bid)!null){ throw new RuntimeException(此书籍已被此用户借阅了); } if (borrowMapper.addBorrow(uid,bid)0){ throw new RuntimeException(录入借阅信息时出错); } // 4. 用户可借阅-1 if (!userClient.userBorrow(uid)){ throw new RuntimeException(借阅时出现错误); } // 5.完成 return true; }由于Seata会分析修改数据的sql同时生成对应的反向回滚sql。这个回滚记录会存放在undo_log表。所以要求每一个client都有一个undo_log表每个服务连接的数据库都要创建这样一个表由于我的三个服务都用的同一个数据库所以只用在这个数据库创建一个undo_log表。DROP TABLE IF EXISTS undo_log; CREATE TABLE undo_log ( id bigint(20) NOT NULL AUTO_INCREMENT, branch_id bigint(20) NOT NULL, xid varchar(100) NOT NULL, context varchar(128) NOT NULL, rollback_info longblob NOT NULL, log_status int(11) NOT NULL, log_created datetime NOT NULL, log_modified datetime NOT NULL, ext varchar(100) default null, PRIMARY KEY (id), UNIQUE KEY ux_undo_log (xid,branch_id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8;此时就配置完成了使用nacos模式部署先单独为Seata配置一个命名空间修改Seata 中的/conf/registry.conf文件registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type nacos nacos { application seata-server serverAddr 127.0.0.1:8848 group SEATA_GROUP # 使用我们刚创建的命名空间 namespace 74cc7e01-083a-48ca-9991-19c93d298ccd cluster default username nacos password nacos } ... config { # file、nacos 、apollo、zk、consul、etcd3 type nacos nacos { serverAddr 127.0.0.1:8848 namespace 74cc7e01-083a-48ca-9991-19c93d298ccd group SEATA_GROUP username nacos password nacos dataId seataServer.properties } ...微服务的配置修改成seata: registry: type: nacos nacos: namespace: 74cc7e01-083a-48ca-9991-19c93d298ccd username: nacos password: nacos config: type: nacos nacos: namespace: 74cc7e01-083a-48ca-9991-19c93d298ccd username: nacos password: nacos默认的数据存储方式是file。现在修改成数据库的存储方式修改file.conf文件store { ## store mode: file、db、redis mode db ... ## database store property db { ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc. datasource druid ## mysql/oracle/postgresql/h2/oceanbase etc. dbType mysql driverClassName com.mysql.cj.jdbc.Driver ## if using mysql to store the data, recommend add rewriteBatchedStatementstrue in jdbc connection param url jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatementstrue user mysql password mysql minConn 5 maxConn 100 globalTable global_table branchTable branch_table lockTable lock_table queryLimit 100 maxWait 5000 }创建seata数据库并创建表-- 1. 全局事务表 CREATE TABLE IF NOT EXISTS global_table ( xid varchar(128) NOT NULL, transaction_id bigint DEFAULT NULL, status tinyint NOT NULL, application_id varchar(32) DEFAULT NULL, transaction_service_group varchar(32) DEFAULT NULL, transaction_name varchar(128) DEFAULT NULL, timeout int DEFAULT NULL, begin_time bigint DEFAULT NULL, application_data varchar(2000) DEFAULT NULL, gmt_create datetime DEFAULT NULL, gmt_modified datetime DEFAULT NULL, PRIMARY KEY (xid), KEY idx_status_gmt_modified (status,gmt_modified), KEY idx_transaction_id (transaction_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 2. 分支事务表 CREATE TABLE IF NOT EXISTS branch_table ( branch_id bigint NOT NULL, xid varchar(128) NOT NULL, transaction_id bigint DEFAULT NULL, resource_group_id varchar(32) DEFAULT NULL, resource_id varchar(256) DEFAULT NULL, branch_type varchar(8) DEFAULT NULL, status tinyint DEFAULT NULL, client_id varchar(64) DEFAULT NULL, application_data varchar(2000) DEFAULT NULL, gmt_create datetime DEFAULT NULL, gmt_modified datetime DEFAULT NULL, PRIMARY KEY (branch_id), KEY idx_xid (xid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 3. 锁表 CREATE TABLE IF NOT EXISTS lock_table ( row_key varchar(128) NOT NULL, xid varchar(128) DEFAULT NULL, transaction_id bigint DEFAULT NULL, branch_id bigint DEFAULT NULL, resource_id varchar(256) DEFAULT NULL, table_name varchar(32) DEFAULT NULL, pk varchar(36) DEFAULT NULL, gmt_create datetime DEFAULT NULL, gmt_modified datetime DEFAULT NULL, PRIMARY KEY (row_key), KEY idx_branch_id (branch_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE IF NOT EXISTS undo_log ( id bigint NOT NULL AUTO_INCREMENT, branch_id bigint NOT NULL, xid varchar(100) NOT NULL, context varchar(128) NOT NULL, rollback_info longblob NOT NULL, log_status int NOT NULL, log_created datetime NOT NULL, log_modified datetime NOT NULL, ext varchar(100) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY ux_undo_log (xid,branch_id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4;