Mysql && Laravel 事务总结

之前维护一个远古laravel5.2的项目,发现一个问题,代码里写了一个 for 循环,然后每次循环都开启一个事务,简单校验后,异常情况直接continue 继续执行,没有任何的 rollback 或者 commit 的操作,最后发生奇怪的现象,操作了2条数据 第一条失败了 直接返回 然后继续执行循环 第二条成功了 然后 commit 循环结束 这时候数据库并没有写入那条成功的记录(这种操作本身就不合理,不过不在本文讨论范围!)

Mysql 中的事务总结

Mysql不支持 嵌套事务。对于事务 Mysql 的存储引擎 MyISAM 是不支持事务的 如果你需要使用事务 需要是像 InnoDB 类的存储引擎。还有一点一般来说最好不要使用嵌套事务 有毒 有毒 有毒!当然如果写代码中不小心使用到了 比如先定义了一个 function A 开启了事务 然后在写了个function B 也开启了事务 并且调用 A() 方法 这种就形成了嵌套事务….

注意 下面的设置 开启/关闭 自动提交都是针对当前会话中的 如果你新开一个会话 来查询 @@autocommit 的值 并不受当前设置的影响 全局的设置方法我没有研究!

Mysql 的自动提交

在默认情况下 Mysql是开启了自动提交的 也就是每一个数据库操作和查询都是一个 事务, Mysql会在每一个 Query 后执行一个 Commit 来提交写入数据库

你可以使用

mysql> select @@autocommit;
+--------------+
| @@autocommit |
+--------------+
|            1 |
+--------------+
1 row in set (0.00 sec)

来查询 当前会话中的自动提交设置 默认情况下是 1 也可以是 On 都是一样的

然后如果你想要关闭自动提交的话可以使用

mysql> set @@autocommit = 0;
Query OK, 0 rows affected (0.00 sec)

来关闭当前会话中的自动提交 当然也可以使用 set @@autocommit = Off; 都是一样的

事务外(没有开启事务的情况) Mysql的自动提交测试

一.测试事务外开启自动提交 set @@autocommit = 1;

自动提交1

可以看到左边的会话中添加了一条记录 然后在右边的会话可以查询可以立马看到 说话数据已经写入到了数据库中

二.测试事务外关闭自动提交 set @@autocommit = 0;

自动提交2

这个时候我们可以看到 左边的会话中已经有记录了 但是右边的会话中确实是没有记录的 说明他还没有真正的写入到数据库中

自动提交3

然后我们在去执行一次 commit 然后数据就真正的提交了 写入到了数据库中

总结:

在Mysql中 没有开启事务的情况下 autocommit 的配置只是告诉数据库,autocommit = 1 的时候,每个数据库操作和查询都是一个 事务, 每一个 Query 都自动带一个隐性的 commit 以保证数据真正的写入到了数据库
autocommit = 0 的时候整个会话是一个事务 在你执行 commit / rollback 之前 所有的操作都是在事务内 另一个会话的访问是不受当前这个会话影响的 也就是当前会话的东西没有真正写入到数据库中

事务内 Mysql 的自动提交测试

一.测试事务内开启自动提交 set @@autocommit = 1;

自动提交4

这时候我们可以看到在右边的会话中并没有看到数据 也就是他并没有真正的提交到数据库中

自动提交5

这是我们执行一下 commit 数据才真正添加到数据库

二.测试事务内关闭自动提交 set @@autocommit = 0;

自动提交6

可以看出和上面一样 数据更加不会提交到数据库中 右边的会话中是不会看到数据的

自动提交7

最后执行一次 commit 数据真正加到数据库中 右边的会话中也可以读取到数据了

三.测试事务内 所谓的嵌套事务

自动提交8

整个流程 ①开始一个事务 ②插入一条记录 值700 ③然后开启第二个事务 这个时候其实被Mysql执行了一次 commit 数据已经提交到了数据库 所以右边的会话中是可以查询到的 ④第二个插入 值800 然后右边的会话中查询是没有800这条记录的 所有说明没有被提交到数据库 ⑤回滚第二条记录 事务结束 右边的会话中获取最终的数据

