数据库嵌套事务的实现

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

下面是一个实验:

sql 复制代码
#第一次查询
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来实现嵌套事务的逻辑:

sql 复制代码
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开启事务:

php 复制代码
 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:

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

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

再看rollback:

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

相关推荐
生活很暖很治愈19 分钟前
C51数字时钟/日历---LCD1602液晶显示屏
数据库·单片机·mongodb
YONG823_API28 分钟前
1688商品数据采集API的测试对接步骤分享(提供免费测试key)
开发语言·数据库·爬虫·python·数据挖掘
码上一元1 小时前
掌握 Spring 事务管理:深入理解 @Transactional 注解
数据库·spring
程序猿毕设源码分享网1 小时前
基于springboot停车场管理系统源码和论文
数据库·spring boot·后端
YiSLWLL1 小时前
Django+Nginx+uwsgi网站使用Channels+redis+daphne实现简单的多人在线聊天及消息存储功能
服务器·数据库·redis·python·nginx·django
.生产的驴1 小时前
Docker Seata分布式事务保护搭建 DB数据源版搭建 结合Nacos服务注册
数据库·分布式·后端·spring cloud·docker·容器·负载均衡
盖盖衍上1 小时前
4.4 MySQL 触发器(Trigger)
数据库·mysql
清心歌1 小时前
Redis入门(九)
数据库·redis
superman超哥1 小时前
Oralce数据库巡检SQL脚本
数据库·oracle·性能优化·dba·rdbms·巡检
墨城烟柳ベ旧人殇1 小时前
MySQL数据库6——SQL优化
数据库·sql·mysql