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