终于有人把分布式事务说清楚了!

前言mysql

这篇文章将给你们介绍一下对分布式事务的一些看法,并讲解分布式事务处理框架 TX-LCN 的执行原理,错误之处望各位不吝指正。程序员

v2-bb348a5cd5ad0d7634636aa3c1543c62_hd.png

1. 什么状况下须要使用分布式事务?sql

使用的场景不少,先举一个常见的:在微服务系统中,若是一个业务须要使用到不一样的微服务,而且不一样的微服务对应不一样的数据库。数据库

打个比方:电商平台有一个客户下订单的业务逻辑,这个业务逻辑涉及到两个微服务,一个是库存服务(库存减一),另外一个是订单服务(订单数加一),示意图以下:服务器

v2-e67e8d42cc14f0cd06143e8c8a452c2b_hd.jpg

若是在执行这个业务逻辑时没有使用分布式事务,当库存与订单其中一个出现故障时,就极可能出现这样的状况:库存数据库的值减小了 1,可是订单数据库没有变化;或是库存没变化,多了一个订单,也就是出现了数据不一致现象。微信

因此在相似的场合下咱们要使用分布式事务,保证数据的一致性。网络

2. 分布式事务的解决思路app

2.1引入:MySQL 中的两阶段提交策略框架

在谈分布式事务的解决思路以前,咱们先来看看单一数据源是如何作事务处理的,咱们能够从中获取一些启发。分布式

咱们以 MySQL 的 InnoDB 引擎为例,因为 MySQL 中有两套日志机制,一套是存储层的 redo log,另外一套是 server 层的 binlog,每次更新数据都要对两个日志进行更新。为了防止写日志时只写了其中一个而没有写另一个,MySQL 使用了一个叫两阶段提交的方式保证事务的一致性。具体是这样的:

假设建立一个这样的数据库:mysql> create table T(ID int primary key, c int);, 而后执行一条这样的更新语句:mysql> update T set c=c+1 where ID=2;

这条更新语句的执行流程是这样子的:

  1. 首先执行器会找引擎取 ID=2 这一行数据

  2. 拿到数据后会把数据进行+1 操做,而后调用引擎接口把新数据写入

  3. 引擎将数据更新到内存中,并将操做记录到 redo log 里,此时 redo log 处于 prepare 状态。但它不会提交事务,只是通知执行器已经完成任务,能够随时提交。

  4. 执行器生成这个操做的 binlog,并把 binlog 写入磁盘

  5. 最后执行器调用引擎的事务接口,把 redo log 改成提交状态,更新完成。

在上述过程当中,redo log 写完后没有直接提交,而是处于 prepare 状态,等通知执行器并把 binlog 写完后,redo log 再进行提交。这个过程就是两阶段提交,这是一个精妙的设计。

可能你会问为何要有两阶段提交?若是不采用两阶段提交的话,也就是使用一阶段提交,那就至关于按顺序执行写 redo log 和 binlog,若是写完 redo log 后系统出现了故障,那么就会只有 redo log 记录了操做,binlog 没有记录,形成数据不一致;使用两阶段提交的话,假设写完 redo log 后系统出现了故障,因为事务尚未提交,因此能够顺利回滚。

两阶段提交的设计还有什么好处?首先要奠基一个概念:一个操做执行的时间越长,这个操做就越有可能失败。打个比方,你吃饭要用 20 分钟,上厕所要用 1 分钟,在吃饭的过程当中收到微信消息的几率确定比去上厕所的过程当中收到微信消息的几率大。因为在数据库中更新操做的时间要远大于提交事务的时间,因此先把更新操做作完,等全部耗时操做都作完最后再提交事务,可以最大程度保证事务执行成功。

2.2分布式事务的两阶段提交策略

根据上述的两阶段提交策略,分布式事务也能够采起相似的办法完成事务。

在第一阶段,咱们要新增一个事务管理者的角色,经过它来协调各个数据源。仍是拿开头的订单案例讲解,在执行下订单的逻辑时,先让各个数据库去执行各自的事务,好比从库存中减 1,在订单库中加 1,可是完成后不提交,只是通知事务管理者已经完成了任务。

v2-f83fb65d814695261e366a30a610b5b3_hd.jpg

到了第二阶段,因为在阶段一咱们已经收到了各个数据源是否就绪的信息,只要有一个数据源没有就绪,在第二阶段就通知全部数据源回滚;若是所有数据源都已经就绪,就通知全部数据源提交事务。

v2-f5b41cd94b91a064934b746a48745ec4_hd.jpg

总结一下这个两阶段提交的过程就是:首先事务管理器通知各个数据源进行操做,并返回是否准备好的信息。等全部数据源都准备好后,再统一发送事务提交(回滚)的通知让各个数据源提交事务。因为最后的提交操做耗时极短,因此操做失败的可能性会很低。

那么这个两阶段提交协议可能存在什么缺点呢?极可能存在被阻塞的问题,假如其中一个数据源出现了某些问题阻塞了,既不能返回成功信息,也不能返回失败信息,那么整个事务将被阻塞。对应的策略是添加一些倒计时的操做,或者是从新发送消息。

欢迎你们关注个人公种浩【程序员追风】,文章都会在里面更新,整理的资料也会放在里面。

3. 分布式事务框架 TX-LCN

