Laravel 之嵌套事务 transactions 实现

写在之前

关于mysql 的事务嵌套可以查看这个地址:
https://dev.mysql.com/doc/refman/5.5/en/im...

里面有这么一句话。

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.

大体意思是db不支持事务嵌套,如果你嵌套执行START TRANSACTION时会隐式执行commit

我们做个测试:

mysql>  BEGIN;
Query OK, 0 rows affected (0.16 sec)

mysql> INSERT INTO T2 VALUES(300);
Query OK, 1 row affected (0.29 sec)

mysql>  BEGIN;
Query OK, 0 rows affected (0.04 sec)

mysql> rollback;
Query OK, 0 rows affected (0.04 sec)

mysql> SELECT * FROM T2;
+------+
| ID   |
+------+
|  300 |
+------+
2 rows in set (0.04 sec)

果然,我们直接rollback上面的语句,但是还是执行了查询操作。

laravel之嵌套事务transactions实现

为啥官网不支持,但是 laravel 框架却优雅的实现了事务嵌套,我们来看看它的实现原理。

调用示例:

\DB::beginTransaction();    //主事务
try{
    \DB::beginTransaction(); //子事务
    \DB::insert('insert into T2 set ID=100');
    \DB::rollBack();         //子事务回滚
    \DB::insert('insert into T2 set ID=200');
    \DB::commit();
}catch (\Exception $e) {
    \DB::rollBack();
    echo $e->getMessage();exit;
}

查看执行结果:

mysql> SELECT * FROM T2;
+------+
| ID   |
+------+
|  100 |
+------+
1 row in set (0.05 sec)

说明子事务成功回滚了,下面看下子事务的实现。

代码分析:

laravel/framework/src/Illuminate/Database/Concerns/ManagesTransactions.php 90行

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

        $this->transactions++;

        $this->fireConnectionEvent('beganTransaction');
    }

每调一次beginTransaction会使$this->transactions加1
接着看一下$this->createTransaction();的实现

/**
     * Create a transaction within the database.
     *
     * @return void
     */
    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();
        }
    }

if ($this->transactions == 0) 首先判断是否在事务中。

没有在事务中则执行 $this->getPdo()->beginTransaction()
相当于执行 BEGIN;

在事务中执行 $this->createSavepoint(); 下面是createSavepoint方法的实现。

/**
     * Create a save point within the database.
     *
     * @return void
     */
    protected function createSavepoint()
    {
        $this->getPdo()->exec(
            $this->queryGrammar->compileSavepoint('trans'.($this->transactions + 1))
        );
    }

这里相当于在mysql里执行 SAVEPOINT trans1;

下面看下rollback方法实现:

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

    if ($toLevel < 0 || $toLevel >= $this->transactions) {
        return;
    }
    $this->performRollBack($toLevel);
    $this->transactions = $toLevel;
    $this->fireConnectionEvent('rollingBack');
}

首先rollback会使$this->transactions减一。
然后调用$this->performRollBack

protected function performRollBack($toLevel)
{
    if ($toLevel == 0) {
        $this->getPdo()->rollBack();
    } elseif ($this->queryGrammar->supportsSavepoints()) {
        $this->getPdo()->exec(
            $this->queryGrammar->compileSavepointRollBack('trans'.($toLevel + 1))
        );
    }
}

performRollBack方式实际就是在重新设定savepoint值。

下面看下commit的实现:

/**
 * Commit the active database transaction.
 *
 * @return void
 */
public function commit()
{
    if ($this->transactions == 1) {
        $this->getPdo()->commit();
    }

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

    $this->fireConnectionEvent('committed');
}

commit方法,只有在最外层时才会真正的提交。

总结:

  1. 基本实现原理是 savepoint
  2. 通过$this->transactions对应的数值设定 不同的savepoint实现不同层次嵌套
  3. 只有在最后一个commit时才会真正提交请求。

SAVEPOINT 使用demo如下:

mysql> CREATE TABLE T2(ID INT);
Query OK, 0 rows affected (0.05 sec)

mysql> select * from T2;
Empty set (0.17 sec)

mysql> BEGIN;
Query OK, 0 rows affected (0.04 sec)

