【MySQL事务与Spring 事务总结】

从 MySQL 事务到 Spring 声明式事务:一次搞懂 @Transactional 与事务传播机制

以用户注册 + 操作日志为例,理解事务提交、回滚、异常处理和传播行为

为什么需要事务?

从数据库系统的角度看,事务并不是简单地把几条 SQL 包在一起执行,而是一种用于维护数据一致性的核心机制。现实业务中的一次操作,往往并不对应数据库中的一条 SQL,而是由多条读写操作共同完成。例如转账操作至少包含“转出账户扣款”和“转入账户加款”两个步骤;注册业务可能包含“插入用户信息”和“记录操作日志”两个步骤;秒杀系统则可能包含“创建订单”和“扣减库存”两个步骤。事务会把一组操作作为一个整体提交或撤销,使这组操作要么同时成功,要么同时失败。

事务首先解决的是业务操作与数据库操作之间粒度不一致的问题。在业务层看来,“转账”“注册”“下单”都是一个完整的业务动作;但在数据库层面,它们会被拆解成多条 SQL。如果缺少事务,系统就可能停留在某个“中间状态”:比如 A 账户已经扣款,但 B 账户尚未加款;订单已经创建,但库存没有减少。这种状态从 SQL 执行角度看可能是“部分成功”,但从业务语义看却是错误状态。

更学术地说,事务的目标是保证数据库从一个一致状态转换到另一个一致状态。所谓一致状态,并不是指数据库没有任何变化,而是指变化之后仍然满足业务约束、完整性约束和系统不变量。例如,在转账场景中,张三和李四的账户余额可以变化,但两人余额总和不能凭空增加或减少;在秒杀场景中,订单数量和库存变化必须保持逻辑一致。在转账案例中,转账前后余额总数应保持不变,且处理过程中不能受到其他并发转账事件干扰,这正对应事务的 ACID 特性。

事务的第二个价值是处理失败不确定性。在真实系统中,失败是常态:程序可能抛异常,网络可能中断,数据库连接可能断开,服务器可能宕机。如果没有事务,系统开发者就必须手动处理每一步失败后的补偿逻辑,这会让业务代码变得复杂且容易出错。事务通过 COMMIT 和 ROLLBACK 提供了统一的失败处理模型:全部成功则提交,任一步失败则回滚。支持事务的数据库可以简化编程模型,使开发者不必在业务代码中反复处理各种潜在错误和并发问题。

事务的第三个价值是提供并发控制。数据库通常会被多个客户端同时访问,如果多个事务同时读写同一批数据,就可能出现脏读、不可重复读、幻读、更新丢失等并发异常。事务的隔离性并不是为了阻止并发,而是为了在“系统吞吐量”和“数据正确性”之间建立可控的平衡。资料中提到,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据不一致,并且可以通过不同隔离级别在性能和安全之间进行取舍。

最后,事务也是一种重要的工程抽象。它把底层复杂的恢复机制、日志机制、锁机制、MVCC 机制等封装起来,向上层应用暴露一个更简单的语义:这一组操作作为一个整体成功或失败。对于 Spring 来说,事务进一步被封装为声明式编程模型,开发者只需要在业务方法上使用 @Transactional,就可以让 Spring 帮助管理事务的开启、提交和回滚。资料中也说明,Spring 事务分为编程式事务和声明式事务,其中声明式事务通过注解自动开启和提交事务。

事务是数据库系统提供的一种一致性维护机制,它通过原子性、隔离性和持久性等约束,将多个物理数据库操作封装为一个逻辑业务操作,从而保证系统在异常、并发和故障环境下仍能从一个一致状态安全地转移到另一个一致状态。

我们就举一个转账的例子来说明:首先连接本地的MySQL数据库,执行如下SQL语句:

### 请读者将数据库的名字换成自己本地的数据库,或者使用create database if not exists trans_test 来进行创建
use trans_test;

create table bank_account (
         ## id为主键,设置为自增
        id bigint primary key auto_increment, 
		name varchar(255) not null comment '姓名',
		balance decimal(10,2) not null comment '余额'
);

insert into bank_account(name,balance) values ('张三',1000),('李四',1200);

select * from bank_account;

#更新操作
update bank_account set balance = balance - 100 where name = '张三';
update bank_account set balance = balance + 100 where name = '李四';

#注意在实际的开发中不要select数据库表的所有字段,在实际的开发中表会很大,这样写容易带来慢SQL的问题
select * from bank_account;

在执行更新操作之前,表中的数据应该是这样的:

在这里插入图片描述
我们把转账这个操作看成一个业务:如果转账成功,那么应该有如下的情况:

  1. 张三的账户余额减少100,变成900,李四的账户余额增加了100,变成了1300,不能出现张三的账户减少而李四的余额没有增加的情况
  2. 张三和李四在发生转账前后的总额不变,也就是说张三和李四的余额总数为1000 + 1200 = 2200,转账后他们的余额总数为: 900 + 1300 = 2200
  3. 转账后的余额结果应当保存到介质中,以便以后读取
  4. 还有一点需要保证:在转账的过程中张三和李四余额不能因为其他的转账事件收到干扰

以上这四点在事务的整个执⾏过程中必须要得到保证,这也就是事务的 ACID 特性

我们执行更新操作,会得到如下的结果:

在这里插入图片描述
本节总结如下:
在这里插入图片描述

MySQL 事务基础:ACID、提交、回滚、保存点

事务的ACID特性

事务的ACID特性是指:Atomicity(原子性)Consistency(一致性)Isolation(隔离性)Durability(持久性)

  • Atomicity(原子性):⼀个事务中的所有操作,要么全部成功,要么全部失败,不会出现只执⾏了⼀半的情况,如果事务在执⾏过程中发⽣错误,会回滚( Rollback )到事务开始前的状态,就像这个事务从来没有执⾏过⼀样。
  • Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性不会被破坏。这表⽰写⼊的数据必须完全符合所有的预设规则,包括数据的精度、关联性以及关于事务执⾏过程中服务器崩溃后如何恢复。
  • Isolation(隔离性):数据库允许多个并发事务同时对数据进⾏读写和修改,隔离性可以防⽌多个事务并发执⾏时由于交叉执⾏⽽导致数据的不⼀致。事务可以指定不同的隔离级别,以权衡在不同的应⽤场景下数据库性能和安全。
  • Durability(持久性): 事务处理结束后,对数据的修改将永久的写⼊存储介质,即便系统故障也不会丢失。

提交与回滚

