数据库-事务
一、什么是事务?
在执行SQL语句的时候,某些业务要求,一系列操作必须全部执行,而不能仅执行一部分。例如,一个转账操作:
-- 从id=1的账户给id=2的账户转账100元
-- 第一步:将id=1的A账户余额减去100
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 第二步:将id=2的B账户余额加上100
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
这两条SQL语句必须全部执行,或者,由于某些原因,如果第一条语句成功,第二条语句失败,就必须全部撤销。
这种把多条语句作为一个整体进行操作的功能,被称为数据库事务。
事务(Transaction)是数据库中的一个操作单元,它是一组要么全部成功、要么全部失败的数据库操作。
如果事务失败,那么效果就和没有执行这些SQL一样,不会对数据库数据有任何改动。
二、事务的四大特性(ACID)
特性 | 名称 | 解释 |
---|---|---|
A | Atomicity(原子性) | 一组操作不可分割,要么都做,要么都不做 |
C | Consistency(一致性) | 事务前后数据必须满足数据库约束规则 |
I | Isolation(隔离性) | 多个事务互不干扰,彼此独立运行 |
D | Durability(持久性) | 一旦事务提交,结果永久保存,即使宕机也不会丢失 |
三、隐形事务 VS 显性事务
对于单条SQL语句,数据库系统自动将其作为一个事务执行,这种事务被称为隐式事务。
要手动把多条SQL语句作为一个事务执行,使用BEGIN
开启一个事务,使用COMMIT
提交一个事务,这种事务被称为显式事务,例如,把上述的转账操作作为一个显式事务:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
很显然多条SQL语句要想作为一个事务执行,就必须使用显式事务。
四、事务执行流程示意图
BEGIN├── 执行 SQL1├── 执行 SQL2├── 执行 SQL3└── COMMIT / ROLLBACK
COMMIT
是指提交事务,即试图把事务内的所有SQL所做的修改永久保存。如果COMMIT
语句执行失败了,整个事务也会失败。
有些时候,我们希望主动让事务失败,这时,可以用ROLLBACK
回滚事务,整个事务会失败:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
ROLLBACK;
数据库事务是由数据库系统保证的,我们只需要根据业务逻辑使用它就可以。
五、示例:银行转账
假设数据库有两张表:
-- A账户:余额1000
-- B账户:余额500
5-1、Java + SQL
转账 Java 伪代码:
public void transfer() {try {// 开启事务conn.setAutoCommit(false);// 扣钱jdbcTemplate.update("UPDATE account SET money = money - 100 WHERE name = 'A'");// 模拟异常int i = 1 / 0;// 加钱jdbcTemplate.update("UPDATE account SET money = money + 100 WHERE name = 'B'");// 提交事务conn.commit();} catch (Exception e) {// 回滚事务conn.rollback();}
}
如果中间出错(如除零异常),那么整个事务会回滚,不会出现“扣了钱但没加钱”的错误。
5-2、Spring 中的事务处理方式
Spring 的事务管理有两种主流方式:
类型 | 编程式事务 | 声明式事务 |
---|---|---|
控制粒度 | 手动编码控制 | 注解/XML 配置即可 |
使用复杂度 | 复杂 | 简单 |
灵活性 | 高,可自定义细节 | 足够应付 90% 场景 |
推荐程度 | 一般用于特殊情况 | ✅主流方式(推荐) |
1、编程式事务:手动控制什么时候开始事务、提交、回滚
2、声明式事务(通过注解或 XML ):你 只负责业务逻辑,事务由 Spring 框架接管控制(推荐!)
1、注解+XML配置实现事务
xml配置:
1、创建事务管理器
2、添加事务命名空间 tx
和相关 schema 定义
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:tx="http://www.springframework.org/schema/tx"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
3、启用事务注解支持
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"><!-- 基本连接信息 --><property name="driverClassName" value="${db.driverClassName}" /><property name="url" value="${db.url}" /><property name="username" value="${db.username}" /><property name="password" value="${db.password}" /></bean><!-- 配置 JdbcTemplate --><bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"><property name="dataSource" ref="dataSource"/></bean><!-- 创建事务管理器 --><bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource" ref="dataSource"/></bean><!-- 启用事务注解支持 --><tx:annotation-driven transaction-manager="txManager"/>
service类上加上@@Transactional
注解
@Service(value = "userServiceTrans")
@Transactional
public class UserService {@Autowired@Qualifier(value = "userDaoTrans")private UserDao userDao;public void bankMoney(){userDao.addUserAccount();int a = 10/0;userDao.minusUserAccount();System.out.println("执行完成");}}
@
@Transactional
注解,可以加在类上面,也可以加在方法上面!把注解添加到类上面,表示:这个类中的所有方法,都添加了事务!
小贴士:
命名空间 | 用途 | 必须? |
---|---|---|
xmlns:tx | 使用事务相关配置 | 必须 |
xmlns:aop | 使用 AOP 标签(如 <aop:aspect> ) | 如果用了 AOP,就必须 |
xmlns:context | 使用 <context:component-scan> 等 | 如果用了注解扫描,就必须 |
Spring 用 @Transactional
注解帮你处理事务:
@Transactional
public void transfer() {userDao.debit("A", 100);userDao.credit("B", 100);
}
-
Spring 会自动开启事务
-
如果中间抛出异常,会自动回滚
-
如果方法正常执行完,会自动提交
六、隔离级别
对于两个并发执行的事务,如果涉及到操作同一条记录的时候,可能会发生问题。
因为并发操作会带来数据的不一致性,包括脏读、不可重复读、幻读等。数据库系统提供了隔离级别来让我们有针对性地选择事务的隔离级别,避免数据不一致的问题。
SQL 标准定义了 4 种隔离级别,它们之间是逐步增强的关系,从低到高:
Read Uncommitted < Read Committed < Repeatable Read < Serializable
隔离级别越高,并发性能越差,但数据一致性越好。
隔离级别 | 脏读 | 不可重复读 | 幻读 | 说明 |
---|---|---|---|---|
🔴 Read Uncommitted | ✅ 会 | ✅ 会 | ✅ 会 | 最弱,任何事务都能读到未提交数据,几乎不隔离 |
🟠 Read Committed(Oracle 默认) | ❌ 不会 | ✅ 会 | ✅ 会 | 只能读到已提交数据,但数据可能变 |
🟡 Repeatable Read(MySQL 默认) | ❌ 不会 | ❌ 不会 | ✅ 会 | 多次读取同一数据结果一致,但新增行可能出现(幻读) |
🟢 Serializable | ❌ 不会 | ❌ 不会 | ❌ 不会 | 最强,事务串行执行,性能最差但最安全 |
实战建议:
不设置时,Spring 默认使用数据库自身的隔离级别(
Isolation.DEFAULT
)一般建议使用
READ_COMMITTED
或REPEATABLE_READ
只有在特别敏感(如财务、统计)场景下,才考虑
SERIALIZABLE
6-1、Read Uncommitted
Read Uncommitted是隔离级别最低的一种事务级别。
在这种隔离级别下,一个事务会读到另一个事务更新后但未提交的数据,如果另一个事务回滚,那么当前事务读到的数据就是脏数据,这就是脏读(Dirty Read)。
示例:
首先,我们准备好students
表的数据,该表仅一行记录:
然后,分别开启两个MySQL客户端连接,按顺序依次执行事务A和事务B:
时刻 | 事务A | 事务B |
---|---|---|
1 | SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; | SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; |
2 | BEGIN; | BEGIN; |
3 | UPDATE students SET name = 'Bob' WHERE id = 1; | |
4 | SELECT * FROM students WHERE id = 1; | |
5 | ROLLBACK; | |
6 | SELECT * FROM students WHERE id = 1; | |
7 | COMMIT; |
当事务A执行完第3步时,它更新了id=1
的记录,但并未提交,而事务B在第4步读取到的数据就是未提交的数据。
随后,事务A在第5步进行了回滚,事务B再次读取id=1
的记录,发现和上一次读取到的数据不一致,这就是脏读。
可见,在Read Uncommitted隔离级别下,一个事务可能读取到另一个事务更新但未提交的数据,这个数据有可能是脏数据。
6-2、Read Committed
在Read Committed隔离级别下,一个事务不会读到另一个事务还没有提交的数据,但可能会遇到不可重复读(Non Repeatable Read)的问题。
不可重复读是指,在一个事务内,多次读同一数据,在这个事务还没有结束时,如果另一个事务恰好修改了这个数据,那么,在第一个事务中,两次读取的数据就可能不一致。
即:一个未提交的事务读到了另一个提交事务的修改的数据。
仍然先准备好students
表的数据:
然后,分别开启两个MySQL客户端连接,按顺序依次执行事务A和事务B:
时刻 | 事务A | 事务B |
---|---|---|
1 | SET TRANSACTION ISOLATION LEVEL READ COMMITTED; | SET TRANSACTION ISOLATION LEVEL READ COMMITTED; |
2 | BEGIN; | BEGIN; |
3 | SELECT * FROM students WHERE id = 1; -- Alice | |
4 | UPDATE students SET name = 'Bob' WHERE id = 1; | |
5 | COMMIT; | |
6 | SELECT * FROM students WHERE id = 1; -- Bob | |
7 | COMMIT; |
当事务B第一次执行第3步的查询时,得到的结果是Alice
,随后,由于事务A在第4步更新了这条记录并提交,所以,事务B在第6步再次执行同样的查询时,得到的结果就变成了Bob
。
因此,在Read Committed隔离级别下,事务不可重复读同一条记录,因为很可能读到的结果不一致。
6-3、Repeatable Read(可重复度: mysql默认)
在Repeatable Read隔离级别下,一个事务可能会遇到幻读(Phantom Read)的问题。
幻读是指,在一个事务中,第一次查询某条记录,发现没有,但是,当试图更新这条不存在的记录时,竟然能成功,并且,再次读取同一条记录,它就神奇地出现了。
即:一个未提交的事务读到了另一个提交事务的添加/删除的数据。
先准备好students
表的数据:
然后,分别开启两个MySQL客户端连接,按顺序依次执行事务A和事务B:
时刻 | 事务A | 事务B |
---|---|---|
1 | SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; | SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; |
2 | BEGIN; | BEGIN; |
3 | SELECT * FROM students WHERE id = 99; -- empty | |
4 | INSERT INTO students (id, name) VALUES (99, 'Bob'); | |
5 | COMMIT; | |
6 | SELECT * FROM students WHERE id = 99; -- empty | |
7 | UPDATE students SET name = 'Alice' WHERE id = 99; -- 1 row affected | |
8 | SELECT * FROM students WHERE id = 99; -- Alice | |
9 | COMMIT; |
事务B在第3步第一次读取id=99
的记录时,读到的记录为空,说明不存在id=99
的记录。随后,事务A在第4步插入了一条id=99
的记录并提交。事务B在第6步再次读取id=99
的记录时,读到的记录仍然为空,但是,事务B在第7步试图更新这条不存在的记录时,竟然成功了,并且,事务B在第8步再次读取id=99
的记录时,记录出现了。
可见,幻读就是没有读到的记录,以为不存在,但其实是可以更新成功的,并且,更新成功后,再次读取,就出现了。
Repeatable Read: 事务安全性:中等;并发性能:中等。大多数系统都适合。
不可重复读 VS 幻读
不可重复读(Non-Repeatable Read) 和 幻读(Phantom Read) 非常像,都是指在同一个事务中,两次查询结果不一致。
但它们本质不同,关键在于:
类型 | 操作对象 | 第二次读到的数据不同,原因是? |
---|---|---|
🟠 不可重复读 | 同一行 | 另一事务 修改了该行的值 |
🟡 幻读 | 多行(集合、范围) | 另一事务 插入或删除了新行 |
对应隔离级别防护效果:
隔离级别 | 不可重复读 | 幻读 |
---|---|---|
Read Committed | ❌ 不防止 | ❌ 不防止 |
Repeatable Read(MySQL 默认) | ✅ 防止 | ❌ 多数场景防不住 |
Serializable | ✅ 防止 | ✅ 防止 |
6-4、Serializable(串行化)
Serializable是最严格的隔离级别。在Serializable隔离级别下,所有事务按照次序依次执行,因此,脏读、不可重复读、幻读都不会出现。
虽然Serializable隔离级别下的事务具有最高的安全性,但是,由于事务是串行执行,所以效率会大大下降,应用程序的性能会急剧降低。
如果没有特别重要的情景,一般都不会使用Serializable隔离级别。
七、默认隔离级别
如果没有指定隔离级别,数据库就会使用默认的隔离级别。
数据库 | 默认隔离级别 |
---|---|
MySQL(InnoDB) | Repeatable Read |
Oracle | Read Committed |
PostgreSQL | Read Committed |
八、设置事务隔离级别
1、SQL 设置(会话级)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
2、Spring 中设置(编程方式)
@Transactional(isolation = Isolation.READ_COMMITTED)
public void doSomething() {...
}
3、隔离级别选择建议(实践角度)
场景 | 推荐隔离级别 |
---|---|
对性能极度敏感、读多写少 | Read Committed(二级) |
大多数业务系统,追求一致性 | Repeatable Read(三级) |
银行/核心财务系统 | Serializable(四级) |
九、JDBC事务
要在JDBC中执行事务,本质上就是如何把多条SQL包裹在一个数据库事务中执行。我们来看JDBC的事务代码:
Connection conn = openConnection();
try {// 关闭自动提交:conn.setAutoCommit(false);// 执行多条SQL语句:insert(); update(); delete();// 提交事务:conn.commit();
} catch (SQLException e) {// 回滚事务:conn.rollback();
} finally {conn.setAutoCommit(true);conn.close();
}
其中,开启事务的关键代码是conn.setAutoCommit(false)
,表示关闭自动提交。
提交事务的代码在执行完指定的若干条SQL语句后,调用conn.commit()
。
要注意事务不是总能成功,如果事务提交失败,会抛出SQL异常(也可能在执行SQL语句的时候就抛出了),此时我们必须捕获并调用conn.rollback()
回滚事务。
最后,在finally
中通过conn.setAutoCommit(true)
把Connection
对象的状态恢复到初始值。
实际上,默认情况下,我们获取到Connection
连接后,总是处于“自动提交”模式,也就是每执行一条SQL都是作为事务自动执行的,这也是为什么一般我们的更新操作总能成功的原因:因为默认有这种“隐式事务”。
只要关闭了Connection
的autoCommit
,那么就可以在一个事务中执行多条语句,事务以commit()
方法结束。
如果要设定事务的隔离级别,可以使用如下代码:
// 设定隔离级别为READ COMMITTED:
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
如果没有调用上述方法,那么会使用数据库的默认隔离级别。MySQL的默认隔离级别是REPEATABLE_READ。
十、
事务提交、回滚、嵌套等操作
1. 事务提交:一切正常,事务提交成功
场景:A 转账给 B(100元),操作成功
BEGIN 事务 T1
├── UPDATE A 扣100 ✔
├── UPDATE B 加100 ✔
└── COMMIT ✔
Java + Spring 代码:
@Transactional
public void transfer() {accountDao.debit("A", 100); // A 扣钱accountDao.credit("B", 100); // B 加钱// 没有异常 → 自动提交
}
结果:
-
A 扣100,B 加100
-
数据库数据持久化,提交成功
2. 事务回滚:中间出错,全部取消
场景:A 扣钱成功,但 B 加钱时报错(或宕机)
BEGIN 事务 T2
├── UPDATE A 扣100 ✔
├── 异常发生(B 账户不存在) ❌
└── ROLLBACK ❌
Java + Spring 代码:
@Transactional
public void transfer() {accountDao.debit("A", 100); // A 扣钱int x = 1 / 0; // 模拟异常accountDao.credit("B", 100); // B 加钱没执行
}
结果:
-
虽然 A 扣钱执行了,但整个事务被回滚,A 账户余额恢复原状。
3. 嵌套事务:内层方法出错会不会影响外层?
场景:转账前要记录一条流水日志,结果日志记录失败
图示 1:默认传播(Propagation.REQUIRED)
BEGIN T3
├── 外层 transfer() 开启事务 T3
│ ├── debit(A) 扣100 ✔
│ ├── logService.saveLog() ❌ 异常
│ └── rollback(T3) ❌ 回滚全部
所有方法共用一个事务 T3,任何一处出错都回滚!!!
示例代码:
// 外层方法
@Transactional
public void transfer() {accountDao.debit("A", 100);logService.saveLog(); // 日志保存失败accountDao.credit("B", 100);
}// 内层方法
@Transactional
public void saveLog() {throw new RuntimeException("日志记录失败");
}
结果:
-
debit()
成功,但因saveLog()
报错,整个事务回滚,debit()
的扣款也被撤销。
图示 2:内层使用新事务(Propagation.REQUIRES_NEW)
BEGIN T4
├── 外层 transfer() 开启事务 T4
│ ├── debit(A) 扣100 ✔
│ ├── BEGIN T5
│ │ └── saveLog() ❌ 报错 → rollback T5
│ ├── credit(B) 加100 ✔
│ └── COMMIT T4 ✔
修改内层传播行为:
// 外层方法
@Transactional
public void transfer() {accountDao.debit("A", 100);try {logService.saveLog(); // 出错了,但只是 T5 的事} catch (Exception e) {System.out.println("记录日志失败");}accountDao.credit("B", 100);
}// 内层方法
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog() {throw new RuntimeException("日志保存失败");
}
结果:
-
扣款成功
-
日志保存失败,但不影响主业务
-
加钱也成功
-
只回滚
saveLog()
的 T5 事务,T4 提交
❗ 常见坑:事务注解不生效
问题场景 | 原因 |
---|---|
方法 A 内调用方法 B(同类中) | Spring 的事务是通过代理实现的,自己调用自己绕过代理 |
没有被 @Service/@Component 扫描到 | Spring 没有管理,事务注解不起作用 |
使用 try-catch 吞掉了异常 | 异常没抛出 → Spring 不会回滚 |
异常是 Checked(如 IOException ) | 默认只回滚 RuntimeException 和 Error |
4、小结:事务控制图表
操作 | 方法 | 会自动提交吗? | 手动回滚? |
---|---|---|---|
单个方法事务 | @Transactional | 是(无异常) | 自动回滚 |
嵌套调用(默认) | REQUIRED | 共用一个事务 | 一处异常,全回滚 |
嵌套调用(隔离) | REQUIRES_NEW | 各自独立事务 | 内部回滚,不影响外部 |
5、推荐实践建议
-
默认使用
@Transactional
(Propagation.REQUIRED) -
日志、通知等“非核心逻辑”可用
REQUIRES_NEW
-
统一异常捕获 + 日志记录
-
注意避免同类内部方法调用导致事务失效
-
在方法签名中使用
throws Exception
保证异常传播
十一、事务与throws Exception 保证异常传播
在使用 Spring @Transactional
注解的方法中,方法签名加上 throws Exception
可以帮助确保异常真正抛出,从而让事务机制生效。
为什么要这样做?
Spring 的事务管理是通过 AOP 代理机制实现的,它默认只对未被捕获并抛出的异常(且是 RuntimeException
或 Error
)才会触发事务回滚。
所以问题就来了,如果你这样写:
@Transactional
public void doSomething() {try {// 出错了int a = 1 / 0;} catch (Exception e) {e.printStackTrace(); // 捕获但没有向外抛}
}
⚠️ 结果:不会回滚!
-
因为异常被你自己 try-catch 掉了,Spring AOP 没有感知到出错
-
所以事务会正常提交
正确写法 1:继续向外抛异常
@Transactional
public void doSomething() throws Exception {// 模拟异常int a = 1 / 0;
}
会触发 Spring 回滚。
正确写法 2:不要吞掉异常
@Transactional
public void doSomething() {int a = 1 / 0; // 异常抛出,Spring 会自动回滚
}
正确写法 3:自定义异常也能回滚(要指定)
@Transactional(rollbackFor = Exception.class)
public void doSomething() throws Exception {throw new Exception("自定义异常");
}
-
Spring 默认 只回滚 RuntimeException
-
如果你抛的是
Exception
(受检异常),需要写rollbackFor = Exception.class
记住一句话:
只要你想让事务在出错时自动回滚,就让异常不要“死在方法体内”,要抛出去,Spring 才能捕捉它并执行回滚。
十二、声明式事务管理的参数配置
12-1、propagation:事务的传播行为
当前方法执行时,是否要使用已有事务?如果没有,是否要新建一个事务?
常见的 7 种传播行为(由 Spring 定义)
Propagation | 中文名 | 行为说明 |
---|---|---|
REQUIRED (默认) | 如果存在事务就加入,否则新建事务 | ✅最常用 |
REQUIRES_NEW | 总是新建事务,原事务挂起 | 独立性强 |
NESTED | 如果存在事务,则嵌套执行(使用保存点) | 类似子事务 |
SUPPORTS | 有事务就加入,没有就非事务执行 | 弱依赖事务 |
NOT_SUPPORTED | 永远不使用事务(挂起原事务) | 禁用事务 |
MANDATORY | 必须有事务,没有就抛异常 | 强依赖事务 |
NEVER | 不能有事务,有就抛异常 | 严禁事务 |
上面这么多种事务的传播级别,其实默认的
REQUIRED
已经满足绝大部分需求。
SUPPORTS
和REQUIRES_NEW
在少数情况下会用到,其他基本不会用到,因为把事务搞得越复杂,不仅逻辑跟着复杂,而且速度也会越慢。
1、REQUIRED
(默认)
Spring的声明式事务,默认传播级别就是REQUIRED,它的意思是,如果当前没有事务,就创建一个新事务,如果当前有事务,就加入到当前事务中执行。
调用方法 A(有事务)→ 调用方法 B(@Transactional(REQUIRED))→ 继续使用 A 的事务(共生死)
2、REQUIRES_NEW
表示不管当前有没有事务,都必须开启一个新的事务执行。如果当前已经有事务,那么当前事务会挂起,等新事务完成后,再恢复执行;
调用方法 A(有事务)→ 调用方法 B(@Transactional(REQUIRES_NEW))→ 挂起 A,开启一个新事务执行 B→ B 提交或回滚不会影响 A
3、SUPPORTS
Propagation.SUPPORTS
是 Spring 事务传播行为中最“温和”的一种。
如果当前线程中已经有事务,就加入事务;
如果没有事务,就以非事务方式运行。即:有事务就用,没有也无所谓。这种传播级别可用于查询方法,因为SELECT语句既可以在事务内执行,也可以不需要事务;
举例说明:
@Transactional(propagation = Propagation.SUPPORTS)
public void readUserData() {// 执行查询或轻量逻辑
}
对比场景演示:
场景一:方法 A 有事务
@Transactional // 默认 REQUIRED
public void methodA() {service.readUserData(); // 有事务环境 → readUserData 加入事务
}
场景二:方法 A 没有事务
public void methodA() {service.readUserData(); // 没事务环境 → readUserData 以非事务执行
}
使用场景建议:
适合情况 | 原因 |
---|---|
数据读取方法 | 读操作大多数不要求事务一致性 |
可接受“非事务执行”的轻量方法 | 提高性能,避免无谓开销 |
多模块通用工具类 | 可用于事务环境,也能在普通环境中使用 |
❗不适合的情况:
-
涉及数据库写操作(插入、更新、删除)
-
希望强制走事务(否则可能出现数据不一致)
示例代码对比
@Service
public class UserService {@Transactional // 主事务public void saveUser() {userDao.insert(); // 写操作logService.logOp(); // logOp 用 SUPPORTS}
}@Service
public class LogService {@Transactional(propagation = Propagation.SUPPORTS)public void logOp() {// 有事务就加入 → 和 insert 同生共死// 没事务就裸跑 → 独立执行,可能写入日志但主流程失败}
}
方法 | 传播行为 | 加入的事务 | 和谁共生死? |
---|---|---|---|
saveUser() | REQUIRED (默认) | 开启主事务 | 本身就是事务发起者 |
insert() | 默认或同类方法 | 使用主事务 | 属于主事务 |
logOp() | SUPPORTS | 加入主事务(如果有) | 和主事务共生死 |
与其他传播行为的对比
Propagation | 有事务时 | 无事务时 |
---|---|---|
REQUIRED | 加入 | 新建事务 |
REQUIRES_NEW | 挂起原事务,创建新事务 | 创建新事务 |
SUPPORTS | 加入 | 非事务执行(⚠️) |
MANDATORY | 加入 | 报错 |
NEVER | 报错 | 非事务执行 |
一句话总结
SUPPORTS
适合“你不强求事务,但如果有最好也用上”的场景。多用于读操作、日志、权限等副作用小的逻辑中。
12-2、timeout:超时时间
事务在多长时间内必须完成,如果超过这个时间,事务会自动回滚,抛出
TransactionTimedOutException
异常。
1、使用场景
-
某些操作耗时较长(如远程调用、复杂批处理)
-
不希望事务“长时间占用数据库连接”
-
防止慢 SQL 或代码死循环导致资源被长期锁死
2、timeout 与 SQL 超时区别?
类型 | 作用范围 | 控制方式 |
---|---|---|
@Transactional(timeout) | 控制整个事务时间 | Spring 管理 |
JDBC / SQL 查询超时 | 控制某条 SQL 执行时间 | JdbcTemplate.setQueryTimeout() 或数据库层配置 |
3、示例:模拟超时
@Transactional(timeout = 3)
public void doWork() throws InterruptedException {System.out.println("开始事务...");Thread.sleep(5000); // 模拟耗时操作System.out.println("完成事务...");
}
运行时会抛出:
org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was 3 seconds
4、总结:
属性 | 默认值 | 单位 | 作用 |
---|---|---|---|
timeout | -1 (永不超时) | 秒 | 限制事务执行时间 |
12-3、readOnly:只读查询
readOnly = true
表示该事务只执行读操作,不会进行写操作。
Spring 会通知数据库做相应优化,比如:不加行级锁、跳过事务日志。
示例:
@Transactional(readOnly = true) // 默认是false
public List<User> getAllUsers() {return userDao.findAll();
}
哪些组件会强制执行 readOnly = true
:
组件 | 是否会设置 connection.setReadOnly(true) |
---|---|
Spring + JdbcTemplate + readOnly = true | ✅ 会 |
数据库连接池(如 Druid、HikariCP) | ✅ 遵守 setReadOnly 设置 |
数据库(如 PostgreSQL、Oracle) | ✅ 会抛错 |
MySQL(默认 InnoDB 引擎) | ✅ 会抛错,尤其在连接池启用严格模式时 |
12-4、timeout 和 readOnly
一起使用
你可以同时使用:
@Transactional(readOnly = true, timeout = 2)
public List<User> queryData() {// 只读查询,限时 2 秒
}
适合对数据库负担较重的复杂查询操作加限制。
12-5、rollbackFor
与 noRollbackFor
发生某个异常时,事务是否要回滚。
Spring 的默认规则是:
异常类型 | 是否回滚事务 |
---|---|
运行时异常(RuntimeException 及其子类) | ✅ 会回滚 |
受检异常(Exception ,不包括 RuntimeException) | ❌ 不回滚 |
示例:
@Transactional
public void save() throws IOException {// 抛出 IOException 是受检异常,默认不会回滚
}
自定义回滚或非回滚异常
参数 | 类型 | 说明 |
---|---|---|
rollbackFor | Class<?>[] | 指定异常发生时强制回滚 |
noRollbackFor | Class<?>[] | 指定异常发生时不回滚(即使它本来应该回滚) |
示例 1:指定回滚某个受检异常
@Transactional(rollbackFor = IOException.class)
public void save() throws IOException {// 抛出 IOException → 会回滚throw new IOException("IO failed");
}
如果不加 rollbackFor
,这个事务是不会回滚的。
示例 2:指定某个运行时异常不要回滚
@Transactional(noRollbackFor = ArithmeticException.class)
public void save() {// 抛出算术异常 → 不回滚int x = 1 / 0;
}
默认情况下,ArithmeticException
会回滚,但此处明确指定 不回滚。
多个异常同时指定:
@Transactional(rollbackFor = {SQLException.class, IOException.class},noRollbackFor = {IllegalArgumentException.class}
)
十三、使用完全注解方式实现声明式的事务管理
-
使用
@Configuration
配置类代替 XML -
启用事务注解:
@EnableTransactionManagement
-
标注事务方法:
@Transactional
-
配置数据源 + JdbcTemplate + 事务管理器
示例:
配置类 TxConfig:
@Configuration // 这是一个配置类
@ComponentScan(basePackages = "transactionSpring")
@EnableTransactionManagement // 开启事务
@PropertySource(value = "classpath:db.properties") //使用外部的配置文件
public class Txconfig {@Value("${db.driverClassName}")private String driverClassName;@Value("${db.url}")private String url;@Value("${db.username}")private String username;@Value("${db.password}")private String password;// 创建数据库的连接池@Beanpublic DruidDataSource getDruidDataSource(){DruidDataSource druidDataSource = new DruidDataSource();druidDataSource.setDriverClassName(driverClassName);druidDataSource.setUrl(url);druidDataSource.setUsername(username);druidDataSource.setPassword(password);return druidDataSource;}// 配置 JdbcTemplate@Beanpublic JdbcTemplate getJdbcTemplate(DataSource dataSource){// 到ioc容器中,根据类型找到dataSource,然后注入JdbcTemplate jdbcTemplate = new JdbcTemplate();// 注入dataSourcejdbcTemplate.setDataSource(dataSource);return jdbcTemplate;}// 创建事务管理器的对象@Beanpublic DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){return new DataSourceTransactionManager(dataSource);}}
Dao层
@Repository(value = "userDaoTrans")
public class UserDaoImpl implements UserDao {@Autowiredprivate JdbcTemplate jdbcTemplate;@Overridepublic void addUserAccount() {String sql = "update tb_user set account = account + ? where user_name = ?";jdbcTemplate.update(sql, 100, "王五");}@Overridepublic void minusUserAccount() {String sql = "update tb_user set account = account - ? where user_name = ?";jdbcTemplate.update(sql, 100, "张三");}
}
service层:
@Service(value = "userServiceTrans")
public class UserService {@Autowired@Qualifier(value = "userDaoTrans")private UserDao userDao;@Transactionalpublic void bankMoney(){userDao.addUserAccount();//int a = 10/0;userDao.minusUserAccount();System.out.println("执行完成");}}
测试类:
@Testpublic void test02(){ApplicationContext context = new AnnotationConfigApplicationContext(Txconfig.class);UserService userServiceTrans = context.getBean("userServiceTrans", UserService.class);userServiceTrans.bankMoney();}