mysql> INSERT INTO T2 VALUES(100);
Query OK, 1 row affected (0.04 sec)

mysql> SAVEPOINT trans1;
Query OK, 0 rows affected (0.04 sec)

mysql> INSERT INTO T2 VALUES(200);
Query OK, 1 row affected (0.04 sec)

mysql> ROLLBACK TO SAVEPOINT trans1;
Query OK, 0 rows affected (0.04 sec)

mysql> RELEASE SAVEPOINT trans1;
Query OK, 0 rows affected (0.03 sec)

mysql> commit;
Query OK, 0 rows affected (0.04 sec)

mysql> SELECT * FROM T2;
+------+
| ID   |
+------+
|  100 |
+------+
1 row in set (0.05 sec)
本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由 leo 于 5年前 加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 16

file
结果不是应该是200 ??

5年前 评论
h-o-o 4年前

做了实验 应该是200的 是博主写错了吧

4年前 评论

写的很好。 平时只是在用,没细了解。👍

5年前 评论

遇到同事的代码跟我一起事务,然后就出BUG了,写得很详细

5年前 评论

无论你中间写了多少个rollback我只看你最后一个commit

5年前 评论

file
结果不是应该是200 ??

5年前 评论
h-o-o 4年前

做了实验 应该是200的 是博主写错了吧

4年前 评论

楼主发的结果是对的,savepoint为增加节点,

begin
DML sql #node1 要执行的sql
savepoint node1
DML sql2 #node2 要执行的sql(包含node1 sql)
savepoint node2
DML sql3
rollback to savepoint node1 # 此操作会放弃DML sql2、3,仅执行node1部分
commit
mysql> INSERT INTO T2 VALUES(100);
Query OK, 1 row affected (0.04 sec)

mysql> SAVEPOINT trans1; # 设置trans1节点结束位置
Query OK, 0 rows affected (0.04 sec)

mysql> INSERT INTO T2 VALUES(200); # 被回滚
Query OK, 1 row affected (0.04 sec)

mysql> ROLLBACK TO SAVEPOINT trans1; # 回滚至trans1节点结束位置
Query OK, 0 rows affected (0.04 sec)

mysql> RELEASE SAVEPOINT trans1;
Query OK, 0 rows affected (0.03 sec)
4年前 评论
tanyang 4年前
yichengo (作者) 4年前
yichengo (作者) 4年前
tanyang 4年前
tanyang 4年前
yichengo (作者) 4年前
tanyang 4年前

失败了,ID不回滚咋整

4年前 评论

@Koba 如果id回滚,那事务就只能顺序执行了。如果不想回滚,那就不使用mysql的自增id,而是改用自己写自增计数器,或抛弃自增id,直接使用uuid等做为主键。

4年前 评论

@Rv_tree 我也是200,我说怎么和我一直理解的不一样,赶紧试了下。博主再检查下代码吧。

4年前 评论
# 开启事务
DB::connection('DB1')->beginTransaction();
DB::connection('DB2')->beginTr2nsaction();
#返回值判断
if($res){
    #提交事务
    DB::connection('DB1')->commit();
    DB::connection('DB2')->commit();
 }else{
   #回滚事务
    DB::connection('DB1')->rollBack();
    DB::connection('DB2')->rollBack();
}
3年前 评论
try {
    // 主事务
    DB::connection('DB1')->transaction(function () use ($data) {
        // DB1数据库操作
        // 子事务
        try {
            DB::connection('DB2')->transaction(function () use ($user) {
                    // DB2数据库操作
            });
        } catch (QueryException $e) {
             throw new ApiException($e->getMessage());
        }
    }, 3);
} catch (QueryException $e) {
      throw new ApiException($e->getMessage());
}
3年前 评论

laravel 的savepoint 应该是这样的

SELECT *From T2;
BEGIN;

SAVEPOINT trans1;

INSERT INTO T2 VALUES (100);

SAVEPOINT trans2;

ROLLBACK TO SAVEPOINT trans1;

INSERT INTO T2 VALUES (200);

SAVEPOINT trans3;

COMMIT;

SELECT *FROM T2;

3年前 评论

事务嵌套,锁是什么时候释放呢?真正提交的时候嘛?

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!