阅读 90

【Laravel-海贼王系列】第十八章,事物的嵌套运行原理

简介

  • MySQL 本身是不支持事物嵌套的,Laravel 用了一些技巧进行支持。

DB 对象到底是谁

DB 的启动周期。

【1、绑定,在 App 启动的时候绑定的对象】 

'db' => 'Illuminate\Database\DatabaseManager'

【2、获取 Connection 对象】

'DatabaseManager'->connection() => '\Illuminate\Database\Connection'

【3、Connection 类头部】

class Connection implements ConnectionInterface {
      use DetectsConcurrencyErrors,
         DetectsLostConnections,
         Concerns\ManagesTransactions;
       ...
}

【4、事物相关特质类,包含了所有事物相关的操作】 

'Illuminate\Database\Concerns\ManagesTransactions'
复制代码

开启事物

当我开始执行 DB::beginTransactions() 

1、 创建事物代码。

public function beginTransaction()
{
    $this->createTransaction();

    $this->transactions++; // 计数器 +1

    $this->fireConnectionEvent('beganTransaction'); // 一个空的事件,不管它
}

protected function createTransaction()
    {
        if ($this->transactions == 0) {
            try {
                $this->getPdo()->beginTransaction();
            } catch (Exception $e) {
                $this->handleBeginTransactionException($e);
            }
        } elseif ($this->transactions >= 1 && $this->queryGrammar->supportsSavepoints()) {
            $this->createSavepoint(); 
        }
    }

【如果$this->transactions == 0】我们将直接调用 \PDO 对象开启一个事物

【如果$this->transactions >= 1】表示当前这个请求周期里面已经开启过事物

此时将会调用

protected function createSavepoint()
{
    $this->getPdo()->exec(
        // 保存位置,'SAVEPOINT trans'.($this->transactions + 1) 执行保存的语句
        $this->queryGrammar->compileSavepoint('trans'.($this->transactions + 1))
    );
}

public function compileSavepoint($name)
{
    return 'SAVEPOINT '.$name;
}


依次类推每叠加依次执行 DB::beginTransactions()
那么就会增加一个新的存储点

SAVEPOINT trans 1
$this->transactions ++ ;
SAVEPOINT trans 2
$this->transactions ++ ;
...
这个计数器是关键!
目前创建完成事物。
复制代码

提交事物

public function commit()
{
    if ($this->transactions == 1) {
        $this->getPdo()->commit();
    }

    $this->transactions = max(0, $this->transactions - 1);

    $this->fireConnectionEvent('committed'); // 一个空的事件不理会它
}

【如果 $this->transactions == 1】 当前只存在一个开启的事物,直接提交即可,重置 $this->transactions 为 0。

【如果 $this->transactions > 1】 当前存在多个开启的事物,将 $this->transactions 进行减 1 操作。

这里可以看出来提交事物逻辑还是很简单的,不存在嵌套就直接提交,否则只是修改计数器。

复制代码

回滚事物

public function rollBack($toLevel = null)
{
        $toLevel = is_null($toLevel)
                    ? $this->transactions - 1
                    : $toLevel; 

        if ($toLevel < 0 || $toLevel >= $this->transactions) {
            return; // 计数器运算之后数值异常将 return
        }

        try {
            $this->performRollBack($toLevel);
        } catch (Exception $e) {
            $this->handleRollBackException($e);
        }

        $this->transactions = $toLevel;

        $this->fireConnectionEvent('rollingBack'); // 不管它
}

这里的执行逻辑是

【未指定 rollBack 的层数时计数器值减 1 并且赋值给 $toLevel】

【执行 performRollBack($toLevel) 根据指定的或者当前的层数进行回滚】

 protected function performRollBack($toLevel)
{
    if ($toLevel == 0) {
        // '如果当前以及是最顶层了,直接 rollBack() 事物'
        $this->getPdo()->rollBack();
    } elseif ($this->queryGrammar->supportsSavepoints()) {
       // 不是最顶层时,执行 'ROLLBACK TO SAVEPOINT trans'.($this->transactions + 1) ,
       // 这里最关键的部分就是返回之前设定的存储点
       // 想象一下打游戏的时候那些存档的点,当你返回到指定位置时,那么后面所有的资料都将回滚!
        $this->getPdo()->exec(
            $this->queryGrammar->compileSavepointRollBack('trans'.($toLevel + 1))
        );
    }
}
【重置 $this->transactions = $toLevel 到当前回滚的位置层数】
复制代码

变化

事物的嵌套其实逻辑上理解起来很简单,很巧妙的用了 MySQL 的 SAVEPOINT 。

我们举个例子来看一下内部的数值改变。

DB::beginTransactions();
Model::create(['tag'=>'top level']); // $this->transactions = 1

DB::beginTransactions();
Model::create(['tag'=>'trans2']); // $this->transactions = 2

DB::beginTransactions();
Model::create(['tag'=>'trans3']); // $this->transactions = 3

DB::beginTransactions();
Model::create(['tag'=>'trans4']); // $this->transactions = 4

DB::rollBack(1); // 返回到 $this->transactions = 1  的位置。

DB::commit(); // 插入了 top Level 的记录。
 
复制代码

拓展

关于事物部分提交以及回滚到指定位置,MySQL 提供的解决方式。

Laravel 嵌套事物也是用此原理实现。MySQL SAVEPOINT

关注下面的标签,发现更多相似文章
评论