分布式事务及解决方案

前言

分布式事务也不是一开始就出现的,它是随着技术架构的演变才渐渐发生的,所以在讲分布式事务之前有必要说下本地事务的概念

本地事务

事务有四个特性,如下

一致性:数据操作前后需要一致,不能凭空加减

持久性:数据一旦提交就必须持久化

原子性:一系列的操作要么都成功,要么都失败

隔离性:多个不同的事务对于同一份数据操作时数据如何做隔离

我们常用的数据库为Mysql,本地事务基本都是数据库帮我们实现的,并不需要我们自己实现,只需要调用对应的API即可,核心的API有 begin 开启一个事务,当所有操作完成后需要提交执行 commit操作,如果中途有失败的话可以使用rollback进行回滚事务,本地事务就说到这里,并不是本篇文章的核心,如有不清晰的请自行百度

分布式事务

早期的程序由于还没有太多人依赖于网络,所以数据量并不是很多,随着时间的推移,越来越多的人使用网络,当人流量,数据量越来越多的时候,程序就不能单纯的使用一个数据库来承载了,因此也就衍生出了分布式事务的问题,分布式事务出现的基本场景如下

多数据操作

分布式事务及解决方案

当程序需要操作多个数据库时

分库分表

分布式事务及解决方案

出现分库分表时也有分布式事务问题

跨服务调用

分布式事务及解决方案

分布式事务及解决方案

以上两种跨服务调用,不过服务是不是在同一个数据库都会有分布式事务问题,因为引入了网络问题,没办法操作到同一个Connetciton连接,所以必然有分布式事务问题

以上乃几种分布式事务出现的情况,可能还有更多的情况

分布式事务解决方案

2PC和XA

2PC名称为二阶段提交,顾名思义,也就是把原来的提交改为两部提交,需要引入一个事务管理器的中间件,第一阶段只是预提交,第二阶段才是真正的事务决定是否提交事务

分布式事务及解决方案

以上为二阶段提交的核心流程,例子说明如下

业务流程:用户在产品上进行下单,下单时需要操作订单服务和库存服务,两个服务操作存在分布式事务问题,以上为二阶段提交处理

第一阶段:订单和库存在本地执行sql,但是不提交,然后发给事务管理器

第二阶段:事务管理器接收到标识以后再根据具体通知通知每个事务提交或者回滚

注:TM是集成在订单服务里的,因此它需要持有其他服务的connection对象,所以才有了资源暂用的问题

全局事务和分支事务

用以上的例子来说,用户整个下单操作便称为全局事务,订单和库存自己本身的事务称为分支事务,上面的概念解释

RM:就是分支事务,也叫资源管理器

TM: 事务管理器,用于控制分支事务的准备执行

BID: 分支事务的id

TID: 全局事务id

同时BID和TID需要进行绑定,才能找到自己属于哪个全局事务

以上就是2PC提交的整个过程,XA其实是一种规范,简单理解就可以理解为预提交,真正提交的接口规范,顺便说一句,Mysql本身是实现了XA规范的,也就是实现了2PC阶段这种方案,只不过这种方案性能比较低

2PC存在的问题

connection连接未释放问题,性能问题

事务管理器单点故障问题

二阶段分支事务没有校验,有可能会出现问题

TCC

TCC模式,可以说是对2PC做的一些优化,它主要包含了三个阶段

try: 尝试锁定资源

commit: 如果没问题了就进行提交

cancel : 如果任何一个有问题就回滚

是不是很类似于2PC,不过TCC解决了2PC的一些问题,具体如下

  1. TCC在try阶段是直接操作的,不会一直持有数据库链接资源

  2. commit阶段可以处理为异步化,进一步提升性能

  3. 可以支持多种数据源,2PC是要求数据源一定要支持XA规范的,而TCC可以纯粹的由我们自己手工进行控制

TCC使用的一些缺点

  1. 如果是扣钱那么可以在try阶段做,但是如果是加钱那肯定不可以这么做的,因为如果直接加成功了,对方去使用,但是事务失败了,此时用户已经把钱用出去了,你是无法回滚的

  2. 需要自己进行大量的编码,对业务侵入也比较大

Seata

seata是阿里开源针对于处理分布式事务的框架,它引入了一个TC的概念,来解决事务管理器的单点故障问题,而这个TC就是seata服务端,TC的数据是统一的数据库,所以不存在集群高可用的问题,只要起多个节点就行,又理解了一点,只要是没有数据交互的集群,实际上是不存在高可用问题的,它不像redis,nacos那样都有自己节点上的数据存储的,它有几种使用模式,如下

注意:使用Seata必须抛出异常,否则Seata无法处理回滚问题,比如库存不够一定要把异常往上抛出来,不然可能会有事务回滚现象的存在

AT模式

参考资料:Seata 是什么

类似于2PC提交,不过它解决了2PC的问题,比如事务单点故障,connetcion连接未释放问题,分支事务提交的时候是真的提交的,seate会记录回滚日志,如果分支事务要回滚的话到时seata会自动帮我们回滚,二阶段如果没有收到TC的响应,也会通知人工介入处理,这是官网的一个图

分布式事务及解决方案

AT的要求和缺点