接下来我们来看一下如何在MySQL之中使用事务:

  • 要使⽤事务那么数据库就要⽀持事务,在MySQL中⽀持事务的存储引擎是InnoDB,可以通过 show engines; 语句查看:
    在这里插入图片描述
    可以通过以下的语句完成事务的控制:
# 开始一个新的事务
start transaction;
# 或
begin;

#提交当前事务,并对更改持久化保存
commit;

# 回滚当前事务,取消其修改
rollback;

接下来我们通过一个小案例来看一下:

开启一个事务,执行修改后回滚:

C:\Windows\System32>mysql -uroot -p
Enter password: ****
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 12
Server version: 8.0.34 MySQL Community Server - GPL

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> show englines
    -> ;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'englines' at line 1
mysql> show engines;
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| Engine             | Support | Comment                                                        | Transactions | XA   | Savepoints |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| MEMORY             | YES     | Hash based, stored in memory, useful for temporary tables      | NO           | NO   | NO         |
| MRG_MYISAM         | YES     | Collection of identical MyISAM tables                          | NO           | NO   | NO         |
| CSV                | YES     | CSV storage engine                                             | NO           | NO   | NO         |
| FEDERATED          | NO      | Federated MySQL storage engine                                 | NULL         | NULL | NULL       |
| PERFORMANCE_SCHEMA | YES     | Performance Schema                                             | NO           | NO   | NO         |
| MyISAM             | YES     | MyISAM storage engine                                          | NO           | NO   | NO         |
| InnoDB             | DEFAULT | Supports transactions, row-level locking, and foreign keys     | YES          | YES  | YES        |
| ndbinfo            | NO      | MySQL Cluster system information storage engine                | NULL         | NULL | NULL       |
| BLACKHOLE          | YES     | /dev/null storage engine (anything you write to it disappears) | NO           | NO   | NO         |
| ARCHIVE            | YES     | Archive storage engine                                         | NO           | NO   | NO         |
| ndbcluster         | NO      | Clustered, fault-tolerant tables                               | NULL         | NULL | NULL       |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
11 rows in set (0.00 sec)

mysql> start transaction;
Query OK, 0 rows affected (0.01 sec)

mysql> select * from bank_account;
ERROR 1046 (3D000): No database selected
mysql> use java118_homework;
Database changed
mysql> select * from bank_account;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |  900.00 |
|  2 | 李四   | 1300.00 |
+----+--------+---------+
2 rows in set (0.00 sec)

mysql> update bank_account set bank_account.banlance = bank_account.banlance - 100 where name = '张三';
ERROR 1054 (42S22): Unknown column 'bank_account.banlance' in 'field list'
mysql> update bank_account set bank_account.balance = bank_account.balance - 100 where name = '张三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> update bank_account set bank_account.balance = bank_account.balance + 100 where name = '李四';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from bank_account;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |  800.00 |
|  2 | 李四   | 1400.00 |
+----+--------+---------+
2 rows in set (0.00 sec)

mysql> ## 回滚事务
mysql> rollback;
Query OK, 0 rows affected (0.01 sec)

mysql> select * from bank_account;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |  900.00 |
|  2 | 李四   | 1300.00 |
+----+--------+---------+
2 rows in set (0.00 sec)

mysql> ## 可以发现修改没有生效
mysql> quit;
Bye

开启一个事务,执行修改后提交:

C:\Windows\System32>mysql -uroot -p
Enter password: ****
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 13
Server version: 8.0.34 MySQL Community Server - GPL

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| book_test          |
| db05               |
| experiment         |
| information_schema |
| itcast             |
| itheima            |
| java118            |
| java118_homework   |
| jdbcdemo           |
| mybatis_test       |
| mysql              |
| performance_schema |
| smart-parking      |
| ssm64hz2kc9        |
| store              |
| sys                |
| test               |
| test_db            |
| trans_test         |
| winter_project     |
+--------------------+
20 rows in set (0.01 sec)

mysql> use java118_homework
Database changed
mysql> show tables;
+----------------------------+
| Tables_in_java118_homework |
+----------------------------+
| bank_account               |
| canteen                    |
| depart                     |
| dormitory                  |
| employee                   |
| employee_1                 |
| salary                     |
| staff                      |
| student                    |
| student_dormitory          |
| t_test_index               |
| t_test_index1              |
| t_test_index2              |
| t_test_index4              |
| t_test_index5              |
| t_test_index6              |
| t_test_pk                  |
| t_test_pk1                 |
| t_test_pk2                 |
| t_test_uk                  |
| t_test_uk1                 |
| t_test_uk2                 |
| user                       |
| vehicle                    |
| violation_record           |
| window_charge_record       |
| window_stall               |
+----------------------------+
27 rows in set (0.00 sec)

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from bank_account;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |  900.00 |
|  2 | 李四   | 1300.00 |
+----+--------+---------+
2 rows in set (0.00 sec)

mysql> # 张三余额减少100
mysql> UPDATE bank_accountset balance = balance - 100 where name = '张三';
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '= balance - 100 where name = '张三'' at line 1
mysql> UPDATE bank_account set balance = balance - 100 where name = '张三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> # 李四余额增加100
mysql>  UPDATE bank_account set balance = balance + 100 where name = '李四';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> # 在修改之后,提交之前查看表中的数据,余额已经被修改
mysql>  select * from bank_account;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |  800.00 |
|  2 | 李四   | 1400.00 |
+----+--------+---------+
2 rows in set (0.00 sec)

mysql> # 提交事务
mysql> commit;
Query OK, 0 rows affected (0.01 sec)

mysql> # 再查询发现数据已被修改,说明数据已经持久化到磁盘
mysql> select * from bank_account;\
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |  800.00 |
|  2 | 李四   | 1400.00 |
+----+--------+---------+
2 rows in set (0.00 sec)

保存点

在事务执行的过程中设置保存点,回滚时指定保存点可以把数据恢复到保存点的状态。

C:\Windows\System32>mysql -uroot -p
Enter password: ****
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 13
Server version: 8.0.34 MySQL Community Server - GPL

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| book_test          |
| db05               |
| experiment         |
| information_schema |
| itcast             |
| itheima            |
| java118            |
| java118_homework   |
| jdbcdemo           |
| mybatis_test       |
| mysql              |
| performance_schema |
| smart-parking      |
| ssm64hz2kc9        |
| store              |
| sys                |
| test               |
| test_db            |
| trans_test         |
| winter_project     |
+--------------------+
20 rows in set (0.01 sec)

