数据库嵌套事务的实现

        Mysql本身(只说InndoDB引擎)是不支持嵌套事务的,就算你开了多个事务,也是按照一层处理。那我们所使用的应用框架,如php的laravel,Java的Spring,都是怎么实现事务嵌套的呢?本文就着这个陈芝麻烂谷子的小知识点啰嗦啰嗦。

下面是一个实验:

#第一次查询
mysql> select * from c_group;
+-------+---------+-----------------+--------------------------------------------+
| id    | user_id | groupname       | avatar                                     |
+-------+---------+-----------------+--------------------------------------------+     |
| 10016 |      -4 | dwd             | dwd                                        |
| 10017 |      12 | wdw             | qee                                        |
| 10019 |     123 | wdw             | qee                                        |
| 10022 |     124 | wdw             | qee                                        |
| 10024 |     125 | wdw             | qee                                        |
| 10026 |     126 | wdw             | qee                                        |
+-------+---------+-----------------+--------------------------------------------+

#开启事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> delete from c_group where id=10016;
Query OK, 1 row affected (0.04 sec)

#第一次提交
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

mysql> delete from c_group where id=10017;
Query OK, 1 row affected (0.01 sec)

#试着操作一下回滚
mysql> rollback;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from c_group;
+-------+---------+-----------------+--------------------------------------------+
| id    | user_id | groupname       | avatar                                     |
+-------+---------+-----------------+--------------------------------------------+    
| 10019 |     123 | wdw             | qee                                        |
| 10022 |     124 | wdw             | qee                                        |
| 10024 |     125 | wdw             | qee                                        |
| 10026 |     126 | wdw             | qee                                        |
+-------+---------+-----------------+--------------------------------------------+

        按照我们所理解的嵌套事务,如果外层回滚了,里层的也应该回滚。实际结果却不是这样,先删除的数据已经被提交了。

        实际上,这里就根本没有外层和里层的概念。当第一次commit之后,整个事务就结束了,没有事务了。后面的delete,如果是autocommit,是默认又开启一个事务。

        不过我们可以借助savepoint来实现嵌套事务,目前很多的应用框架都通过savepoint实现了事务嵌套,比如著名的laravel,这是php领域内的一个比较牛逼的web框架,地为堪比JAVA的spring。

        先了解一下savepoint。

        savepoint是在事务中设置的暂存点,设置后,如果回滚,可以选择性地回滚到某个暂存点。下面是借助savepoint来实现嵌套事务的逻辑:

mysql> select * from c_group;
+-------+---------+-----------------+--------------------------------------------+
| id    | user_id | groupname       | avatar                                     |
+-------+---------+-----------------+--------------------------------------------+         
| 10019 |     123 | wdw             | qee                                        |
| 10022 |     124 | wdw             | qee                                        |
| 10024 |     125 | wdw             | qee                                        |
| 10026 |     126 | wdw             | qee                                        |
+-------+---------+-----------------+--------------------------------------------+
9 rows in set (0.00 sec)

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update c_group set groupname="ff" where id=10019;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

#保存第一个暂存点
mysql> savepoint fistupdate;
Query OK, 0 rows affected (0.00 sec)

mysql> update c_group set groupname="sswdwd" where id=10019;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

#保留第二个暂存点
mysql> savepoint sencondupdate;
Query OK, 0 rows affected (0.00 sec)

mysql> update c_group set groupname="hhtt" where id=10019;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

#回滚到第二个暂存点
mysql> rollback to savepoint sencondupdate;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from c_group;
+-------+---------+-----------------+--------------------------------------------+
| id    | user_id | groupname       | avatar                                     |
+-------+---------+-----------------+--------------------------------------------+
| 10019 |     123 | sswdwd          | qee                                        |
| 10022 |     124 | wdw             | qee                                        |
| 10024 |     125 | wdw             | qee                                        |
| 10026 |     126 | wdw             | qee                                        |
+-------+---------+-----------------+--------------------------------------------+

#回滚到第一个暂存点
mysql> rollback to savepoint fistupdate;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from c_group;
+-------+---------+-----------------+--------------------------------------------+
| id    | user_id | groupname       | avatar                                     |
+-------+---------+-----------------+--------------------------------------------+
| 10019 |     123 | ff              | qee                                        |
| 10022 |     124 | wdw             | qee                                        |
| 10024 |     125 | wdw             | qee                                        |
| 10026 |     126 | wdw             | qee                                        |
+-------+---------+-----------------+--------------------------------------------+