使用AT有两点要求:

  • 基于支持本地 ACID 事务的关系型数据库。
  • Java 应用,通过 JDBC 访问数据库。

缺点:

AT模式会记录数据的前置版本和后置版本,但是如果后置版本跟数据库不一样时那分布式事务会实现,而且还会导致其他请求进不来,目前这个问题没法解决,,,

例子: 比如商品库存未10,要改为9,那么seate会记录前置版本为10,后置版本为9,然后假设此时发生了分布式事务问题,seate会根据全局事务id和分支事务id到undo_log日志表里面查询唯一的后置日志取出里面的数据为9,跟数据库最新的状态对比也是9,那么就会回滚事务,分布式事务就是正常的,假设数据库数据已经不是9了,那么分布式事务就会失效了,只能人工介入处理了,,,那这样使用感觉是不是不太行。。。

所以如果数据参与了分布式事务,那要尽可能的避免不要让其他程序进行调用,不过感觉很难避免吧,毕竟数据总要有接口去操作

XA模式

跟AT模式其实差不多,只不过是一直需要持有数据库连接,不需要undo_log日志表了,因为并没有先写入前置和后置镜像的需要,不过他需要用@Transaction注解标识,而AT模式是可以不需要的

使用的时候很简单,只需要配置属性seata.data-source-proxy-mode: XA 即可,默认是AT模式的,除了seata提供的全局事务注解再加上@Transaction注解即可

TCC模式

跟上面说的TCC就是一个意思,只不过是seata进行了封装处理,TCC可以理解为seata的手工AT模式,需要我们自己编写代码进行相关的业务逻辑处理,而AT是自动帮我们处理当出现分布式事务问题的一种模式

对业务代码有侵入性,也就是说需要对接口进行改造,比如原来扣库存一个接口需要改为三个接口,分别对应try, commit,cancal 三个操作相关的逻辑

Seata实现TCC机制,还是需要我们自己写代码的,如下,通过注解@TwoPhaseBusinessAction把原来一个接口改为了三个接口

public interface TccActionOne {
    @TwoPhaseBusinessAction(name = "prepare", commitMethod = "commit", rollbackMethod = "rollback")
    public boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "a") String a);

    public boolean commit(BusinessActionContext actionContext);

    public boolean rollback(BusinessActionContext actionContext);
}

然后使用如下即可,只需要调用prepare即可,seata会自动调用commit或者rollback方法

@GlobalTransactional
public String doTransactionCommit(){
    //服务A事务参与者
    tccActionOne.prepare(null,"one");
    //服务B事务参与者
    tccActionTwo.prepare(null,"two");
}

TCC还存在相关的一些问题,seata已经帮我们解决了,比如下面

TCC空回滚问题

因为我们第一个操作是执行try操作的,此时可能由于网络问题或者本身有问题从而导致接口失败,但是我们上层逻辑还是去调用commit方法,那会导致数据出现不一致,比如下面的例子

张三原来有40块,使用TCC机制要先扣减100块,但是金额不足,而在rollback里面直接加上100块,那么等于张三变成了有140块,那很明显是出现数据不一致问题的,那么如何解决这个问题呢?其实很简单,可以在TCC的try接口中插入一个日志操作表,这个日志是跟TCC在同一个本地事务中的,然后在回滚的时候可以先去定位日志,如果日志定位失败,那说明try阶段压根就没执行成功,rollback直接返回即可,否则要进行对应的处理

TCC幂等性处理

try和commit阶段可能会产生重复提交,那么对应的TC接口就需要保证幂等性的问题,一般的做法就是利用全局唯一标识来做处理的,也就是生成一个全局唯一标识然后利用数据库等唯一索引机制来保证接口的幂等性

Seata 是如何处理幂等问题的呢?

同样的也是在 TCC 事务控制表中增加一个记录状态的字段 status,该字段有 3 个值,分别为:

  1. tried:1
  2. committed:2
  3. rollbacked:3

二阶段 Confirm/Cancel 方法执行后,将状态改为 committed 或 rollbacked 状态。当重复调用二阶段 Confirm/Cancel 方法时,判断事务状态即可解决幂等问题。

TCC悬挂问题

由于网络问题导致了canal先执行,try后执行

如何处理悬挂

悬挂指的是二阶段 Cancel 方法比 一阶段 Try 方法优先执行,由于允许空回滚的原因,在执行完二阶段 Cancel 方法之后直接空回滚返回成功,此时全局事务已结束,但是由于 Try 方法随后执行,这就会造成一阶段 Try 方法预留的资源永远无法提交和释放了。

Seata 是怎么处理悬挂的呢?

在 TCC 事务控制表记录状态的字段 status 中增加一个状态:

  • suspended:4

当执行二阶段 Cancel 方法时,如果发现 TCC 事务控制表有相关记录,说明二阶段 Cancel 方法优先一阶段 Try 方法执行,因此插入一条 status=4 状态的记录,当一阶段 Try 方法后面执行时,判断 status=4 ,则说明有二阶段 Cancel 已执行,并返回 false 以阻止一阶段 Try 方法执行成功。

Original: https://blog.csdn.net/zxc_user/article/details/127825086
Author: zxc_user
Title: 分布式事务及解决方案

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/660519/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球