mysql> use java118_homework
Database changed
mysql> show tables;
+----------------------------+
| Tables_in_java118_homework |
+----------------------------+
| bank_account               |
| canteen                    |
| depart                     |
| dormitory                  |
| employee                   |
| employee_1                 |
| salary                     |
| staff                      |
| student                    |
| student_dormitory          |
| t_test_index               |
| t_test_index1              |
| t_test_index2              |
| t_test_index4              |
| t_test_index5              |
| t_test_index6              |
| t_test_pk                  |
| t_test_pk1                 |
| t_test_pk2                 |
| t_test_uk                  |
| t_test_uk1                 |
| t_test_uk2                 |
| user                       |
| vehicle                    |
| violation_record           |
| window_charge_record       |
| window_stall               |
+----------------------------+
27 rows in set (0.00 sec)

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from bank_account;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |  900.00 |
|  2 | 李四   | 1300.00 |
+----+--------+---------+
2 rows in set (0.00 sec)

mysql> # 张三余额减少100
mysql> UPDATE bank_accountset balance = balance - 100 where name = '张三';
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '= balance - 100 where name = '张三'' at line 1
mysql> UPDATE bank_account set balance = balance - 100 where name = '张三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> # 李四余额增加100
mysql>  UPDATE bank_account set balance = balance + 100 where name = '李四';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> # 在修改之后,提交之前查看表中的数据,余额已经被修改
mysql>  select * from bank_account;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |  800.00 |
|  2 | 李四   | 1400.00 |
+----+--------+---------+
2 rows in set (0.00 sec)

mysql> # 提交事务
mysql> commit;
Query OK, 0 rows affected (0.01 sec)

mysql> # 再查询发现数据已被修改,说明数据已经持久化到磁盘
mysql> select * from bank_account;\
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |  800.00 |
|  2 | 李四   | 1400.00 |
+----+--------+---------+
2 rows in set (0.00 sec)

mysql> # 开启事务
mysql>  START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

mysql> # 在修改之前查看表中的数据
mysql>  select * from bank_account;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |  800.00 |
|  2 | 李四   | 1400.00 |
+----+--------+---------+
2 rows in set (0.00 sec)

mysql> # 张三余额增加100
mysql>  UPDATE bank_account set balance = balance + 100 where name = '张三';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> # 李四余额减少100
mysql> UPDATE bank_account set balance = balance - 100 where name = '李四';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> # 余额已经被修改
mysql> select * from bank_account;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |  900.00 |
|  2 | 李四   | 1300.00 |
+----+--------+---------+
2 rows in set (0.00 sec)

mysql> # 设置保存点
mysql> savepoint savepoint1;
Query OK, 0 rows affected (0.01 sec)

mysql> # 再次执⾏,张三余额减少100
mysql>  UPDATE bank_account set balance = balance - 100 where name = '张三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> # 再次执⾏,李四余额增加100
mysql>  UPDATE bank_account set balance = balance + 100 where name = '李四';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> # 余额已经被修改
mysql> select * from bank_account;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |  800.00 |
|  2 | 李四   | 1400.00 |
+----+--------+---------+
2 rows in set (0.00 sec)

mysql> # 设置第二个保存点
mysql> savepoint savepoint2;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into bank_account values (null, '王五', 1000);
Query OK, 1 row affected (0.00 sec)

mysql> select * from bank_account;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |  800.00 |
|  2 | 李四   | 1400.00 |
|  3 | 王五   | 1000.00 |
+----+--------+---------+
3 rows in set (0.00 sec)

mysql> # 回滚到第二个保存点
mysql> rollback to savepoint2;
Query OK, 0 rows affected (0.01 sec)

mysql> select * from bank_account;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |  800.00 |
|  2 | 李四   | 1400.00 |
+----+--------+---------+
2 rows in set (0.00 sec)

mysql> #回滚到第一个保存点
mysql> rollback to savepoint1;
Query OK, 0 rows affected (0.00 sec)

mysql> # 回滚成功
mysql> select * from bank_account;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |  900.00 |
|  2 | 李四   | 1300.00 |
+----+--------+---------+
2 rows in set (0.00 sec)

mysql> # 回滚时不指定保存点,直接回滚到事务开始时的状态,事务关闭
mysql> rollback;
Query OK, 0 rows affected (0.01 sec)

mysql> select * from bank_account;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |  800.00 |
|  2 | 李四   | 1400.00 |
+----+--------+---------+
2 rows in set (0.00 sec)

mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set, 1 warning (0.00 sec)

我们可以通过如下的语句来设置保存点:

#savepoint1为保存点的名字,而savepoint是关键字
savepoint savepoint1

回滚至保存点的SQL语句如下:

#rollback to 是关键字 savepoint1是保存点的名称
rollback to savepoint1

注意:如果回滚不指定保存点的话,将会直接回滚到事务最开始的时候的状态,事务关闭

MySQL事务隔离级别与Spring 事务隔离级别

隔离性:MySQL服务可以同时被多个客⼾端访问,每个客⼾端执⾏的DML语句以事务为基本单位,那么不同的客⼾端在对同⼀张表中的同⼀条数据进⾏修改的时候就可能出现相互影响的情况,为了保证不同的事务之间在执⾏的过程中不受影响,那么事务之间就需要要相互隔离,这种特性就是隔离性。

MySQL事务隔离级别

事务间不同程度的隔离,称为事务的隔离级别;不同的隔离级别在性能和安全方面做了取舍,有的隔离级别注重并发性,有的注重安全性,有的则是并发性和安全性适中;在MySQL的InnoDB引擎中事务的隔离级别有四种,分别是:

  1. READ UNCOMMITTED , 读未提交
  2. READ COMMITED,读已提交
  3. REPEATABLE READ,可重复读(默认)
  4. SERIALIZABLE,串行化
查看和设置隔离级别

可以通过以下的两行命令来查看全局事务隔离级别和会话事务隔离级别:

mysql> SELECT @@GLOBAL.transaction_isolation;
+--------------------------------+
| @@GLOBAL.transaction_isolation |
+--------------------------------+
| REPEATABLE-READ                |
+--------------------------------+
1 row in set (0.01 sec)
5. List item