讲了这么多理论的知识,下面讲解一款真正应用在生产中的分布式事务框架 TX-LCN 的运行原理。(典型的分布式事务框架不止 TX-LCN,好比还有阿里的 GTS,不过 GTS 是收费的,TX-LCN 是开源的)

咱们先看一下官方文档中给出的运行原理示意图:

v2-8c5aaa1b5c6546d13f581bfec4842787_hd.jpg

思路和咱们上面讲的两阶段分布式事务处理流程差很少(有小不一样),核心步骤分为 3 步:

  1. 建立事务组:在事务发起方开始执行业务代码以前先调用 TxManager 建立事务组对象,而后拿到事务表示 GroupId 的过程。简单来讲就是对此次下订单的操做在事务管理中内心建立一个对象,拿到一个 id。

  2. 加入事务组:参与方在执行完业务方法后,将该模块的事务信息通知给 TxManager 的操做。也就是指各个数据源(各个服务)完成操做后,和事务管理中心说一声,注册一下本身。

  3. 通知事务组:发起方执行业务代码后,将发起方执行结果状态通知给 TxManager,TxManager 将根据事务最终状态和事务组的信息来通知相应的参与模块提交或回滚事务,并返回结果给事务发起方。和客户打交道的下订单服务会收到减库存和加订单是否成功消息,它会把这两个消息通知给事务管理者,事务管理者根据状况通知两个库存服务提交事务或回滚事务。

目前发现网上有一篇不错的 TX-LCN 执行源码分析文章: https://blog.csdn.net/cgj296645438/article/details/93860384

文章中跟着源码走一遍会发现和上面的流程图差很少,落实到代码中有一些精彩的地方,好比:

public Object runTransaction(DTXInfo dtxInfo, BusinessCallback business) throws Throwable {
        if (Objects.isNull(DTXLocalContext.cur())) {
            DTXLocalContext.getOrNew();
        } else {
            return business.call();
        }
        log.debug("<---- TxLcn start ---->");
        DTXLocalContext dtxLocalContext = DTXLocalContext.getOrNew();
        TxContext txContext;
        // ---------- 保证每一个模块在一个DTX下只会有一个TxContext ---------- //
        if (globalContext.hasTxContext()) {
            // 有事务上下文的获取父上下文
            txContext = globalContext.txContext();
            dtxLocalContext.setInGroup(true);
            log.debug("Unit[{}] used parent's TxContext[{}].", dtxInfo.getUnitId(), txContext.getGroupId());
        } else {
            // 没有的开启本地事务上下文
            txContext = globalContext.startTx();
        }
        //......
}

这段代码保证了每一个模块下只会有一个 TxContext,换个说法就是假设一个业务逻辑不是操做不一样的数据源,而是对同一个数据源执行屡次相同的操做,那么该数据源对应的模块在 DTX 下会只有一个 TxContext

v2-3726a67ba0cb29ed5e73be58847c1d7b_hd.png

LCN 的事务协调机制

LCN 的口号是:LCN 并不生产事务,LCN 只是本地事务的协调工。你们确定会有个疑问,它不生产事务,那么它是怎么控制各个模块在完成事务的逻辑操做以后不立刻提交,而是等到 TxManager 最后一块儿通知各模块提交的呢?

由于每一个模块都是一个 TxClient,每一个 TxClient 下都有一个链接池,是框架自定义的链接池,对 Connection 使用静态代理的方式进行包装。

public class LcnConnectionProxy implements Connection {
    private Connection connection;
    public LcnConnectionProxy(Connection connection) {
        this.connection = connection;
    }
    /**
     * notify connection
     *
     * @param state transactionState
     * @return RpcResponseState RpcResponseState
     */
    public RpcResponseState notify(int state) {
        try {
            if (state == 1) {
                log.debug("commit transaction type[lcn] proxy connection:{}.", this);
                connection.commit();
            } else {
                log.debug("rollback transaction type[lcn] proxy connection:{}.", this);
                connection.rollback();
            }
            connection.close();
            log.debug("transaction type[lcn] proxy connection:{} closed.", this);
            return RpcResponseState.success;
        } catch (Exception e) {
            log.error(e.getLocalizedMessage(), e);
            return RpcResponseState.fail;
        }
    }
    @Override
    public void setAutoCommit(boolean autoCommit) throws SQLException {
        connection.setAutoCommit(false);
    }
    //......
}

链接池在没有接收到通知事务以前会一直占有着此次分布式事务的链接资源。等到最后 TxManager 通知 TxClient 时,TxClient 才会去执行相应的提交或回滚。因此 LCN 的事务协调机制至关因而拦截了一下链接池,控制了链接的事务提交。

v2-8a7886b1ed99d649419b267b70e48af9_hd.png

LCN 的事务补偿机制

因为咱们不能保证事务每次都正常执行,若是在执行某个业务方法时,本应该执行成功的操做却由于服务器挂机或网络抖动等问题致使事务没有正常提交,这种场景就须要经过补偿来完成事务。

在这种状况下 TxManager 会作一个标示;而后返回给发起方。告诉他本次事务有存在没有通知到的状况,而后 TxClient 再次执行该次请求事务。

最后

欢迎你们一块儿交流,喜欢文章记得关注我点赞转发哟,感谢支持!

相关文章
相关标签/搜索