总结:

对于Mysql而言,他是不支持 嵌套事务 的,在上一个事务未关闭的情况下,开启一个新的事务 它会隐式的对上一个事务 执行 commit 操作 数据会真正的写入到数据库中,在这种所谓的嵌套事务中 自动提交的参数 autocommit = 0 或者 1 对它都没有影响!

Mysql的 savepoint 功能

创建 savepoint your_point_name

回滚 rollback to savepoint your_point_name 他的回滚范围就是创建的point 到rollback的这一段的范围

删除 release savepoint your_point_name;

如下来 先来一个测试

自动提交10

讲解下 上图例子中的 savepoint 用法

第一步.我们开启了一个事务 首先我们插入一条记录 值 100 在右边的会话中我们并没有查询到数据库有记录

第二步.然后创建了一个 savepoint 取名 sp1

第三步.然后插入第二条记录 值 200 这时我们在右边的会话中也是查询不到数据库有记录的 但是当前事务中是可以看到有2条记录了

第四步.然后我们回滚到记录点 sp1 在查询 可以看到只有一条记录了 同时右边的会话中也是没有记录的

第五步.最后我们提交 由于 已经提前回滚了 sp1 而记录100的记录在 sp1 范围外 所以我们提交了 只有第一条记录会写入到数据库 同时右边的会话中是可以查询到结果的

总结:

在Mysql中 savepoint 的作用范围 只是从创建到 rollback 这一段 其作用就是保证中间这一段的数据 当然如果你在事务内使用他 他是受包裹他的事务的 提交或者回滚 影响的

Laravel 中的事务总结

先来一个小测试 来重复一开始的那个问题 所谓的嵌套循环 来看看 Laravel 是怎么处理的

/**
* this is a test
*/
public function test()
{
    //循环2次即可
    for ($i = 1; $i <= 2; $i++) {
        $this->add($i);
    }

    $list = Test::get();
    dd($list->toArray());
}

public function add($i)
{
    DB::beginTransaction();
    //return 中断第一个
    if ($i == 1) {
        return false;
    }

    $a = Test::create([
        'n' => $i,
    ]);

    if ($a !== false) {
        DB::commit();
    } else {
        DB::rollback();
    }
}

分析过程

1.首先我写了一个循环 执行了2次 为了模拟效果 第一次循环进入添加的方法直接 return 了 也就只是开启了 第一个事务
2.继续走 return 返回 然后开始第二次循环进入 开启 第二个事务 然后正常添加数据记录 然后执行 commit
3.循环结束打印 获取表数据 整个测试结束
4.我们来看下结果 自动提交9
5.在数组的打印中有一条记录 但是我们另外开一个会话去查询数据库 却是空 说明 数据并没有真正的写入到数据库中 按道理说 如果是Mysql的嵌套循环 第二次事务开启的时候 上一次的事务内容就会被提交了 但是现在事实却并没有 到底发生了什么鬼事情 于是挖了下 Laravel 底层的数据库处理 下面来分析一波

Laravel中的事务处理

当前测试框架Laravel5.2

在框架内的目录 \vendor\laravel\framework\src\Illuminate\Database 下就是Laravel的相关数据库处理 先来看下 Connection.php 文件

可以看到 在 100 行的地方有一个事务的变量 看说明应该是一个活动事务的数量记录数

    /**
     * The number of active transactions.
     *
     * @var int
     */
    protected $transactions = 0;

Laravel 的 beginTransaction 方法处理