mysql> SELECT @@SESSION.transaction_isolation;
+---------------------------------+
| @@SESSION.transaction_isolation |
+---------------------------------+
| REPEATABLE-READ                 |
+---------------------------------+
1 row in set (0.00 sec)
不同隔离级别存在的问题
  1. 读未提交与脏读
    脏读出现在事务的 READ UNCOMMITED隔离级别下,由于在读取数据时不做任何限制,所以并发性能很高,但是会出现大量的数据安全的问题,比如在事务A中执行了一条Insert 语句,在没有执行 Commit的情况下,会在事务B中被读取到,此时如果事务A执行回滚操作,那么事务B中读取到事务A写入的数据将没有任何意义,我们把这个现象叫做 脏读
  2. 读已提交与不可重复读
    为了解决脏读的问题,可以把事务的隔离级别设置为READ COMMITED,这时事务只能读到其他事务提交之后的数据,但是会出现不可重复读的问题,比如事务A先对某条数据进行查询,之后事务B对这条数据进行修改,并且提交(COMMIT)事务,事务A再对这条数据进行查询的时候,得到了事务B修改之后的结果,这导致了事务A在同一个事务中以相同的条件查询得到了不同的值,这个现象叫做 不可重复读
  3. 可重复读与幻读
    为了解决不可重复读的问题,可以把事务的隔离级别设置为REPEATBLE READ,这时同⼀个事务中读取的数据在任何时候都是相同的结果,但还会出现⼀个问题,事务A查询了⼀个区间的记录得到结果集A,事务B向这个区间的间隙中写⼊了⼀条记录并提交,事务A再查询这个区间的结果集时会查到事务B新写⼊的记录得到结果集B,两次查询的结果集不⼀致,这个现象就是 幻读

Mysql的InnoDB存储引擎使用了Next-Key 锁解决了大部分的幻读问题
5. 串行化
进⼀步提升事务的隔离级别到 SERIALIZABLE ,此时所有事务串⾏执⾏,可以解决所有并发中的安全问题。

不同的隔离级别与安全性的问题
隔离级别脏读幻读不可重复读
READ UNCOMMITED存在存在存在
READ COMMITED解决存在存在
REPEATABLE READ解决解决存在
SERIALIABLE解决解决解决

随着事务隔离级别的提升,隔离力度也就是安全性是逐渐提升的,但是并发性能是逐渐下降的。在实际开发的时候我们要对安全性和性能做出权衡,然后在不完美的解决方案中选择一个对当前场景最有利的。

补充:InnoDB存储引擎事务隔离性以及相关的隔离级别是由锁和MVCC机制配合实现的。

Spring 事务隔离级别

Spring的事务隔离级别有5种:
1. Isolation.DEFAULT : 以连接的数据库的事务隔离级别为主
2. Isolation.READ_UNCOMMITTED : 读未提交, 对应SQL标准中 READ–UNCOMMITTED
3. Isolation.READ_COMMITTED : 读已提交,对应SQL标准中 READ COMMITTED
4. Isolation.REPEATABLE_READ : 可重复读, 对应SQL标准中 REPEATABLE READ
5. Isolation.SERIALIZABLE : 串⾏化, 对应SQL标准中 SERIALIZABLE

我们来看一下Isolation.java的源码:

/*
 * Copyright 2002-present the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.transaction.annotation;

import org.springframework.transaction.TransactionDefinition;

/**
 * Enumeration that represents transaction isolation levels for use with the
 * {@link Transactional @Transactional} annotation, corresponding to the
 * {@link TransactionDefinition} interface.
 *
 * @author Colin Sampaleanu
 * @author Juergen Hoeller
 * @since 1.2
 */
public enum Isolation {

	 // 使用数据库默认隔离级别
	DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),

   // 读未提交:可能出现脏读、不可重复读、幻读
	READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),

   // 读已提交:可以避免脏读
	READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),

   // 可重复读:可以避免脏读和不可重复读,MySQL InnoDB 默认级别
	REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),

  // 串行化:隔离性最高,但并发性能最低
	SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);

  // 保存当前枚举值对应的 Spring 事务隔离级别常量
	private final int value;

  // 枚举构造方法,为每个枚举对象绑定一个整数常量
	Isolation(int value) {
		this.value = value;
	}

  // 返回底层事务隔离级别常量
	public int value() {
		return this.value;
	}

}

Isolation 枚举类是 Spring 对事务隔离级别的抽象封装。它本身并不实现隔离机制,而是将 @Transactional 注解中配置的隔离级别映射为 TransactionDefinition 中的整数常量。Spring 在开启事务时,会根据这些常量配置底层 JDBC 连接的事务隔离级别,最终由 MySQL InnoDB 等数据库引擎负责实现具体的并发控制语义。Spring 负责声明和传递隔离级别,数据库负责真正实现隔离级别。

本节总结如下:
在这里插入图片描述

Spring 为什么还要封装事务?

Spring 封装事务的核心原因是:数据库只提供“事务能力”,但应用程序需要的是“业务级事务管理能力”。

MySQL/InnoDB 可以做到 START TRANSACTION、COMMIT、ROLLBACK,也可以通过 ACID 保证数据一致性;你的资料里也提到,事务的本质是数据库对 ACID 模型的实现,是为应用层服务的。 但在 Java Web 项目里,一个业务操作往往分散在多个 Service、多个 Mapper、多个方法调用之间。如果每次都让开发者手动开启事务、提交事务、回滚事务,代码会非常繁琐,而且容易出错。

所以,Spring 封装事务不是为了替代 MySQL 事务,而是为了在应用层帮我们解决这几个问题。

1.把数据库业务升级为业务方法级事务

数据库事务的原始形式是这样的:

START TRANSACTION;

INSERT INTO user_info(...);
INSERT INTO log_info(...);

COMMIT;

但是在 Java 项目中,我们真正关心的不是这几条 SQL,而是一个完整业务方法:

public void register(String name, String password) {
    userInfoMapper.insertUser(name, password);
    logInfoMapper.insertLog(name, "用户注册");
}

在Service层,我们想要表达的事情是:用户注册这个业务,要么用户信息和日志都成功,要么都失败。

Spring 封装事务之后,我们只需要这样写:

@Transactional
public void register(String name, String password) {
    userInfoMapper.insertUser(name, password);
    logInfoMapper.insertLog(name, "用户注册");
}

这样,事务边界就从数据库层的SQL语句,提升到了Java业务层面的方法
这也是Spring 事务最重要的价值之一:

让事务围绕方法展开,而不是围绕单条SQL展开。

2.把重复的事务控制代码从业务代码种剥离出去

如果没有Spring的事务封装,编程式的事务大概是这样的:

TransactionStatus status = transactionManager.getTransaction(transactionDefinition);

try {
    userInfoMapper.insertUser(name, password);
    logInfoMapper.insertLog(name, "用户注册");

    transactionManager.commit(status);
} catch (Exception e) {
    transactionManager.rollback(status);
    throw e;
}

