• 数据库嵌套事务的实现


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

    下面是一个实验:

    1. #第一次查询
    2. mysql> select * from c_group;
    3. +-------+---------+-----------------+--------------------------------------------+
    4. | id | user_id | groupname | avatar |
    5. +-------+---------+-----------------+--------------------------------------------+ |
    6. | 10016 | -4 | dwd | dwd |
    7. | 10017 | 12 | wdw | qee |
    8. | 10019 | 123 | wdw | qee |
    9. | 10022 | 124 | wdw | qee |
    10. | 10024 | 125 | wdw | qee |
    11. | 10026 | 126 | wdw | qee |
    12. +-------+---------+-----------------+--------------------------------------------+
    13. #开启事务
    14. mysql> begin;
    15. Query OK, 0 rows affected (0.00 sec)
    16. mysql> begin;
    17. Query OK, 0 rows affected (0.00 sec)
    18. mysql> delete from c_group where id=10016;
    19. Query OK, 1 row affected (0.04 sec)
    20. #第一次提交
    21. mysql> commit;
    22. Query OK, 0 rows affected (0.00 sec)
    23. mysql> delete from c_group where id=10017;
    24. Query OK, 1 row affected (0.01 sec)
    25. #试着操作一下回滚
    26. mysql> rollback;
    27. Query OK, 0 rows affected (0.00 sec)
    28. mysql> select * from c_group;
    29. +-------+---------+-----------------+--------------------------------------------+
    30. | id | user_id | groupname | avatar |
    31. +-------+---------+-----------------+--------------------------------------------+
    32. | 10019 | 123 | wdw | qee |
    33. | 10022 | 124 | wdw | qee |
    34. | 10024 | 125 | wdw | qee |
    35. | 10026 | 126 | wdw | qee |
    36. +-------+---------+-----------------+--------------------------------------------+

            按照我们所理解的嵌套事务,如果外层回滚了,里层的也应该回滚。实际结果却不是这样,先删除的数据已经被提交了。

            实际上,这里就根本没有外层和里层的概念。当第一次commit之后,整个事务就结束了,没有事务了。后面的delete,如果是autocommit,是默认又开启一个事务。

            不过我们可以借助savepoint来实现嵌套事务,目前很多的应用框架都通过savepoint实现了事务嵌套,比如著名的laravel,这是php领域内的一个比较牛逼的web框架,地为堪比JAVA的spring。

            先了解一下savepoint。

            savepoint是在事务中设置的暂存点,设置后,如果回滚,可以选择性地回滚到某个暂存点。下面是借助savepoint来实现嵌套事务的逻辑:

    1. mysql> select * from c_group;
    2. +-------+---------+-----------------+--------------------------------------------+
    3. | id | user_id | groupname | avatar |
    4. +-------+---------+-----------------+--------------------------------------------+
    5. | 10019 | 123 | wdw | qee |
    6. | 10022 | 124 | wdw | qee |
    7. | 10024 | 125 | wdw | qee |
    8. | 10026 | 126 | wdw | qee |
    9. +-------+---------+-----------------+--------------------------------------------+
    10. 9 rows in set (0.00 sec)
    11. mysql> begin;
    12. Query OK, 0 rows affected (0.00 sec)
    13. mysql> update c_group set groupname="ff" where id=10019;
    14. Query OK, 1 row affected (0.00 sec)
    15. Rows matched: 1 Changed: 1 Warnings: 0
    16. #保存第一个暂存点
    17. mysql> savepoint fistupdate;
    18. Query OK, 0 rows affected (0.00 sec)
    19. mysql> update c_group set groupname="sswdwd" where id=10019;
    20. Query OK, 1 row affected (0.00 sec)
    21. Rows matched: 1 Changed: 1 Warnings: 0
    22. #保留第二个暂存点
    23. mysql> savepoint sencondupdate;
    24. Query OK, 0 rows affected (0.00 sec)
    25. mysql> update c_group set groupname="hhtt" where id=10019;
    26. Query OK, 1 row affected (0.00 sec)
    27. Rows matched: 1 Changed: 1 Warnings: 0
    28. #回滚到第二个暂存点
    29. mysql> rollback to savepoint sencondupdate;
    30. Query OK, 0 rows affected (0.00 sec)
    31. mysql> select * from c_group;
    32. +-------+---------+-----------------+--------------------------------------------+
    33. | id | user_id | groupname | avatar |
    34. +-------+---------+-----------------+--------------------------------------------+
    35. | 10019 | 123 | sswdwd | qee |
    36. | 10022 | 124 | wdw | qee |
    37. | 10024 | 125 | wdw | qee |
    38. | 10026 | 126 | wdw | qee |
    39. +-------+---------+-----------------+--------------------------------------------+
    40. #回滚到第一个暂存点
    41. mysql> rollback to savepoint fistupdate;
    42. Query OK, 0 rows affected (0.00 sec)
    43. mysql> select * from c_group;
    44. +-------+---------+-----------------+--------------------------------------------+
    45. | id | user_id | groupname | avatar |
    46. +-------+---------+-----------------+--------------------------------------------+
    47. | 10019 | 123 | ff | qee |
    48. | 10022 | 124 | wdw | qee |
    49. | 10024 | 125 | wdw | qee |
    50. | 10026 | 126 | wdw | qee |
    51. +-------+---------+-----------------+--------------------------------------------+
    52. mysql> commit;

            看上面的代码,我没有像第一次那样执行commit,而是反向的操作回滚到已设置的savepoint。通过实验发现,都回滚成功了。上面的实现思想就是目前的应用框架实现嵌套事务的基本思路。

            接着看看laravel的具体实现。

            它的基本思想就是:遇到一个事务,就会发起begin命令;之后的事务都不会再发起begin,并计数+1。如果计数不是0,就增加一个savepoint暂存点。如果是1,就直接执行commit操作,否则不做任何操作。如果遇到任何一层的rollback,都执行rollback命令。

            可以看到,真正执行begin操作和commit操作都只是在最外层,里层只是增加事务暂存点,以便回滚的时候直接回滚。

    看下源码:

    Laravel执行beginTransaction开启事务:

    1. public function beginTransaction()
    2. {
    3. $this->createTransaction();
    4. //事务数+1
    5. $this->transactions++;
    6. $this->fireConnectionEvent('beganTransaction');
    7. }
    8. protected function createTransaction()
    9. {
    10. //当前连接第一个事务,开启事务
    11. if ($this->transactions == 0) {
    12. try {
    13. $this->getPdo()->beginTransaction();
    14. } catch (Exception $e) {
    15. $this->handleBeginTransactionException($e);
    16. }
    17. //创建暂存点
    18. } elseif ($this->transactions >= 1 && $this->queryGrammar->supportsSavepoints()) {
    19. $this->createSavepoint();
    20. }
    21. }
    22. protected function createSavepoint()
    23. {
    24. $this->getPdo()->exec(
    25. $this->queryGrammar->compileSavepoint('trans'.($this->transactions + 1))
    26. );
    27. }

    接着看commit:

    1. public function commit()
    2. {
    3. //只有最外层的事务才会执行真正的commit操作
    4. if ($this->transactions == 1) {
    5. $this->getPdo()->commit();
    6. }
    7. //里层的就是减1
    8. $this->transactions = max(0, $this->transactions - 1);
    9. $this->fireConnectionEvent('committed');
    10. }

    再看rollback:

    1. public function rollBack($toLevel = null)
    2. {
    3. if ($toLevel < 0 || $toLevel >= $this->transactions) {
    4. return;
    5. }
    6. $this->performRollBack($toLevel);
    7. $this->transactions = $toLevel;
    8. $this->fireConnectionEvent('rollingBack');
    9. }
    10. protected function performRollBack($toLevel)
    11. {
    12. if ($toLevel == 0) {
    13. $this->getPdo()->rollBack();
    14. //跳到某个暂存点
    15. } elseif ($this->queryGrammar->supportsSavepoints()) {
    16. $this->getPdo()->exec(
    17. $this->queryGrammar->compileSavepointRollBack('trans'.($toLevel + 1))
    18. );
    19. }
    20. }

            上面的回滚操作还可以选择回滚到哪个事务,如果不选择,默认向前回滚。上面的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()获取代理对象、直接把目标方法迁移到外部类中,本文对此不过多阐述。

  • 相关阅读:
    远程连接wsl
    AUTOSAR知识点 之 Dem (三):基于ETAS工具ISOLAR-AB配置实现DEM分析
    如何用golang写一个自己的后端框架
    自然语言处理(NLP)中的迁移学习
    RK3566快速上手 | ROC-RK3566-PC开发板快速上手
    Leetcode 位运算
    国际服务贸易期末考试复习资料
    ZYNQ从放弃到入门(十二)- AMP — Zynq 上的非对称多核处理器
    .NET Emit 入门教程:第六部分:IL 指令:1:概要介绍
    GIT中对子仓库的使用方法介绍
  • 原文地址:https://blog.csdn.net/hbnn111/article/details/137271579