《SpringBoot 3:入门与应用实战》第 12 章 JDBC 与事务 Spring Framework 的事务管理 阅读笔记 34
12.4 Spring Framework 的事务管理
12.4.1 代码准备
为了保持 Controller-Service-Dao 的三层架构风格,需要补充一个 UserService 作为业务层的实现,为了演示方便,此处没有使用接口+实现类的方式。UserService 中提供了一个 saveAndQuery 方法,其中包含一个用户的保存动作和查询动作,在这两个动作之间插入一个引发除零异常的计算。希望当 saveAndQuery 方法执行时,保存动作之后出现除零异常,从而使保存动作回滚。

java
package com.yangjunbo.springboot.jdbc.exampleb;
import com.yangjunbo.springboot.jdbc.examplea.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
@Autowired
private UserDao userDao;
public void saveAndQuery() {
User user = new User();
user.setName("测试保存用户");
user.setTel("123654789");
userDao.save(user);
int i = 1 / 0;
List<User> userList = userDao.findAll();
System.out.println(userList);
}
}
之后再创建一个新的 TransactionController,测试用的所有事务控制相关的代码和接口都放在这个 Controller 下。在 TransactionController 中注入 UserService 并调用其 saveAndQuery 方法。

java
package com.yangjunbo.springboot.jdbc.exampleb;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TransactionController {
@Autowired
private UserService userService;
@GetMapping("/tx1")
public String tx1() {
userService.saveAndQuery();
return "success";
}
}
测试代码准备完毕后可以先测试一下效果,重启应用后访问 /tx1 接口,浏览器中会显示除零异常的信息,但此时查询数据库的 tbl_user 表时,却发现数据库中插入了一条数据。这个运行结果与期望的不符,所以需要引入事务控制。

最后一步不是必需的,可以在 Spring Boot 的主启动类 SpringBootJdbcApplication 上标注一个开启事务控制的注解@EnableTransactionManagement,这个注解会在底层注册很多与事务控制相关的组件,当然不标注该注解也可以,因为 Spring Boot 在自动装配时已经使用了该注解。
12.4.2 编程式事务控制
Spring Framework 提供的事务控制都非常简单,下面先讲解编程式事务的使用方法。得益于 Spring Boot 的约定大于配置,不需要进行任何配置,即可得到一个能够控制事务的模板类 TransactionTemplate,使用它可以很轻松地完成事务控制。
TransactionTemplate 的使用方式相当简单,只需要在 UserService 中注入它,并且在需要控制事务的方法中调用其 executeWithoutResult 方法,并将业务逻辑代码写入传入的 Lambda 表达式中。代码提供了一个 TransactionTemplate 的使用方式,与 executeWithoutResult 方法相似的还有一个 execute 方法,使用这个方法可以得到 Lambda 表达式的返回值,不过通常情况下不会用到内部的返回值,所以更常使用的是executeWithoutResult 方法(注意 executeWithoutResult 方法来自 Spring Framework 5.2,之前较低的版本中只能使用 execute 方法)。

java
package com.yangjunbo.springboot.jdbc.exampleb;
import com.yangjunbo.springboot.jdbc.examplea.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
import java.util.List;
@Service
public class UserService {
@Autowired
private UserDao userDao;
public void saveAndQuery() {
User user = new User();
user.setName("测试保存用户");
user.setTel("123654789");
userDao.save(user);
int i = 1 / 0;
List<User> userList = userDao.findAll();
System.out.println(userList);
}
@Autowired
private TransactionTemplate transactionTemplate;
public void saveAndQuery2() {
transactionTemplate.executeWithoutResult(transactionStatus -> {
User user = new User();
user.setName("测试保存用户2");
user.setTel("123654789");
userDao.save(user);
int i = 1 / 0;
List<User> userList = userDao.findAll();
System.out.println(userList);
});
}
}
接下来在 TransactionController 中编写一个 tx2 方法,并在其中调用 saveAndQuery2 方法。重新编译后访问 /tx2 接口,浏览器中仍然会响应异常信息,不过此时查看 tbl_user 表时会发现没有新的数据被插入,证明编程式事务控制已经生效。