这段代码可以工作,但是问题很明显:

业务代码:插入用户、插入日志
事务代码:开启事务、提交事务、回滚事务

两类逻辑混在一起,业务代码会变得很不干净。

并且Spring 手动操作同样需要开启事务,提交事务和回滚事务,并且要使用DataSourceTransactionManager和TransactionDefinition等对象。这些代码如果每一个业务方法都写一遍,就会非常的重复。、

Spring 的声明式事务就是为了解决这个问题:

@Transactional
public void register(String name, String password) {
    userInfoMapper.insertUser(name, password);
    logInfoMapper.insertLog(name, "用户注册");
}

事务控制逻辑被Spring 接管,业务方法种只保留业务逻辑。

这体现的是一个经典的软件工程思想:

事务管理是横切关注点,应该从核心业务逻辑中解耦出去。

  1. Spring 统一了不同事务资源的管理方式
    MySQL 有 MySQL 的事务,Oracle 有 Oracle 的事务,JDBC 有 JDBC 的事务,JPA/Hibernate 也有自己的事务管理方式。

    如果应用代码直接依赖某一种具体技术,那么以后切换持久层框架时,事务代码也要跟着大改。

Spring 通过统一的事务抽象屏蔽了这些差异:

PlatformTransactionManagerDataSourceTransactionManager      管理 JDBC / MyBatis 事务
JpaTransactionManager             管理 JPA 事务
HibernateTransactionManager       管理 Hibernate 事务

所以应用层不用关心底层到底是MyBatis、JPA还是JDBC,只需要写:

@Transactional

这就是Spring 事务封装的第二层意义:

Spring 提供了一套统一的事务抽象,让业务代码不直接依赖底层数据库事务 API。

  1. Spring 提供了事务传播机制
    这是数据库原生事务本身不直接解决的问题。比如有两个业务方法:
@Transactional
public void methodA() {
    methodB();
}

@Transactional
public void methodB() {
}

问题来了:

methodA 已经有事务了,
methodB 被调用时,是加入 methodA 的事务?
还是自己开启一个新事务?
还是不使用事务?

这就涉及Spring的事务传播机制。
例如:

@Transactional(propagation = Propagation.REQUIRED)

表示:
当前有事务就加入当前事务,没有事务就新建事务。

而:

@Transactional(propagation = Propagation.REQUIRES_NEW)

表示:不管外面有没有事务,都会开启一个新的事务,外层事务会被挂起。

事务传播机制解决的是:多个事务方法存在调用关系的时候,事务如何在这些方法之间传播的问题。

这就是Spring 相比数据库原生事务更加适合业务开发的地方:

数据库解决的是“一组SQL怎么提交或者回滚”;
Spring 事务进一步解决的是“多个业务方法互相调用时,事务边界应该如何组织”。

  1. Spring 可以统一配置事务属性
    @Transactional 不只是一个“开启事务”的注解,它还可以声明很多事务属性:
@Transactional(
   isolation = Isolation.REPEATABLE_READ,
   propagation = Propagation.REQUIRED,
   rollbackFor = Exception.class,
   timeout = 3,
   readOnly = false
)

这些属性分别控制:

isolation      隔离级别
propagation    传播行为
rollbackFor    遇到哪些异常回滚
timeout        事务超时时间
readOnly       是否只读事务

所以Spring 的封装并不是简单地替程序猿写了commit和rollback,而是把事务变成了一种可以声明、配置和统一管理的业务规则。

  1. Spring 事务的本质链路
    可以这样理解Spring事务的执行流程:
@Transactional
        ↓
Spring AOP 生成代理对象
        ↓
TransactionInterceptor 拦截方法调用
        ↓
TransactionManager 开启事务
        ↓
执行业务方法
        ↓
如果正常结束:提交事务
如果抛出异常:判断是否需要回滚
        ↓
底层通过 JDBC Connection 操作数据库事务
        ↓
最终由 MySQL/InnoDB 执行真正的事务控制

一句话总结:

Spring 负责事务的声明、边界、传播、异常回滚规则和资源管理;MySQL/InnoDB 负责真正执行事务的 ACID 语义。

本节小结如下:
在这里插入图片描述

Spring 编程式事务实验

Spring 手动操作事务和MySQL事务操作类似,有三个重要的步骤:

  • 开启事务
  • 提交事务
  • 回滚事务

SpringBoot内置了两个对象:
1.DataSourceTransactionManager 事务管理器。用来获取事务(开启事务),提交或者回滚事务
2.TransactionDefinition是事务的属性,在获取事务的时候需要将
TransactionDefinition 传递进去从而获得一个事务 TransactionStatus

我们依旧是根据代码案例来进行学习:

package com.djm.controller;

import com.djm.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/user")
@RestController
public class UserController {
    @Autowired
    private UserService userService;

    // JDBC 事务管理器
    @Autowired
    private DataSourceTransactionManager dataSourceTransactionManager; // 定义事务属性

    @Autowired
    private TransactionDefinition transactionDefinition;

//    @Transactional
    @RequestMapping("/registry")
    public String registry(String name,String password){
        // 开启事务
        TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);

        //用户注册
        userService.registyUser(name,password);

        //提交事务
        dataSourceTransactionManager.commit(transactionStatus);

        //回滚事务
        //dataSourceTransactionManager.rollback(transactionStatus);

        return "注册成功";

    }

}

事务提交是以下这一行的关键代码:

//提交事务
 dataSourceTransactionManager.commit(transactionStatus);

访问如下的url:

http://127.0.0.1:8080/user/registry?name=djm&password=djm

测试结果如下:
在这里插入图片描述
后端的日志如下:
在这里插入图片描述
注意这一行日志:

Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7ed1497d]

Transaction committing说明事务正常提交,整个业务逻辑生效。

我们通过navicat连接数据库,查看trans_test数据库种对应的user-info表:
在这里插入图片描述
可以发现注册的用户信息已经可以成功的插入到数据库对应的表中

接下来我们来看回滚操作:
只需要对代码做一点小小的微调即可

package com.djm.controller;

import com.djm.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/user")
@RestController
public class UserController {
   @Autowired
   private UserService userService;

   // JDBC 事务管理器
   @Autowired
   private DataSourceTransactionManager dataSourceTransactionManager; // 定义事务属性

   @Autowired
   private TransactionDefinition transactionDefinition;

//    @Transactional
   @RequestMapping("/registry")
   public String registry(String name,String password){
       // 开启事务
       TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);

