数据库嵌套事务的实现

本文探讨了MysqlInnoDB不支持嵌套事务的情况,以及如何利用savepoint在PHPLaravel和JavaSpring框架中实现类似功能。通过savepoint设置暂存点,实现在事务中的回滚和保留操作。
摘要由CSDN通过智能技术生成

        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()获取代理对象、直接把目标方法迁移到外部类中,本文对此不过多阐述。

  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值