java
package com.yangjunbo.springboot.jdbc.exampleb;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TransactionController {
@Autowired
private UserService userService;
@GetMapping("/tx1")
public String tx1() {
userService.saveAndQuery();
return "success";
}
@GetMapping("/tx2")
public String tx2() {
userService.saveAndQuery2();
return "success";
}
}
12.4.3 声明式事务控制
之所以称使用 TransactionTemplate 为编程式事务,是因为每个 Service 中都需要编码注入一个 TransactionTemplate 类,并在每个需要事务控制的方法中都使用 executeWithoutResult 方法,这本身会产生大量模板代码,效率相对低一些。为此 Spring Framework 提供了第二套方案:声明式事务控制。使用声明式事务可以实现更加简单的事务控制。
1.使用 @Transactional
Spring Framework 中提供的声明式事务控制是用注解实现的,只需要在需要控制事务的方法上标注 @Transactional。代码中使用@Transactional 对 saveAndQuery3 方法进行事务控制,编写相应的 Controller 层代码后重新编译,之后访问 /tx3 接口,测试结果说明声明式事务控制也生效了。

java
package com.yangjunbo.springboot.jdbc.exampleb;
import com.yangjunbo.springboot.jdbc.examplea.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
@Service
public class UserService {
@Autowired
private UserDao userDao;
public void saveAndQuery() {
User user = new User();
user.setName("测试保存用户");
user.setTel("123654789");
userDao.save(user);
int i = 1 / 0;
List<User> userList = userDao.findAll();
System.out.println(userList);
}
@Autowired
private TransactionTemplate transactionTemplate;
public void saveAndQuery2() {
transactionTemplate.executeWithoutResult(transactionStatus -> {
User user = new User();
user.setName("测试保存用户2");
user.setTel("123654789");
userDao.save(user);
int i = 1 / 0;
List<User> userList = userDao.findAll();
System.out.println(userList);
});
}
@Transactional
public void saveAndQuery3() {
User user = new User();
user.setName("测试保存用户");
user.setTel("123654789");
userDao.save(user);
int i = 1 / 0;
List<User> userList = userDao.findAll();
System.out.println(userList);
}
}