       //用户注册
       userService.registyUser(name,password);

       //提交事务
     //  dataSourceTransactionManager.commit(transactionStatus);

       //回滚事务
      dataSourceTransactionManager.rollback(transactionStatus);

       return "注册成功";

   }

}

执行回滚操作的核心代码为:

        dataSourceTransactionManager.rollback(transactionStatus);

访问如下的URL:

http://127.0.0.1:8080/user/registry?name=tl&password=tl

测试结果如下:
在这里插入图片描述
可以看到后端返回的HTTP状态码是200,接下来我们来看一下后端的日志
在这里插入图片描述
我们可以看到以下关键的三行:

Releasing transactional SqlSession 
Transaction synchronization deregistering SqlSession 
Transaction synchronization closing SqlSession 

和之前提交事务的日志相比,这段日志种没有出现committing,只有:

Transaction synchronization deregistering SqlSession [...]
Transaction synchronization closing SqlSession [...]

这说明SqlSession被释放了、注销、关闭了,但是没有走正常的提交流程。原因也很简单,我们主动标记了回滚。这个业务的操作是不会落到数据库中的。

通过上面的实验可以看到,Spring 编程式事务已经能够完成事务的开启、提交与回滚。开发者可以借助 DataSourceTransactionManager 获取事务状态,并在业务代码执行成功后手动 commit,在出现异常时手动 rollback。这种方式的优点是事务控制过程非常直观,能够清楚地看到 Spring 对数据库事务的管理流程;但它的问题也同样明显:事务控制代码和业务逻辑代码被写在了一起,使得 Service 方法中充斥着大量与业务无关的模板代码,例如获取事务、提交事务、捕获异常、回滚事务等。当业务方法越来越多、调用链越来越复杂时,这种写法不仅会降低代码可读性,也容易造成事务边界不清晰、异常处理不统一等问题。事务管理本质上属于一种典型的“横切关注点”,它不应该侵入每一个具体的业务方法。因此,Spring 在编程式事务的基础上进一步提供了声明式事务机制。开发者只需要在需要事务控制的方法或类上添加 @Transactional 注解,Spring 就可以基于 AOP 代理在方法执行前自动开启事务,在方法正常结束后提交事务,在方法抛出异常时根据回滚规则自动回滚事务。由此,事务控制逻辑从业务代码中被解耦出来,业务方法只需要关注自身的业务语义,而事务的边界、传播行为、隔离级别和回滚规则则统一交给 Spring 事务框架管理。

Spring 声明式事务 @Transactional 实验

遵循以下两个步骤可以实现Spring 声明式的事务:
1.添加依赖:

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
        </dependency>

2.使用@Transactional注解
在需要事务的方法上添加@Transactional 注解就可以实现了,无需手动开启事务和提交事务,进入方法时自动开启事务,如果中途发生了没有处理的异常会自动回滚事务,下面直接来看代码实现:

package com.djm.controller;

import com.djm.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;

@RestController
@RequestMapping("/trans")
@Slf4j
public class TransController {
    @Autowired
    private UserService userService;
    /**
     * 事务提交
     */
    @Transactional
    @RequestMapping("/registry")
    public String registry(String name,String password) {
        //用户注册
        userService.registyUser(name,password);

        log.info("用户数据插入成功");

        return "注册成功";
    }
}

运行程序发现数据插入成功

接下来我们修改程序让程序发生异常:

package com.djm.controller;

import com.djm.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;

@RestController
@RequestMapping("/trans")
@Slf4j
public class TransController {
    @Autowired
    private UserService userService;
    /**
     * 事务提交
     */
    @Transactional
    @RequestMapping("/registry")
    public String registry(String name,String password) {
        //用户注册
        userService.registyUser(name,password);

        log.info("用户数据插入成功");

        //强制程序抛出异常

//        修改程序,对异常进行捕获,
        try {

            int a = 10 / 0;

        } catch (Exception e) {
           //可以手动回滚事务
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

        }

        return "注册成功";
    }
}

注: 真实的开发中一般会在业务逻辑层当中(也就是service中)来控制事务,因为在业务逻辑层中国,一个业务功能可能包含多个数据访问操作,在业务罗哦基层来控制事务,我们就可以将多个数据访问操作控制在一个事务的范围之内,而这个案例中代码在Controller中书写,只是为了方便学习。

访问URL:

http://127.0.0.1:8080/trans/registry?name=888&password=999

测试结果如下:

在这里插入图片描述
可以看到后端正常的额返回了200这个HTTP状态码,并且返回了注册成功。

后端的日志如下:

Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2fa04164]
JDBC Connection [HikariProxyConnection@1450615794 wrapping com.mysql.cj.jdbc.ConnectionImpl@5e9d7f96] will be managed by Spring
==>  Preparing: insert into user_info(`user_name`,`password`)values(?,?)
==> Parameters: 888(String), 999(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2fa04164]
2026-07-08T14:39:35.598+08:00  INFO 35548 --- [spring-trans-demo] [nio-8080-exec-1] com.djm.controller.TransController       : 用户数据插入成功
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2fa04164]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2fa04164]

可以看到事务并没有提交,而数据库中也没有对应的数据。事务回滚了。

@Transactional 作⽤

@Transactional 可以⽤来修饰⽅法或类:

  • 修饰⽅法时: 只有修饰public ⽅法时才⽣效(修饰其他⽅法时不会报错, 也不⽣效)[推荐]
  • 修饰类时: 对 @Transactional 修饰的类中所有的 public ⽅法都⽣效.

⽅法/类被 @Transactional 注解修饰时, 在⽬标⽅法执⾏开始之前, 会⾃动开启事务, ⽅法执⾏结束之后, ⾃动提交事务。如果在⽅法执⾏过程中, 出现异常, 且异常未被捕获, 就进⾏事务回滚操作。如果异常被程序捕获, ⽅法就被认为是成功执⾏, 依然会提交事务

@Transactional 详解

接下来看一下@Transactional 注解的使用细节,主要学习@Transactional注解中的三个属性:

  1. rollbackFor: 异常回滚属性. 指定能够触发事务回滚的异常类型. 可以指定多个异常类型
  2. Isolation: 事务的隔离级别. 默认值为 Isolation.DEFAULT
  3. propagation: 事务的传播机制. 默认值为 Propagation.REQUIRED

rollbackFor

@Transactional 默认只在遇到运⾏时异常和Error时才会回滚, ⾮运⾏时异常不回滚. 即Exception的⼦类中, 除了RuntimeException及其⼦类。

