分布式事务seata从入门到放弃

最近工作中遇到分布式的事务,耐心学习完后,整理下原理到实际使用方式。参考部分博客以及官网流程图。

一、事务指的就是一个操作单元,在这个操作单元中的所有操作最终要保持一致的行为,要么所有操作都成功,要么所有的操作都被撤销
分两种:
    本地事务:本地事物其实可以认为是数据库提供的事务机
    分布式事务:​简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用
​                分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,就是为了保证不同数据库的数据一致性


二、常见分布式事务解决方案
1.2PC 和 3PC 两阶段提交, 基于XA协议
2.TCC Try、Confirm、Cancel
3.事务消息 最大努力通知型

分布式事务分类
◦刚性事务:遵循ACID
◦柔性事务:遵循BASE理论

分布式事务框架

◦TX-LCN:支持2PC、TCC等多种模式
◦Seata:支持 AT、TCC、SAGA 和 XA 多种模式

◦RocketMq:自带事务消息解决分布式事务

三、事务模型

在分布式系统中,每一个机器节点能够明确知道自己在进行事务操作过程中的 结果是成功还是失败,但无法直接获取到其他分布式节点的操作结果
​当一个事务操作跨越多个分布式节点的时候,为了保持事务处理的 ACID 特性,
​需要引入一个“协调者”(TM)来统一调度所有分布式节点的执行逻辑,这些被调度的分布式节点被称为 AP。
​TM 负责调度 AP 的行为,并最终决定这些 AP 是否要把事务真正进行提交到(RM)

四、2PC和3PC目前使用不是很多,只做简单了解

XA协议规范-实现分布式事务的原理如下

◦一般习惯称为 两阶段提交协议(The two-phase commit protocol,2PC)
◦是XA用于在全局事务中协调多个资源的机制,MySql5.5以上开始支持
◦准备阶段:
事务管理器给每个参与者都发送Prepared消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo日志,此时事务没有提交。
Undo日志是记录修改前的数据,用于数据库回滚
Redo日志是记录修改后的数据,用于提交事务后写入数据
◦提交阶段:
如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息,否则发送提交(Commit)消息;
参与者根据事务管理器的指令执行【提交】或者【回滚】操作,并释放事务处理过程中使用的锁资源
注意:必须在最后阶段释放锁资源。

五、

TCC柔性事务 

◦刚性事务:遵循ACID
◦柔性事务:遵循BASE理论
◦TCC: 
将事务提交分为 
Try:完成所有业务检查( 一致性 ) ,预留必须业务资源( 准隔离性 )
Confirm :对业务系统做确认提交,默认 Confirm阶段不会出错的 即只要Try成功,Confirm一定成功
Cancel : 业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放, 进行补偿性
TCC 事务和 2PC 的类似,Try为第一阶段,Confirm - Cancel为第二阶段,它对事务的提交/回滚是通过执行一段 confirm/cancel 业务逻辑来实现,并且也并没有全局事务来把控整个事务逻辑
使用TCC时要注意Try - Confirm - Cancel 3个操作的幂等控制,由于网络原因或者重试操作都有可能导致这几个操作的重复执行

六、

分布式事务的解决方案之一事务消息

•事务消息
◦消息队列提供类似Open XA的分布式事务功能,通过消息队列事务消息能达到分布式事务的最终一致
•半事务消息
◦暂不能投递的消息,发送方已经成功地将消息发送到了消息队列服务端,但是服务端未收到生产者对该消息的二次确认,此时该消息被标记成“暂不能投递”状态,处于该种状态下的消息即半事务消息。
•消息回查
◦由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失,消息队列服务端通过扫描发现某条消息长期处于“半事务消息”时,需要主动向消息生产者询问该消息的最终状态(Commit或是Rollback),该询问过程即消息回查

七、

Seata主要由三个重要组件组成:
•TC:Transaction Coordinator 事务协调器,管理全局的分支事务的状态,用于全局性事务的提交和回滚。
•TM:Transaction Manager 事务管理器,用于开启、提交或者回滚【全局事务】。
•RM:Resource Manager 资源管理器,用于分支事务上的资源管理,向TC注册分支事务,上报分支事务的状态,接受TC的命令来提交或者回滚分支事务
◦传统XA协议实现2PC方案的 RM 是在数据库层,RM本质上就是数据库自身;
◦Seata的RM是以jar包的形式嵌入在应用程序里面

过程描述如下:

A服务的TM 向 TC 申请开启(Begin)一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
◦A服务的RM向TC注册分支事务
◦A服务执行分支事务,对数据库做操作
◦A服务开始远程调用B服务,并把XID 在微服务调用链路的上下文中传播。
◦B服务的RM向TC注册分支事务,并将其纳入XID对应的全局事务的管辖
◦B服务执行分支事务,向数据库做操作
◦全局事务调用链处理完毕,TM 根据有无异常向 TC 发起针对 XID 的全局提交(Commit)或回滚(Rollback)决议。
◦TC 调度 XID 下管辖的全部分支事务完成提交(Commit)或回滚(Rollback)请求。

八、seata的AT模式

一阶段:执行用户SQL
Seata 会拦截“业务 SQL”,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据
​在业务数据更新之后,再将其保存成“after image”,最后生成行锁
以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性

二阶段:Seata框架自动生成提交或者回滚
二阶段提交: 因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将阶段一保存的快照数据和行锁删掉,完成数据清理即可。
​二阶段回滚: 还原业务数据, 回滚方式便是用“before image”还原业务数据;
但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”
如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理
-------------------------------------------------------------------实战分割线---------------------------------------------------------------------------

1.创建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,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

2.下载中心 下载seata的TC服务端 seata-server-1.3.0.zip

