昆明企业自助建站系统,互联网公司花名大全男,中国建设网官方网站洞庭湖治理,招聘信息网站建设分布式事务Seata 1. 本地事务2. 分布式事务3. 实现思路#xff1a;两阶段提交协议#xff08;2PC#xff09;3.1 基础理解3.2 2PC的隐患 4. Seata4.1 Seata是什么4.2 Seata的三大角色4.3 Seata一次事务的生命周期4.4 Seata AT模式的设计思路4.4.1 设计思路4.4.1.1 一阶段4.4… 分布式事务Seata 1. 本地事务2. 分布式事务3. 实现思路两阶段提交协议2PC3.1 基础理解3.2 2PC的隐患 4. Seata4.1 Seata是什么4.2 Seata的三大角色4.3 Seata一次事务的生命周期4.4 Seata AT模式的设计思路4.4.1 设计思路4.4.1.1 一阶段4.4.1.2 二阶段4.4.1.3 写隔离4.4.1.4 读隔离 4.4.2 详细过程一阶段二阶段分布式事务操作成功-提交分布事事务操作失败-回滚 1. 本地事务
操作单一的一个数据库这种情况下的事务叫本地事务(Local Transaction);
本地事务的ACID特性由各数据库直接提供支持
在JDBC编程中可以通过Connection对象来开启、关闭和提交事务
代码示例
只需要引入mysql-connector-java依赖即可 dependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactIdversion8.0.33/version/dependencypackage com.yz.local.transaction;import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;/*** 本地事务验证** author yunze* date 2023/11/26 0026 14:21*/
public class LocalTransaction {private static final String url jdbc:mysql://localhost:3306/t_mall_account;private static final String username root;private static final String password 123456;public static void main(String[] args) throws SQLException {String sql insert into t_account (id, name, cash_balance) values (?, ?, ?);Connection connection null;PreparedStatement preparedStatement null;try {// 加载数据库驱动Class.forName(com.mysql.cj.jdbc.Driver);// 建立数据库连接获得连接对象Connectionconnection DriverManager.getConnection(url, username, password);connection.setAutoCommit(false); // 关闭自动提交也就是开启手动控制事务的提交// 创建和执行PreparedStatement操作preparedStatement connection.prepareStatement(sql);// 设置参数preparedStatement.setLong(1, 1L);preparedStatement.setString(2, 王五);preparedStatement.setBigDecimal(3, BigDecimal.valueOf(2000));// 执行插入sql语句preparedStatement.executeUpdate();// 提交事务connection.commit();} catch (SQLException e) {e.printStackTrace();// 事务回滚assert connection ! null;connection.rollback();} catch (ClassNotFoundException e) {throw new RuntimeException(e);// 事务回滚} finally {// 关闭连接资源if (preparedStatement ! null) {try {preparedStatement.close();} catch (SQLException e) {e.printStackTrace();}}if (connection ! null) {try {connection.close();} catch (SQLException e) {e.printStackTrace();}}}}
}2. 分布式事务
在微服务架构中想完成一个业务功能可能需要涉及到多个服务甚至是多个数据库这就需要保证对于多个数据库的数据操作要么一起成功要么一起失败以保证多个数据库的数据一致性这就是分布式事务所需要干的事情
3. 实现思路两阶段提交协议2PC
3.1 基础理解
理解将一次事务的提交commit划分为2个阶段Phase
两阶段提交里有这么两个定义TM事务管理器和RM资源管理器一个TM下管理多个RM
第一阶段准备提交–可以提交
TM向所有RM发出准备提交请求消息通知他们准备提交自己事务分支各个RM则会判断自己的工作是否可以被提交
如果可以提交则执行任务SQL进行持久化然后告诉TM该RM分支执行成功执行成功后数据库服务器会将事务的状态改为可以提交此时事务并不是真正提交了数据库里还看不到变动更新的数据
如果发生了异常不能正常提交则会告诉TM该RM分支执行失败需要回滚数据 第二阶段确认提交–提交完成 TM根据第一阶段里各个RM返回的消息来决定提交事务还是回滚事务
如果所有的RM都返回的是成功则TM会向所有的RM发送确认提交请求消息通知他们正式提交事务各分支的数据库服务器就会将事务的可以提交状态改为提交完成状态此时事务才是真正的提交数据库里可以看到变动更新的数据
如果有RM失败了或没有收到某一RM的回应则会认为事务失败TM就会通知所有的RM回滚他们自己的事务分支如果RM分支数据库服务接收不到第二阶段的确认提交请求消息也会把处于可以提交状态的事务撤销
例 现在用户下了个订单则在调接口新增订单的同时还需要去扣减用户的余额和扣减商品的库存且用户、订单、库存均不在一个数据库中则此时新增订单这个发起分布式事务的开端为TM事务管理器用户、订单、库存的各自事务分支为RM资源管理器可将其分别理解为全局事务和分支事务全局管理各个分支 TM会通知用户、订单、库存准备提交事务用户、订单、库存接收到TM的消息后会执行对应的数据更新等操作用户扣余额、新增订单、扣减库存并将执行结果告诉TM如果用户、订单、库存全部执行成功则TM会再次向用户、订单、库存发出确认提交请求用户、订单、库存接收到TM的确认提交消息后会正式提交各自的事务使更新的数据生效如果之前用户、订单、库存没有全部执行成功则TM会通知用户、订单、库存回滚事务撤回之前的数据更新操作 [外链图片转存中…(img-WUpIy3HX-1701305220708)]
3.2 2PC的隐患 数据无法保持一致 如果第一阶段所有RM分支均提交成功则第二阶段TM通知所有RM分支确认提交时有一个RM分支出现网络异常导致没有接收到确认提交的消息则会放弃事务。而其他RM又接收到了确认提交消息最终提交了事务则最后会导致数据的不一致。 同步阻塞 2PC里所有的参与者都是同步进行的且是阻塞的所有RM在第一阶段接收到请求后都会预先锁定资源一直到第二阶段commit或rollback之后才会释放。 协调者TM故障 由于RM都是由TM协调进行工作所以当TM出现故障会导致已经获取了数据库资源的RM一直阻塞下去如果在第二阶段那么所有的RM参与者都还处于锁定事务资源状态中而无法完成事务释放资源。
4. Seata
4.1 Seata是什么
Seata是阿里开源的一款分布式事务处理框架致力于提供高性能和简单易用的分布式事务服务。Seata提供了AT、TCC、SAGA和XA事务模式其中首推的是AT模式。
Seata官网地址https://seata.io/zh-cn/
4.2 Seata的三大角色
Seata中一共分为3个角色分别负责不同的工作 TCTransaction Coordinator事务协调者 维护全局和分支事务的状态驱动全局事务的提交或回滚 TMTransaction Manager事务管理器 定义全局事务的范围开始全局事务、提交或回滚全局事务 RMResource Manager资源管理器 管理分支事务处理的资源同TC进行交互将分支事务注册到TC同时报告分支事务的状态并驱动分支事务提交或回滚
其中TC 为单独部署的一个服务在这套分布式事务处理方案里属于是 Server 服务端而 TM 和 RM 则是应用里的概念属于是 Client 端
4.3 Seata一次事务的生命周期
TM 请求 TC 开启一个全局事务TC 端会生成一个 XID 作为本次全局事务的唯一标识且这个 XID 是会在本次服务的整个调用链路中传递的后续的分支事务也是根据 XID 关联上该全局事务。RM 请求 TC 将本地事务注册为全局事务的分支事务通过全局事务的 XID 进行关联。各 RM 分支事务告知 TM 自己是否执行成功。TM 根据各 RM 分支汇报的情况判断应该提交事务还是回滚事务然后请求 TC 告诉本次全局事务根据XID来判断是哪个全局事务应该提交还是回滚。TC 驱动 各 RM 将本次 XID 对应的分支事务本地事务进行提交还是回滚。
示例图
[外链图片转存中…(img-4hCgo2Ou-1701305220711)]
4.4 Seata AT模式的设计思路
4.4.1 设计思路
Seata AT模式的核心是对业务无侵入是一种改进之后的两阶段提交
4.4.1.1 一阶段
业务数据和回滚日志记录在同一个本地事务中提交使用AT模式是需要到本地业务数据库中添加一个undo_log表建表sql后文会有释放本地锁和连接资源此时已经可以看到业务数据已经变动SeataAT 模式的默认全局隔离级别是 读未提交。
4.4.1.2 二阶段
成功–RM异步执行undolog日志删除操作
失败–回滚操作为通过第一阶段记录的回滚日志记录进行反向补偿。
4.4.1.3 写隔离
一阶段本地事务提交前需要确保先拿到 全局锁 。拿不到 全局锁 不能提交本地事务。拿 全局锁 的尝试被限制在一定范围内超出范围将放弃并回滚本地事务释放本地锁。
以一个示例来说明
两个全局事务 tx1 和 tx2分别对 a 表的 m 字段进行更新操作m 的初始值 1000。
tx1 先开始开启本地事务拿到本地锁更新操作 m 1000 - 100 900。本地事务提交前先拿到该记录的 全局锁 本地提交释放本地锁。 tx2 后开始开启本地事务拿到本地锁更新操作 m 900 - 100 800。本地事务提交前尝试拿该记录的 全局锁 tx1 全局提交前该记录的全局锁被 tx1 持有tx2 需要重试等待 全局锁 。
[外链图片转存中…(img-QEl7rNM2-1701305220711)]
tx1 二阶段全局提交释放 全局锁 。tx2 拿到 全局锁 提交本地事务。
[外链图片转存中…(img-yRGvXnKW-1701305220712)]
如果 tx1 的二阶段全局回滚则 tx1 需要重新获取该数据的本地锁进行反向补偿的更新操作实现分支的回滚。
此时如果 tx2 仍在等待该数据的 全局锁同时持有本地锁则 tx1 的分支回滚会失败。分支的回滚会一直重试直到 tx2 的 全局锁 等锁超时放弃 全局锁 并回滚本地事务释放本地锁tx1 的分支回滚最终成功。
因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的所以不会发生 脏写 的问题。
4.4.1.4 读隔离
SeataAT 模式的默认全局隔离级别是 读未提交Read Uncommitted
如果应用在特定场景下必需要求全局的 读已提交 可通过 SELECT FOR UPDATE 语句的实现。
例
select a.name from a where a.id 1 for update;[外链图片转存中…(img-sa9LM7MK-1701305220712)]
SELECT FOR UPDATE 语句的执行会申请 全局锁 如果 全局锁 被其他事务持有则释放本地锁回滚 SELECT FOR UPDATE 语句的本地执行并重试。这个过程中查询是被 block 住的直到 全局锁 拿到即读取的相关数据是 已提交 的才返回。
出于总体性能上的考虑Seata 目前的方案并没有对所有 SELECT 语句都进行代理仅针对 FOR UPDATE 的 SELECT 语句。
4.4.2 详细过程 Seata-AT模式相关表结构地址 TC端 https://github.com/seata/seata/blob/1.7.1/script/server/db/mysql.sql RM端https://github.com/seata/seata/blob/1.7.1/script/client/at/db/mysql.sql 以一个示例来说明整个 AT 分支的工作过程。
业务表product
字段类型主键idbigintPRKcodevarchaer(50)namevarchaer(50)
业务数据
idcodename1PHONE0001xiaomi 13
分支事务要执行的业务SQL
update product set name xiaomi 14 pro where name xiaomi 13;一阶段
[外链图片转存中…(img-yAMjeHLX-1701305220715)] 解析SQLParse SQL通过解析业务SQL得到SQL的类型为UPDATE操作表为product条件where name ‘xiaomi 13’等相关的信息。 查询前置镜像Query for Before Image根据第一步解析得到的条件信息生成查询语句查询执行业务SQL之前该数据的信息从而得到了前置镜像数据用于后续回滚时反向补偿的数据依据。 select id, code, name where name xiaomi 13前置镜像数据如下 idcodename1PHONE0001xiaomi 13 执行业务SQLBusiness SQL更新数据的 name 为’xiaomi 14 pro’。 查询后置镜像Query for After Image根据前置镜像结果的主键再次查询数据从而得到了后置镜像数据。 select id, code, name where id 1;后置镜像数据如下 idcodename1PHONE0001xiaomi 14 pro 插入回滚日志Insert Undo log将前置镜像、解析业务SQL得到的信息、后置镜像信息一起组成一条回滚日志记录插入到与业务表在同一个库的 undo_log 日志表。 回滚日志记录 {branchId: 641789253,undoItems: [{afterImage: {rows: [{fields: [{name: id,type: 4,value: 1}, {name: code,type: 12,value: PHONE0001}, {name: name,type: 12,value: xiaomi 14 pro}]}],tableName: product},beforeImage: {rows: [{fields: [{name: id,type: 4,value: 1}, {name: code,type: 12,value: PHONE0001}, {name: name,type: 12,value: xiaomi 13}]}],tableName: product},sqlType: UPDATE}],xid: xid:10001
}本地事务提交前请求 TCBefore Commit在本地事务提交之前回滚日志记录插入之后RM 会请求 TC 注册分支并申请 product 表中主键值为1的记录的 全局锁 全局写排他锁该锁申请到之后会一直持有到这一个全局事务结束其他全局事务申请不到该记录的全局锁就无法提交事务所以才不会出现 脏写 的问题。 本地事务提交Commit业务数据的更新操作和前面步骤中生成的 UNDO LOG 日志数据一并提交。 分支事务状态报告After Local TX本地事务提交之后如果分支事务提交失败则会向 TC 进行报告如果分支事务提交成功 RM 是不会向 TC 报告的所以RM成功的时候TC是不清楚的而是TM没有接收到任一 RM 的异常则代表所有的 RM 都提交成功TM 就会通知 TC 进行全局提交。
二阶段
分布式事务操作成功-提交
[外链图片转存中…(img-mOarpa6n-1701305220717)]
TC 通知分支提交Pending in async queueTC 将分支提交任务放入一个异步的任务队列中然后 TC 会通知各个 RM 分支进行提交RM 接收到 TC 的通知后会立刻响应 TC 告知提交成功了其实这个时候RM还啥都没开始做。RM 删除日志提交事务Delete Undo Log and Commit各 RM 分支接收到 TC 的通知后会异步和批量的删除 UNDO LOG 日志数据。 具体实现操作如下 TC 在接收到 TM 的全局提交请求之后TC 仅仅是将这个全局事务的状态改为 GlobalStatus.AsyncCommitting 后续的提交是由一个定时线程池去负责调度的每秒执行一次会先从Seata的 global_table 表里获取全局事务列表信息如果其中某一条数据的状态为 GlobalStatus.AsyncCommitting 则会从 global_table 表里删除这一条全局事务信息然后根据要删除的这条全局事务信息的 XID 在Seata的 branch_table 表里找到关联的分支事务信息再删除分支事务信息数据删除全局锁。最后向各 RM 发送请求让 RM 自己去删除对应的 UNDO LOG 日志数据RM自己也有定时器去清理 UNDO LOG 日志每天执行一次默认每次删除7天前的日志数据。 分布事事务操作失败-回滚
[外链图片转存中…(img-k7oXUPcd-1701305220717)] 各 RM 分支接收到 TC 的回滚请求后RM 会开启一个本地事务然后开始执行回滚操作。 查询日志Find Undo Log根据全局事务标识 XID和当前 RM 所对应的分支事务标识 Branch ID 查询到对应的 UNDO LOG 日志数据。 校验数据Check by After Image根据后置镜像的数据记录与当前的数据进行比对检查是否一致如果出现不同则代表数据被当前全局事务之外的操作更新了数据可能是其他线程或其他服务这种意外情况需要额外去写专门的配置策略进行处理。 执行数据回滚SQLExecute undo SQL根据前置镜像的数据记录生成数据回滚的SQL语句。 update product set name xiaomi 13 where id 1;删除日志信息Delete Undo Log执行回滚SQL语句之后会及时删除 UNDO LOG 日志数据。 提交本地事务Commit。 上报结果After Local TX本地事务提交之后 RM 会向 TC 上报该分支事务的回滚结果。 具体实现操作如下 TC 在接收到 TM 的全局回滚请求之后TC 会先根据 XID 获取到该全局事务信息再将其事务状态改为 GlobalStatus.Rollbacking 然后后续的回滚操作同全局提交一样交由一个定时线程池去负责调度的每秒执行一次会先从Seata的 global_table 表里获取全局事务列表信息然后找到处于 GlobalStatus.Rollbacking 状态的全局事务再根据 XID 从 branch_table 表查询出该全局事务的所有分支事务然后循环所有分支事务挨个通知分支事务进行回滚回滚成功则删除分支事务信息失败则会一直重试RM 接到通知后会根据 UNDO LOG 日志数据进行回滚、 UNDO LOG 日志数据删除、事务提交、向 TC 上报结果。