如果我们需要所有的异常都回滚,需要配置rollbackFor属性,通过这个属性指定出现何种异常类型时事务进行回滚.

   /**
     * 方法有异常,事务回滚
     */
    @Transactional(rollbackFor =  Exception.class)
    @RequestMapping("/r2")
    public String r2(String name, String password) throws IOException {
        // 用户注册
        Integer result = userService.registyUser(name,password);
        System.out.println("用户注册成功, 影响行数:"+ result);

        if (true) {
            throw new IOException() ;
        }

        return "注册成功";
    }

重启服务,访问如下的URL

http://127.0.0.1:8080/trans/r2?name=zhixuan%20guo&password=zhixuan%20guo

测试结果如下:
在这里插入图片描述
后端日志如下:

Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4368e15c]
JDBC Connection [HikariProxyConnection@1655200760 wrapping com.mysql.cj.jdbc.ConnectionImpl@73cadf73] will be managed by Spring
==>  Preparing: insert into user_info(`user_name`,`password`)values(?,?)
==> Parameters: zhixuan guo(String), zhixuan guo(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4368e15c]
用户注册成功, 影响行数:1
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4368e15c]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4368e15c]
2026-07-08T15:30:43.717+08:00 ERROR 35548 --- [spring-trans-demo] [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception

java.io.IOException
	at com.djm.controller.TransController.r2(TransController.java:58) ~[classes/:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:569) ~[na:na]
	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359) ~[spring-aop-7.0.8.jar:7.0.8]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190) ~[spring-aop-7.0.8.jar:7.0.8]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:158) ~[spring-aop-7.0.8.jar:7.0.8]
	at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:133) ~[spring-tx-7.0.8.jar:7.0.8]
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:371) ~[spring-tx-7.0.8.jar:7.0.8]
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:130) ~[spring-tx-7.0.8.jar:7.0.8]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-7.0.8.jar:7.0.8]
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:719) ~[spring-aop-7.0.8.jar:7.0.8]
	at com.djm.controller.TransController$$SpringCGLIB$$0.r2(<generated>) ~[classes/:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:569) ~[na:na]
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:252) ~[spring-web-7.0.8.jar:7.0.8]
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:184) ~[spring-web-7.0.8.jar:7.0.8]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117) ~[spring-webmvc-7.0.8.jar:7.0.8]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:934) ~[spring-webmvc-7.0.8.jar:7.0.8]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:853) ~[spring-webmvc-7.0.8.jar:7.0.8]
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:86) ~[spring-webmvc-7.0.8.jar:7.0.8]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:963) ~[spring-webmvc-7.0.8.jar:7.0.8]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:866) ~[spring-webmvc-7.0.8.jar:7.0.8]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1000) ~[spring-webmvc-7.0.8.jar:7.0.8]
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:892) ~[spring-webmvc-7.0.8.jar:7.0.8]
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:622) ~[tomcat-embed-core-11.0.22.jar:6.1]
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:874) ~[spring-webmvc-7.0.8.jar:7.0.8]
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:710) ~[tomcat-embed-core-11.0.22.jar:6.1]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:128) ~[tomcat-embed-core-11.0.22.jar:11.0.22]
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-11.0.22.jar:11.0.22]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:107) ~[tomcat-embed-core-11.0.22.jar:11.0.22]
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-7.0.8.jar:7.0.8]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-7.0.8.jar:7.0.8]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:107) ~[tomcat-embed-core-11.0.22.jar:11.0.22]
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-7.0.8.jar:7.0.8]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-7.0.8.jar:7.0.8]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:107) ~[tomcat-embed-core-11.0.22.jar:11.0.22]
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:199) ~[spring-web-7.0.8.jar:7.0.8]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-7.0.8.jar:7.0.8]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:107) ~[tomcat-embed-core-11.0.22.jar:11.0.22]
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:165) ~[tomcat-embed-core-11.0.22.jar:11.0.22]
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:77) ~[tomcat-embed-core-11.0.22.jar:11.0.22]
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:492) ~[tomcat-embed-core-11.0.22.jar:11.0.22]
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:113) ~[tomcat-embed-core-11.0.22.jar:11.0.22]
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:83) ~[tomcat-embed-core-11.0.22.jar:11.0.22]
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:72) ~[tomcat-embed-core-11.0.22.jar:11.0.22]
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:341) ~[tomcat-embed-core-11.0.22.jar:11.0.22]
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397) ~[tomcat-embed-core-11.0.22.jar:11.0.22]
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-11.0.22.jar:11.0.22]
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:1272) ~[tomcat-embed-core-11.0.22.jar:11.0.22]
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1801) ~[tomcat-embed-core-11.0.22.jar:11.0.22]
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-11.0.22.jar:11.0.22]
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:946) ~[tomcat-embed-core-11.0.22.jar:11.0.22]
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:480) ~[tomcat-embed-core-11.0.22.jar:11.0.22]
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:57) ~[tomcat-embed-core-11.0.22.jar:11.0.22]
	at java.base/java.lang.Thread.run(Thread.java:840) ~[na:na]

可以看到事务并没有提交,而是进行了回滚。原因在于下面这一行的代码:

@Transactional(rollbackFor =  Exception.class)

这一行代码设置了只要发生了异常事务就直接回滚,而不只是运行时的异常。

结论:

  • 在Spring 的事务管理中,默认只在遇到运行时异常RuntimeException 和 Error时才进行回滚
  • 如果需要回滚指定类型的异常,可以通过rollbackFor属性来指定

事务传播机制实验:REQUIRED、REQUIRES_NEW、NESTED

事务传播机制就是:多个事务方法存在调用关系的时候,事务是如何在这些方法间进行传播的。事务传播机制解决的是一个事务在多个节点(方法)中传递的问题

@Transactional 注解⽀持事务传播机制的设置, 通过 propagation 属性来指定传播⾏为.
Spring 事务传播机制有以下 7 种:

  1. Propagation.REQUIRED : 默认的事务传播级别. 如果当前存在事务, 则加⼊该事务. 如果当前没有事务, 则创建⼀个新的事务.
  2. Propagation.SUPPORTS : 如果当前存在事务, 则加⼊该事务. 如果当前没有事务, 则以⾮事务的⽅式继续运⾏
  3. Propagation.MANDATORY :强制性. 如果当前存在事务, 则加⼊该事务. 如果当前没有事务, 则抛出异常.
  4. Propagation.NOT_SUPPORTED:以非事务的方式运行,如果当前存在事务,则把当前的事务挂起
  5. Propagation.REQUIRES_NEW:创建一个新的事务,如果存在当前事务,则把当前事务挂起,也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW修饰的内部方法都会新开启自己的事务,而开启的事务相互独立,互不干扰。
  6. Propagation.NEVER : 以⾮事务⽅式运⾏, 如果当前存在事务, 则抛出异常。
  7. Propagation.NESTED : 如果当前存在事务, 则创建⼀个事务作为当前事务的嵌套事务来运⾏如果当前没有事务, 则该取值等价PROPAGATION_REQUIRED。