Linux下(阿里云)安装
1.解压 unzip seata-server-1.3.0.zip
2.修改jvm内存(默认是2g,防止内存不够) vim seata-server.sh 


3./seata-server.sh 启动,默认是8091端口(记得防火墙开放端口,也可以nohup守护进程启动)

 ./seata-server.sh -p 8091 -h 47.xxxx.xxxx.158 也可以用这种指定IP 端口

TC需要存储全局事务和分支事务的记录,支持三种存储模式
1.file模式 (默认):性能高, 适合单机模式,在内存中读写,并持久化到本地文件中
在 bin/sessionStore/root.data文件
2.db模式 :性能较差,适合tc集群模式
3.redis模式:性能教高,适合tc集群模式

common包添加依赖

     <!--alibaba微服务整合分布式事务,这个方式才行-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-spring-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.3.0</version>
        </dependency>

application.yml 添加配置 颜色一样的要保持一致

1.注释掉全局异常

因全局异常会引起seata监听返回异常失败,需要暂时先注释掉全局异常的捕获。


2.TM入口service的方法增加 @GlobalTransactional 注解

结束了。。。。。使用很简单 但是 容易踩坑 

A服务调用B服务中的接口(样例中是使用springcloud里面的fegin调用)

A服务中引入pom依赖,application.yml中加入配置 ,service上加@GlobalTransactional注解

B服务中引入pom依赖,application.yml中加入配置

注意事项:

1.io.seata.1.3.0 加入注解后,需要确认导入的包是1.3.0里面的jar 跟服务器上部署的seata保持版本一致才有效

九、分布式事务AT模式执行机制回顾梳理

一阶段:执行用户SQL

◦AT模式可以应对大多数的业务场景,并且基本可以做到无业务入侵、开发者无感知
◦用户只需关心自己的 业务SQL. AT 模式分为两个阶段,可以认为是2PC
一阶段:执行用户SQL
Seata 会拦截“业务 SQL”,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据
​在业务数据更新之后,再将其保存成“after image”,最后生成行锁
​以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性

二阶段:Seata框架自动生成提交或者回滚
二阶段提交: 因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将阶段一保存的快照数据和行锁删掉,完成数据清理即可。
​二阶段回滚: 还原业务数据, 回滚方式便是用“before image”还原业务数据;
但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”
如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理 


undo_log表的rollback_info字段

{"@class":"io.seata.rm.datasource.undo.BranchUndoLog","xid":"47.107.xxx.xxxx:8091:138047137640624128","branchId":138047145731436544,"sqlUndoLogs":["java.util.ArrayList",
[{"@class":"io.seata.rm.datasource.undo.SQLUndoLog","sqlType":"INSERT","tableName":"user","beforeImage":{"@class":"io.seata.rm.datasource.sql.struct.TableRecords$EmptyTableRecords"
,"tableName":"user","rows":["java.util.ArrayList",[]]},"afterImage":{"@class":"io.seata.rm.datasource.sql.struct.TableRecords","tableName":"user",
"rows":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Row","fields":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Field",
"name":"id","keyType":"PRIMARY_KEY","type":-5,"value":["java.math.BigInteger",30]},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"name","keyType":"NULL","type":12,
"value":"anna77776"},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"pwd","keyType":"NULL","type":12,"value":"$1$AOPA4LY4$egWxKcekaI9sE2igxUKa7."},
{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"head_img","keyType":"NULL","type":12,"value":null},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":
"slogan","keyType":"NULL","type":12,"value":"好好学习 天天向上"},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"sex","keyType":
"NULL","type":-6,"value":1},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"points","keyType":"NULL","type":4,"value":0},{"@class":"io.seata.rm.datasource.sql.struct.Field"
,"name":"create_time","keyType":"NULL","type":93,"value":["java.sql.Timestamp",[1621348246000,0]]},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"mail","keyType":"NULL",
"type":12,"value":"[email protected]"},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"secret","keyType":"NULL","type":12,"value":"$1$AOPA4LY4"}]]}]]}}]]}


十、自定义全局异常下分布式事务失效解决方案

 问题: 微服务场景下,配置了统一全局异常处理,导致seata在AT模式下无法正常回滚问题
◦如果使用Feign 配置了容错类(fallback)或者容错工厂(fallbackFactory),也是一样的问题
•原因:服务A调用服务B, 服务B发生异常,由于全局异常处理的存在(@ControllerAdvice), seata 无法拦截到B服务的异常,从而导致分布式事务未生效
•解决思路
配置了全局异常处理,所以rpc一定会有返回值, 所以在每个全局事务方法最后, 需要判断rpc是否发生异常 
发生异常则抛出 RuntimeException或者子类

比如:

//调用发放优惠券的接口 ,正常情况下是返回0的
JsonData jsonData = couponFeignService.addNewUserCoupon(request);
        if(jsonData.getCode()!=0){
            throw new RuntimeException("发放优惠券异常");
        }

方式一:RPC接口不配置全局异常
方式二:利用AOP切面解决
方式三:程序代码各自判断RPC响应码是否正常,再抛出异常

最后  -----------------------放弃----------------------

分布式事务解决方案很多,XA的2PC、TCC、MQ事务消息等
•框架也有Seata, 同时支持多种方式模式
•重点
不管选哪一种方案,在项目中应用都要谨慎再思考,
除特定的数据强一致性场景外,能不用尽量就不要用
​因为无论它们性能如何优越,一旦项目链路加入分布式事务
整体效率会几倍的下降,在高并发情况下弊端尤为明显

•总之
◦分布式事务和分布式锁一样,能不用就不用
◦实在要用,使用优先是 柔性事务,实在无法满足再考虑 刚性事务
◦分布式锁也是,尽量降低锁的粒度

猜你喜欢

转载自blog.csdn.net/wnn654321/article/details/116804214
今日推荐