java
package com.yangjunbo.springboot.jdbc.exampleb;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TransactionController {
@Autowired
private UserService userService;
@GetMapping("/tx1")
public String tx1() {
userService.saveAndQuery();
return "success";
}
@GetMapping("/tx2")
public String tx2() {
userService.saveAndQuery2();
return "success";
}
@GetMapping("/tx3")
public String tx3() {
userService.saveAndQuery3();
return "success";
}
}
2.@Transactional 的属性
@Transactional 注解是 Spring 声明式事务管理的核心,它提供了多个属性来精细控制事务的行为。以下是该注解中常用属性的详细解析:
⚙️ @Transactional 核心属性详解
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| propagation | Propagation |
REQUIRED |
事务传播行为。定义了当一个事务方法被另一个事务方法调用时,事务应该如何传播。例如,是加入当前事务,还是新建一个独立事务。 |
| isolation | Isolation |
DEFAULT |
事务隔离级别 。定义了事务之间的数据可见性,用于解决脏读、不可重复读和幻读等并发问题。DEFAULT 表示使用底层数据库的默认隔离级别。 |
| timeout | int |
-1 |
事务超时时间 (单位:秒)。如果事务在指定时间内未完成,将自动回滚。-1 表示永不超时。 |
| readOnly | boolean |
false |
是否为只读事务 。设置为 true 可以提示数据库和持久化框架进行性能优化(如跳过脏检查、使用更轻量级的锁)。 |
| rollbackFor | Class[] |
{} |
触发回滚的异常类型 。默认情况下,只有 RuntimeException 和 Error 会触发回滚。通过此属性可以指定其他异常(如 Exception.class)也触发回滚。 |
| noRollbackFor | Class[] |
{} |
不触发回滚的异常类型。指定哪些异常发生时,事务依然会正常提交,而不是回滚。 |
| transactionManager | String |
"" |
指定事务管理器。当项目中存在多个数据源和多个事务管理器时,用于指定当前事务应使用哪个事务管理器。 |
🔍 重点属性深入解析
1. 事务传播行为 (propagation)
这是最核心也是最复杂的属性,共有 7 种传播行为,其中最常用的是:
REQUIRED(默认):如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是绝大多数业务场景的首选。- **
REQUIRES_NEW:无论当前是否存在事务,都会创建一个新的事务,并将当前事务挂起。新事务与外层事务完全独立,互不影响。常用于记录日志、发送消息等需要独立提交的操作。 NESTED:如果当前存在事务,则在嵌套事务内执行(通过数据库的 Savepoint 实现);如果当前没有事务,则创建一个新事务。嵌套事务回滚不会影响外层事务,但外层事务回滚会带动嵌套事务一起回滚。
2. 事务隔离级别 (isolation)
用于控制并发事务之间的数据可见性:
READ_UNCOMMITTED:最低级别,允许读取未提交的数据,可能导致脏读、不可重复读和幻读。READ_COMMITTED:保证只能读取已提交的数据,解决了脏读问题,但不可重复读和幻读仍可能发生。这是 Oracle、SQL Server 等数据库的默认级别。REPEATABLE_READ:保证在同一事务中多次读取同一数据的结果一致,解决了脏读和不可重复读问题。这是 MySQL InnoDB 引擎的默认级别,通过 MVCC 和间隙锁在很大程度上也解决了幻读问题。SERIALIZABLE:最高级别,强制事务串行执行,彻底解决所有并发问题,但性能开销最大。
3. 回滚规则 (rollbackFor / noRollbackFor)
Spring 的默认回滚策略是:仅在抛出 RuntimeException 及其子类或 Error 时回滚 。对于受检异常(Checked Exception,如 IOException、SQLException),默认不会回滚。
最佳实践 :在实际开发中,为了保证数据一致性,通常建议显式指定 rollbackFor = Exception.class,让所有异常都触发回滚。
java
// 推荐写法:明确指定所有异常都触发回滚
@Transactional(rollbackFor = Exception.class)
public void createUser(User user) throws IOException {
// ... 业务逻辑
}
4. 只读事务 (readOnly)
对于纯粹的查询操作,强烈建议加上 readOnly = true。这不仅能向数据库优化器传递信号,提升查询性能,还能防止在只读事务中意外执行写操作。
java
@Transactional(readOnly = true)
public List<User> listAllUsers() {
return userRepository.findAll();
}
3.@Transactional 的作用范围
通常会将 @Transactional 标注在需要控制事务的方法上,当一个 Service 中的方法几乎都需要事务控制时,可以直接将 @Transactional 标注在整个类 / 接口上,即代表当前类的所有 public 非静态方法都会添加事务控制。支持这样做的原因是 @Transactional 的可声明范围包括 METHOD 和 TYPE。

12.4.4 事务控制失效的场景
注解声明式事务控制的本质是基于 AOP 的逻辑增强,在具体使用事务时可能会遇到事务控制失效的情况,本节列出 Spring Boot 项目中几种常见的事务控制失效场景,读者在项目开发中一定要避免。
1.方法的可访问范围不是 public
@Transactional 必须标注在 public 修饰的方法上才可以生效,如果将注解标注在非 public 方法上,则调用该方法时不会有事务控制参与。
2.接口与实现类的动态代理
如果 @Transactional 标注在 Service 层的接口上,但实现类使用 Cglib 代理(在 @EnableAspectJAutoProxy 上设置 proxyTargetClass=true),则事务控制会失效,这是因为基于 Cglib 的动态代理基于类增强,而忽略类上实现的接口。
3.捕获异常类型的缺失
@Transactional 默认只会捕获 RuntimeException 和 Error 的子类,如果抛出了 Exception 类型的异常,@Transactional 默认不会捕获,事务控制也会因此失效。这就是在使用 @Transactional 时都要显式声明 rollbackFor=Exception.class 的原因。
4.手动捕获异常后未抛出
如果 Service 层的方法中使用 try-catch 结构捕获了异常但没有抛出,会导致事务拦截器无法感知到异常,从而导致事务控制出现错误。请读者一定明白,事务控制就是以方法运行时抛出的异常作为裁定依据,如果 Service 层将异常捕获后"吞掉",Spring Framework 会认为方法执行正确,从而错误地提交本应回滚的事务。
5.调用自身方法
同一个 Service 类中,如果一个方法使用 this 调用了自身另一个带有事务控制的方法,则也会导致事务控制失效。
造成这个现象的原因是 Service 代理对象在调用自身方法时,使用 this 获取的是原始对象而不是代理对象,这就造成使用 this 调用方法时无法触发 AOP 的增强效果。