我们首先来测试progation = Propagation.REQUIRED
代码如下:
TransController:

package com.djm.controller;

import com.djm.service.LogService;
import com.djm.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;

@RestController
@RequestMapping("/trans")
@Slf4j
public class TransController {
    @Autowired
    private UserService userService;

    @Autowired
    private LogService logService;

    /**
     * 事务的传播机制
     */
    @Transactional
    @RequestMapping("/r10")
    public String r10(String name, String password) throws IOException {
        //用户注册
        userService.registyUser(name,password);
        logService.insertLog(name, "用户注册");
        return "注册成功";
    }
}

UserService:

package com.djm.service;

import com.djm.mapper.UserInfoMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
public class UserService {
    @Autowired
    private UserInfoMapper userInfoMapper;

    @Transactional(propagation = Propagation.REQUIRED)
    public Integer registyUser(String name,String password) {
        //插入用户信息
      return  userInfoMapper.insert(name,password);
    }
}

LogService:

package com.djm.service;

import com.djm.mapper.LogInfoMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
public class LogService {
    @Autowired
    private LogInfoMapper logInfoMapper;

    @Transactional(propagation =  Propagation.REQUIRED)
    public void insertLog(String name,String op) {
        logInfoMapper.insertLog(name, op);

        // 强行发生异常
        int a = 10/0;
    }
}

访问URL:

http://127.0.0.1:8080/trans/r10?name=zhixuan%20guo&password=zhixuan%20guo

测试结果如下:

在这里插入图片描述
核心代码:

int a = 10 / 0;

引发算数异常,下面来看后端的日志

Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@684bcdf7]
JDBC Connection [HikariProxyConnection@695650252 wrapping com.mysql.cj.jdbc.ConnectionImpl@2894a2d1] will be managed by Spring
==>  Preparing: insert into user_info(`user_name`,`password`)values(?,?)
==> Parameters: zhixuan guo(String), zhixuan guo(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@684bcdf7]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@684bcdf7] from current transaction
==>  Preparing: insert into log_info(`user_name`,`op`)values(?,?)
==> Parameters: zhixuan guo(String), 用户注册(String)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@684bcdf7]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@684bcdf7]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@684bcdf7]
2026-07-08T19:15:36.607+08:00 ERROR 26856 --- [spring-trans-demo] [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.ArithmeticException: / by zero] with root cause

java.lang.ArithmeticException: / by zero
	at com.djm.service.LogService.insertLog(LogService.java:21) ~[classes/:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:569) ~[na:na]
	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359) ~[spring-aop-7.0.8.jar:7.0.8]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190) ~[spring-aop-7.0.8.jar:7.0.8]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:158) ~[spring-aop-7.0.8.jar:7.0.8]

注意这几行的日志:

Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@684bcdf7]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@684bcdf7] from current transaction

从日志可以看到,user_info 和 log_info 两次插入操作使用的是同一个事务型 SqlSession。当执行日志插入时,MyBatis 输出了 Fetched SqlSession from current transaction,说明当前已经存在 Spring 事务,LogService 并没有重新开启一个独立事务,而是加入了当前事务。这正是 Propagation.REQUIRED 的语义:如果当前存在事务,就加入当前事务;如果当前不存在事务,才创建新事务。因此,虽然用户表和日志表的插入语句都执行成功,并且都返回了 Updates: 1,但由于 LogService.insertLog() 后续抛出了 ArithmeticException,该异常属于运行时异常,会触发 Spring 默认事务回滚规则,最终整个事务被回滚,两个表中的插入结果都会被撤销。

核心结论:
Propagation.REQUIRED 下,多个事务方法会共用同一个事务。只要其中一个方法抛出未处理的运行时异常,整个事务都会回滚。

再来测试一下 propagation = Propagation.REQUIRES_NEW
将UserService 和LogService中的@Transactional 注解中的属性更改为

propagation = Propagation.REQUIRES_NEW

UserService:

package com.djm.service;

import com.djm.mapper.UserInfoMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
public class UserService {
    @Autowired
    private UserInfoMapper userInfoMapper;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Integer registyUser(String name,String password) {
        //插入用户信息
      return  userInfoMapper.insert(name,password);
    }
}

LogService:

package com.djm.service;

import com.djm.mapper.LogInfoMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
public class LogService {
    @Autowired
    private LogInfoMapper logInfoMapper;

    @Transactional(propagation =  Propagation.REQUIRES_NEW)
    public void insertLog(String name,String op) {
        logInfoMapper.insertLog(name, op);

        // 强行发生异常
        int a = 10/0;
    }
}

运行程序,发现用户数据插入成功了,日志表数据插入失败.
LogService方法中的事务不影响UserService中的事务
当我们不希望事务之间相互影响的时候,可以使用该传播行为。

propagation = Propagation.NESTED

将上述UserService 和LogService 中相关⽅法事务传播机制改为 Propagation.NESTED

运⾏程序, 发现没有任何数据插⼊.

流程描述:

  1. Controller 中p1 ⽅法开始事务
  2. UserService ⽤⼾注册, 插⼊⼀条数据 (嵌套p1事务)
  3. LogService 记录操作⽇志, 插⼊⼀条数据(出现异常, 执⾏失败) (嵌套p1事务, 回滚当前事务, 数据添加失败)
  4. 由于是嵌套事务, LogService 出现异常之后, 往上找调⽤它的⽅法和事务, 所以⽤⼾注册也失败了
  5. 最终结果是两个数据都没有添加

p1事务可以认为是⽗事务, 嵌套事务是⼦事务. ⽗事务出现异常, ⼦事务也会回滚, ⼦事务出现异常, 如果不进⾏处理, 也会导致⽗事务回滚.

NESTED和REQUIRED区别

  • 整个事务如果全部执⾏成功, ⼆者的结果是⼀样的.
  • 如果事务⼀部分执⾏成功, REQUIRED加⼊事务会导致整个事务全部回滚. NESTED嵌套事务可以实现局部回滚, 不会影响上⼀个⽅法中执⾏的结果.

小结如下:

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值