mysql> commit;

        看上面的代码,我没有像第一次那样执行commit,而是反向的操作回滚到已设置的savepoint。通过实验发现,都回滚成功了。上面的实现思想就是目前的应用框架实现嵌套事务的基本思路。

        接着看看laravel的具体实现。

        它的基本思想就是:遇到一个事务,就会发起begin命令;之后的事务都不会再发起begin,并计数+1。如果计数不是0,就增加一个savepoint暂存点。如果是1,就直接执行commit操作,否则不做任何操作。如果遇到任何一层的rollback,都执行rollback命令。

        可以看到,真正执行begin操作和commit操作都只是在最外层,里层只是增加事务暂存点,以便回滚的时候直接回滚。

看下源码:

Laravel执行beginTransaction开启事务:

 public function beginTransaction()
    {
        $this->createTransaction();
        //事务数+1 
        $this->transactions++;

        $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();
        }
    }

  protected function createSavepoint()
    {
        $this->getPdo()->exec(
            $this->queryGrammar->compileSavepoint('trans'.($this->transactions + 1))
        );
    }

接着看commit:

 public function commit()
    {
       //只有最外层的事务才会执行真正的commit操作
        if ($this->transactions == 1) {
            $this->getPdo()->commit();
        }
        //里层的就是减1
        $this->transactions = max(0, $this->transactions - 1);

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

再看rollback:

 public function rollBack($toLevel = null)
    {
       

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

        $this->performRollBack($toLevel);

        $this->transactions = $toLevel;

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


  protected function performRollBack($toLevel)
    {
        if ($toLevel == 0) {
            $this->getPdo()->rollBack();
         //跳到某个暂存点
        } elseif ($this->queryGrammar->supportsSavepoints()) {
            $this->getPdo()->exec(
                $this->queryGrammar->compileSavepointRollBack('trans'.($toLevel + 1))
            );
        }
    }

        上面的回滚操作还可以选择回滚到哪个事务,如果不选择,默认向前回滚。上面的toLevel就是表示层级。

        其实spring实现嵌套事务的基本思想也是一致的。当然,spring的事务管理更加复杂,实现的功能也更多。spring的事务管理是通过AOP代理实现的。它通过事务传播的方式,来实现不同场景的事务要求。多个子事务可以保持在一个事务中,也可以新建事务。

在注解上,可以定义传播方式。@Transactional(propagation = Propagation.XXXX)

事务传播行为类型 说明
PROPAGATION_REQUIRED 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。
PROPAGATION_SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY 使用当前的事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW 新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER 以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

        其实最重要的、且用的最多的就是第一个REQUIRED,NESTED,NEW。第一个很好理解,就是所有的都在一个事务中进行。

        NESTED的就是嵌套事务,也是通过savepoint实现的,这里就不再赘述了,原理和laravel都是一样的。

        这里额外多说一句,Spring的事务是通过代理实现的,所以要在使用中要额外注意,我之前看同事的代码就会经常出现事务失效的问题,出现最多的就是类的内部调用,即某个方法调用同一个类中的某个被Transactional装饰的方法,这肯定是失效的,因为其绕过了代理,直接调用的是目标对象的方法。解决方案网上一搜一大把,比如通过依赖注入自己、使用AopContext.currentProxy()获取代理对象、直接把目标方法迁移到外部类中,本文对此不过多阐述。

相关推荐

  1. 数据库嵌套事务实现

    2024-04-03 11:34:03       35 阅读
  2. rabbitmq事务实现、消费者事务实现

    2024-04-03 11:34:03       45 阅读
  3. 分布式事务实现方式

    2024-04-03 11:34:03       45 阅读
  4. GaussDB 数据库事务管理

    2024-04-03 11:34:03       22 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-04-03 11:34:03       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-04-03 11:34:03       100 阅读
  3. 在Django里面运行非项目文件

    2024-04-03 11:34:03       82 阅读
  4. Python语言-面向对象

    2024-04-03 11:34:03       91 阅读

热门阅读

  1. 设计模式(15):迭代器模式

    2024-04-03 11:34:03       33 阅读
  2. 建造者模式:构建复杂对象的优雅之道

    2024-04-03 11:34:03       37 阅读
  3. 堆积排序算法C代码

    2024-04-03 11:34:03       42 阅读
  4. 5分钟安装docker和docker compose环境

    2024-04-03 11:34:03       34 阅读
  5. 微信小程序-语音输入(录音并播放)

    2024-04-03 11:34:03       39 阅读
  6. STC8H8K64U 学习笔记 - 位运算

    2024-04-03 11:34:03       34 阅读
  7. android 所有音量默认最大

    2024-04-03 11:34:03       34 阅读
  8. 小程序页面滚动?

    2024-04-03 11:34:03       30 阅读
  9. Flink总结

    2024-04-03 11:34:03       40 阅读