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