数据库嵌套事务的实现

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

相关推荐
小陈工10 小时前
Python Web开发入门(十七):Vue.js与Python后端集成——让前后端真正“握手言和“
开发语言·前端·javascript·数据库·vue.js·人工智能·python
科技小花14 小时前
数据治理平台架构演进观察:AI原生设计如何重构企业数据管理范式
数据库·重构·架构·数据治理·ai-native·ai原生
一江寒逸14 小时前
零基础从入门到精通MySQL(中篇):进阶篇——吃透多表查询、事务核心与高级特性,搞定复杂业务SQL
数据库·sql·mysql
D4c-lovetrain14 小时前
linux个人心得22 (mysql)
数据库·mysql
阿里小阿希15 小时前
CentOS7 PostgreSQL 9.2 升级到 15 完整教程
数据库·postgresql
荒川之神15 小时前
Oracle 数据仓库雪花模型设计(完整实战方案)
数据库·数据仓库·oracle
做个文艺程序员15 小时前
MySQL安全加固十大硬核操作
数据库·mysql·安全
不吃香菜学java15 小时前
Redis简单应用
数据库·spring boot·tomcat·maven
一个天蝎座 白勺 程序猿15 小时前
Apache IoTDB(15):IoTDB查询写回(INTO子句)深度解析——从语法到实战的ETL全链路指南
数据库·apache·etl·iotdb
不知名的老吴15 小时前
Redis的延迟瓶颈:TCP栈开销无法避免
数据库·redis·缓存