然后我们来找下 beginTransaction 方法 在 570 行的地方

    /**
     * Start a new database transaction.
     *
     * @return void
     * @throws Exception
     */
    public function beginTransaction()
    {
        ++$this->transactions;

        if ($this->transactions == 1) {
            try {
                $this->getPdo()->beginTransaction();
            } catch (Exception $e) {
                --$this->transactions;

                throw $e;
            }
        } elseif ($this->transactions > 1 && $this->queryGrammar->supportsSavepoints()) {
            $this->getPdo()->exec(
                $this->queryGrammar->compileSavepoint('trans'.$this->transactions)
            );
        }

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

1.先来看 beginTransaction 方法的处理。 进入后直接把当前会话的活动 事务数量 + 1 然后判断事务数量 如果是只有一个的时候 就是那个 if = 1 的判断 这个时候才会真正的去开启一个事务. 如果发生异常把 事务数量 - 1 然后抛出异常
2.如果事务数量 大于 1 的话 并且支持 savepoint (这个东西参考上面的 savepoint 用法讲解) 他就会添加一个 savepoint 保存记录点
3.最后触发连接事件 这个没什么
4.从这里可以看出来 如果我们在代码中发生了嵌套事务 其实Laravel是把他当作 一个整体的 一个大的事务 如果你后面又开启了事务 他并没有去真正的在开启一个事务 而只是把事务的 数量+1 记录了下 然后开启了 savepoint 来做记录点

Laravel 的 commit 方法处理

来看下 commit 方法 在同一个文件中 596 行

    /**
     * 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');
    }

1.进入 commit 方法 他首先判断了当前的事务数量 是否只有一个 如果只有一个的时候才会去真正的提交 去执行 Mysql 的 commit 操作
2.如果当前的事务不是只有一个 他就把 事务数量进行了减1 操作
3.触发事件监听

Laravel 的 rollback 方法处理

最后来看下 rollback 方法的处理 在 612 行

    /**
     * Rollback the active database transaction.
     *
     * @return void
     */
    public function rollBack()
    {
        if ($this->transactions == 1) {
            $this->getPdo()->rollBack();
        } elseif ($this->transactions > 1 && $this->queryGrammar->supportsSavepoints()) {
            $this->getPdo()->exec(
                $this->queryGrammar->compileSavepointRollBack('trans'.$this->transactions)
            );
        }

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

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

1.进入 rollback 方法 他也是首先判断了当前事务数量 如果只有一个事务 则会真正的去执行回滚 去执行 Mysql 的 rollback 操作
2.如果事务数量 大于1 并且支持 savepoint 他就去根据数量把当前的内容回滚到对应的 savepoint 记录点
3.然后把 事务的数量进行减1 操作
4.触发事件监听

总结

在Laravel框架中 对于 嵌套事务 框架本身做了处理。当你开启一个事务的时候 Laravel会先判断当前会话中事务数量,如果只有一个 他会真正的去开启一个事务,如果已经存在了一个事务 后续的在执行 beginTransaction 都是只是做了事务数量的计数+1 然后创建 savepoint 记录保存点 来继续操作,对于嵌套事务中的 commitrollback 他的处理同样。在这个过程中间执行的commitrollback 都只是把事务数量减1操作 当然rollback 有个回滚 savepoint记录保存点的操作。

只有当事务数量只有 一个 的时候 也就是回到了一开始创建的事务 这样就对应上了 这是一个完整的事务(因为他只创建了一个事务嘛) 这个时候操作的 commit 或者 rollback 操作才会真正的对数据库来提交 然后 结束整个事务 (只有这个时候的提交才会对数据库生效 真正的写入到数据库中 别的会话也就可以访问到了)

php
本作品采用《CC 协议》,转载必须注明作者和本文链接
铁甲依然在
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
讨论数量: 2

你用的版本是最开始的处理方式,begin和commit需要成对出现,少一次提交,这个事务会失败,6之后的事务这个逻辑有些改动,事务不管是闭包还是单独命令,最终也是成对出现才能成功,嵌套里面新版本才用了mysql的savepoin策略处理的,也是以最后一次提交数量为0为标准去提交,如果执行了只是生成一个undolog文件,但数据库并没持久化,这个位置tp处理机制也是一样,嵌套事务少写提交不行,推荐闭包方式嵌套,让begin和commit 成对出现

1个月前 评论

在跨库事务和事务隔离级别方面,可以补充一下

1个月前 评论
raybon 1个月前

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