讲讲Spring事务
# 讲讲 Spring 事务
# Spring 事务简介
Spring 事务管理是 Spring 框架中的一项重要功能,用于简化企业级应用中的事务管理操作。它通过声明式(Declarative)或编程式(Programmatic)的方式,帮助开发者保证数据的一致性、完整性和隔离性。
事务的核心目标是确保一组操作(如数据库读写)要么全部成功,要么全部失败(回滚),从而保证系统数据的可靠性。
# 事务的基本概念
- 事务(Transaction):事务是一组操作的集合,这些操作作为一个单元被执行。事务需要满足 ACID 特性:
- A(原子性):事务是一个不可分割的工作单元,要么全部执行,要么全部回滚。
- C(一致性):事务完成后,数据库必须从一个一致性状态变为另一个一致性状态。
- I(隔离性):多个事务并发执行时,一个事务的执行不会被其他事务干扰。
- D(持久性):事务一旦提交,其对数据库的修改就是永久的。
- 事务传播(Transaction Propagation):定义事务如何传播到被调用的方法。例如,如果一个方法已经启动了事务,另一个方法是否应该加入现有事务。
- 事务隔离级别(Transaction Isolation Level):定义多个事务并发执行时的行为,用于解决脏读、不可重复读、幻读等问题。
# Spring 事务管理的方式
Spring 提供了两种事务管理方式:
- 声明式事务管理:
- 推荐的方式。
- 使用
@Transactional
注解或 XML 配置事务管理。 - 简单易用,解耦了事务管理和业务逻辑代码。
- 编程式事务管理:
- 通过手动调用 Spring 的事务管理 API(
TransactionTemplate
或PlatformTransactionManager
)实现。 - 灵活但侵入性强,一般不推荐。
- 通过手动调用 Spring 的事务管理 API(
# 1、声明式事务管理
# 基于注解的声明式事务
@Transactional
注解是 Spring 中声明事务的核心注解,通常标注在类或方法上。
@Service
public class UserService {
@Transactional
public void createUser(String name, int age) {
// 插入用户数据
userDao.insert(name, age);
// 模拟一个异常,触发回滚
if (age < 0) {
throw new IllegalArgumentException("年龄不能为负数!");
}
// 插入日志数据
logDao.insert("用户创建成功:" + name);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
常见属性:
propagation
:设置事务的传播行为。isolation
:设置事务的隔离级别。timeout
:设置事务的超时时间(秒)。readOnly
:设置事务是否为只读。rollbackFor
:指定哪些异常会触发事务回滚。noRollbackFor
:指定哪些异常不会触发事务回滚。
# 事务传播行为(Propagation)
事务传播行为定义当前方法事务的处理方式。常见值如下:
值 | 描述 |
---|---|
REQUIRED | 默认值。如果当前有事务存在,则加入当前事务;如果没有,则新建一个事务。 |
REQUIRES_NEW | 总是新建一个事务。如果当前有事务存在,则暂停当前事务。 |
SUPPORTS | 如果当前有事务存在,则加入当前事务;如果没有事务存在,则以非事务方式执行。 |
NOT_SUPPORTED | 总是以非事务方式执行。如果当前有事务存在,则暂停当前事务。 |
MANDATORY | 必须在一个事务中执行,如果当前没有事务存在,则抛出异常。 |
NEVER | 总是以非事务方式执行,如果当前有事务存在,则抛出异常。 |
NESTED | 如果当前有事务存在,则在当前事务中嵌套一个子事务(依赖于底层数据库是否支持保存点)。 |
# 事务隔离级别(Isolation)
事务隔离级别用于控制并发事务之间的相互影响。Spring 支持以下五种隔离级别:
值 | 描述 |
---|---|
DEFAULT | 默认隔离级别,使用底层数据库的默认设置。(Mysql 默认是 可重复读) |
READ_UNCOMMITTED (未提交读) | 允许脏读、不可重复读和幻读。性能最高,数据安全性最低。 |
READ_COMMITTED (提交读) | 防止脏读,但可能发生不可重复读和幻读。 |
REPEATABLE_READ (可重复读) | 防止脏读和不可重复读,但可能发生幻读。 |
SERIALIZABLE (串行化) | 防止脏读、不可重复读和幻读。性能最低,但数据安全性最高。 |
各级别比较:
隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 |
---|---|---|---|---|
READ UNCOMMITTED | √ | √ | √ | 高 |
READ COMMITTED | × | √ | √ | 较高 |
REPEATABLE READ | × | × | × | 中 |
SERIALIZABLE | × | × | × | 低 |
# 2、编程式事务管理
编程式事务管理需要手动控制事务的开启、提交和回滚:
@Service
public class UserService {
@Autowired
private PlatformTransactionManager transactionManager;
public void createUser(String name, int age) {
TransactionDefinition def = new DefaultTransactionDefinition();
TransactionStatus status = transactionManager.getTransaction(def);
try {
// 插入用户数据
userDao.insert(name, age);
// 模拟一个异常
if (age < 0) {
throw new IllegalArgumentException("年龄不能为负数!");
}
// 插入日志数据
logDao.insert("用户创建成功:" + name);
// 提交事务
transactionManager.commit(status);
} catch (Exception e) {
// 回滚事务
transactionManager.rollback(status);
throw e;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 事务回滚的规则
默认行为:
- Spring 默认会对所有未捕获的运行时异常(
RuntimeException
和Error
)进行事务回滚。 - 对受检异常(
CheckedException
)不回滚。
- Spring 默认会对所有未捕获的运行时异常(
自定义回滚规则:
- 使用
@Transactional
的rollbackFor
或noRollbackFor
属性可以自定义回滚行为。
@Transactional(rollbackFor = Exception.class) public void someMethod() { // 代码逻辑 }
1
2
3
4- 使用
- 在
@Transactional
注解中如果不配置rollbackFor
属性,那么事务只会在遇到RuntimeException
的时候才会回滚- 加上
rollbackFor=Exception.class
, 可以让事务在遇到非运行时异常时也会回滚。
# 注意事项
- 方法必须是由 Spring 管理的【代理对象】调用,否则
@Transactional
不会生效。 - 事务传播与嵌套事务:理解传播机制,避免事务失控。
- 正确处理异常:捕获异常(不抛出)可能会导致事务失效。
- readOnly 属性:查询操作应设置为
readOnly = true
,以优化性能。
Spring 的事务是通过代理类实现的。
# 什么情况下事务会失效?
导致事务失效的主要原因
# 1、非 public
方法
原因:
- Spring 的
@Transactional
注解只在public
方法 上有效,因为 Spring 的事务管理基于 AOP(代理模式)。如果@Transactional
注解在非public
方法上,代理类无法拦截调用,事务不会生效。
解决方法:
- 确保带有
@Transactional
注解的方法是public
。
# 2、自身方法调用(内部方法调用)
原因:
- Spring 的事务是通过代理类实现的。如果一个方法调用同类中的另一个方法(即 内部方法调用),此时不会通过代理类,而是直接调用实际方法,导致事务失效。
示例:
@Service
public class TransactionService {
@Transactional
public void methodA() {
methodB(); // 不会经过代理,事务失效
}
@Transactional
public void methodB() {
// 事务代码
}
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
解决方法:
将需要事务管理的方法提取到 其他类,通过类间调用确保事务代理生效。
使用 Spring 的
AopContext
手动获取代理对象调用方法:public void methodA() { ((TransactionService) AopContext.currentProxy()).methodB(); }
1
2
3
# 3、异常未被正确传播
原因:
- Spring 默认只会回滚 运行时异常(
RuntimeException
) 和 错误(Error
)。 - 如果方法抛出了非运行时异常(如
CheckedException
),Spring 不会自动回滚事务,导致事务提交。
示例:
@Transactional
public void updateData() throws IOException {
throw new IOException("非运行时异常");
}
1
2
3
4
2
3
4
解决方法:
显式配置事务的回滚规则:
@Transactional(rollbackFor = IOException.class) public void updateData() throws IOException { // ... }
1
2
3
4将非运行时异常包装为运行时异常重新抛出:
throw new RuntimeException(e);
1
# 4、捕获异常后不抛出
原因:
- 如果异常被
try-catch
块捕获并处理(不抛出),Spring 事务管理器无法感知到异常发生,事务不会回滚。
示例:
@Transactional
public void updateData() {
try {
// 事务操作
updateTable1();
updateTable2(); // 抛出异常
} catch (Exception e) {
// 异常被捕获,事务未回滚
System.out.println("异常被捕获:" + e.getMessage());
}
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
解决方法:
捕获异常后手动回滚:
@Transactional public void updateData() { try { updateTable1(); updateTable2(); } catch (Exception e) { TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); throw e; } }
1
2
3
4
5
6
7
8
9
10避免捕获异常,让 Spring 事务管理器自动处理。
# 5、多线程场景
原因:
- Spring 事务是基于 ThreadLocal 管理事务上下文的,事务默认只能在当前线程中生效。
- 如果在事务方法中开启了新线程,线程上下文不再共享,事务失效。
示例:
@Transactional
public void updateData() {
new Thread(() -> {
// 此处事务无效
updateTable();
}).start();
}
1
2
3
4
5
6
7
2
3
4
5
6
7
解决方法:
- 避免直接在事务方法中启动线程。
- 如果需要异步操作,可以使用 Spring 的异步事务支持(例如
@Async
和事务结合)。 - 手动提交事务并手动处理回滚
# 6、数据库引擎不支持事务
原因:
- MySQL 中只有 InnoDB 引擎支持事务。如果使用的是不支持事务的存储引擎(如 MyISAM),事务相关操作将失效。
解决方法:
确保数据库表的存储引擎为 InnoDB:
ALTER TABLE table_name ENGINE=InnoDB;
1
注意事项:
- 从 MySQL 5.5 开始,MySQL 的默认存储引擎就是 InnoDB。
- 在此之前,MySQL 的默认存储引擎是 MyISAM。因此,现代版本的 MySQL 默认支持事务,因为 InnoDB 是一个支持事务的存储引擎。
- 即使是 MySQL 5.5+ 版本,也有可能是 MyISAM。因为可手动设置。
# 7、方法未被代理管理
原因:
- Spring 的事务管理基于代理。
- 如果一个方法未被 Spring 容器管理(例如直接使用
new
创建对象实例),Spring 的事务管理器不会生效。
解决方法:
- 确保事务方法是由 Spring 容器托管的,不能直接使用
new
。
# 8、事务传播配置错误
原因:
- Spring 支持多种事务传播行为(如
REQUIRED
、REQUIRES_NEW
等),不同传播行为在嵌套调用时可能导致事务失效。例如:- 如果外部事务回滚,而嵌套事务没有独立的事务上下文,嵌套事务的操作也会被回滚。
示例:
@Transactional
public void methodA() {
methodB(); // 默认传播行为为 REQUIRED,使用同一个事务
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
// 独立事务,方法完成后提交
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
解决方法:
- 根据业务需要,合理选择传播行为。
# 正确使用事务传播行为的场景
适合使用事务传播行为的场景
- 需要分开提交或回滚的事务:如果希望某些方法开启独立事务,与主事务隔离(如日志记录、异步任务处理等),可以使用 REQUIRES_NEW。
- 嵌套事务回滚:如果希望某些方法是主事务的子事务,可以使用 NESTED,在子事务出错时回滚到保存点。
不适合使用事务传播行为的场景
- 解决类内方法调用的问题:如果只是为了修复类内调用导致的事务失效问题,不建议仅依赖事务传播行为,而是应通过代理调用方法。
上次更新: 2025/2/6 17:50:04