MySQL 嵌套事务、PHP+MySQL嵌套事务、ThinkPHP 嵌套事务、Laravel 嵌套事务

MySQL 嵌套事务、PHP+MySQL嵌套事务、ThinkPHP 嵌套事务、Laravel 嵌套事务

在 MySQL 的官方文档中有明确的说明不支持嵌套事务:

Transactions cannot be nested. This is a consequence of the implicit commit performed for any current transaction when you issue a START TRANSACTION statement or one of its synonyms.

翻译:

当执行一个START TRANSACTION指令时,会隐式的执行一个commit操作。 所以我们就要在系统架构层面来支持事务的嵌套。

所幸的是在一些成熟的ORM框架中都做了对嵌套的支持,比如 ThinkPHP 和 Laravel等。

如果多层嵌套,transTimes会进行标记。如果嵌套,只有最外层的事务是生效的。这种事务嵌套处理方式跟laravel是一模一样的。

ThinkPHP 6.x 嵌套事务的逻辑代码

文件位置:vendor/topthink/think-orm/src/db/PDOConnection.php

    /**
     * 启动事务
     * @access public
     * @return void
     * @throws \PDOException
     * @throws \Exception
     */
    public function startTrans(): void
    {
        try {
            $this->initConnect(true);

            ++$this->transTimes;

            if (1 == $this->transTimes) {
                $this->linkID->beginTransaction();
            } elseif ($this->transTimes > 1 && $this->supportSavepoint()) {
                $this->linkID->exec(
                    $this->parseSavepoint('trans' . $this->transTimes)
                );
            }
            $this->reConnectTimes = 0;
        } catch (\Exception $e) {
            if ($this->reConnectTimes < 4 && $this->isBreak($e)) {
                --$this->transTimes;
                ++$this->reConnectTimes;
                $this->close()->startTrans();
            } else {
                throw $e;
            }
        }
    }

    /**
     * 用于非自动提交状态下面的查询提交
     * @access public
     * @return void
     * @throws PDOException
     */
    public function commit(): void
    {
        $this->initConnect(true);

        if (1 == $this->transTimes) {
            $this->linkID->commit();
        }

        --$this->transTimes;
    }

    /**
     * 事务回滚
     * @access public
     * @return void
     * @throws PDOException
     */
    public function rollback(): void
    {
        $this->initConnect(true);

        if (1 == $this->transTimes) {
            $this->linkID->rollBack();
        } elseif ($this->transTimes > 1 && $this->supportSavepoint()) {
            $this->linkID->exec(
                $this->parseSavepointRollBack('trans' . $this->transTimes)
            );
        }

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

Laravel 8.x 嵌套事务的逻辑代码

文件位置:/laravel/framework/blob/8.x/src/Illuminate/Database/DatabaseTransactionsManager.php

    /**
     * Start a new database transaction.
     *
     * @param  string  $connection
     * @param  int  $level
     * @return void
     */
    public function begin($connection, $level)
    {
        $this->transactions->push(
            new DatabaseTransactionRecord($connection, $level)
        );
    }

    /**
     * Rollback the active database transaction.
     *
     * @param  string  $connection
     * @param  int  $level
     * @return void
     */
    public function rollback($connection, $level)
    {
        $this->transactions = $this->transactions->reject(function ($transaction) use ($connection, $level) {
            return $transaction->connection == $connection &&
                   $transaction->level > $level;
        })->values();
    }

    /**
     * Commit the active database transaction.
     *
     * @param  string  $connection
     * @return void
     */
    public function commit($connection)
    {
        $this->transactions = $this->transactions->reject(function ($transaction) use ($connection) {
            if ($transaction->connection == $connection) {
                $transaction->executeCallbacks();

                return true;
            }

            return false;
        })->values();
    }

文件位置:/laravel/framework/blob/8.x/src/Illuminate/Database/DatabaseTransactionRecord.php

<?php

namespace Illuminate\Database;

class DatabaseTransactionRecord
{
    /**
     * The name of the database connection.
     *
     * @var string
     */
    public $connection;

    /**
     * The transaction level.
     *
     * @var int
     */
    public $level;

    /**
     * The callbacks that should be executed after committing.
     *
     * @var array
     */
    protected $callbacks = [];

    /**
     * Create a new database transaction record instance.
     *
     * @param  string  $connection
     * @param  int  $level
     * @return void
     */
    public function __construct($connection, $level)
    {
        $this->connection = $connection;
        $this->level = $level;
    }

    /**
     * Register a callback to be executed after committing.
     *
     * @param  callable  $callback
     * @return void
     */
    public function addCallback($callback)
    {
        $this->callbacks[] = $callback;
    }

    /**
     * Execute all of the callbacks.
     *
     * @return void
     */
    public function executeCallbacks()
    {
        foreach ($this->callbacks as $callback) {
            call_user_func($callback);
        }
    }

    /**
     * Get all of the callbacks.
     *
     * @return array
     */
    public function getCallbacks()
    {
        return $this->callbacks;
    }
}

php+mysql 事务多层嵌套的实现方式

msyql 本身不支持事务嵌套的,但是可以按照嵌套事务的思路变相实现事务多层嵌套。
开启事务时 先 mark 一个标志,每嵌套一次,就将该值加 1,但是开启事务这个操作只在 mark=1 时才真的去实现,其他只是累加。
而提交时,肯定是从最内层开始提交,每提交一次,mark 减去 1,直到 mark=1 时,才真的去实现提交。
回滚也是如此。
代码如下:

public function beginTransaction() {    
    ++$this->transactions;    
    if ($this->transactions==1){        
        $this->pdo->beginTransaction();    
    } 
} 
public function rollBack() { 
    if ($this->transactions ==1) { 
        $this->transactions ==0; 
        $this->pdo->rollBack(); 
    } else { 
        --$this->transactions; 
    } 
}
public function commit() { 
    if ($this->transactions ==1){
        $this->pdo->commit();
    }
    --$this->transactions; 
}

MySQL 使用 savepoint 和 rollback to 的语句实现事务嵌套

在 MySQL 多次开启事务,出现了数据错乱问题,伪代码如下:

begin;
# 操作逻辑代码块1
begin;
# 操作逻辑代码块2
rollback;

执行完后出现了操作1的数据真正写入,只有操作2的数据回滚了。在第一个事务没有提交或回滚时,再开启第二个事务时,会自动提交第一个事务。

这明显不符合心理预期,而且也无法回滚一部分操作。那么问题来了,MySQL 支不支持事务嵌套呢?

这个问题很难准确回答支持还是不支持!

首先,调用多次begin的写法,在 MySQL 里肯定是无法首先事务嵌套的。经过群内一位朋友的提醒,了解到 MySQL 中有一个叫 savepoint 和 rollback to 的语句。

示例代码:

DROP TABLE IF EXISTS `test`;
CREATE TABLE `test` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

begin;
insert into `test`(`name`) values('111');
SAVEPOINT p1;
insert into `test`(`name`) values('222');
ROLLBACK TO p1;
commit;

最终执行结果,test表中只有 111 这个数据,实现了部分操作的回滚操作。同理也避免了多次开启事务,导致前一个事务被提交的问题。

可能savepointrollback to语句并不能称之为事务嵌套,也不能说 MySQL 是支持还是不支持事务嵌套。总之通过savepointrollback to,是可以用来达到一些事务嵌套特性的。

posted on 2021-02-07 12:40  sochishun  阅读(663)  评论(0编辑  收藏  举报