做了五年Java后端开发,我敢说事务是Spring Boot项目里最容易出问题的模块之一——要么是事务不生效,要么是回滚失败,最头疼的是线上出了问题才发现,数据都不一致了。我之前在做电商订单模块的时候,就踩过“同一个类里方法调用导致事务不生效”的坑,查了半天才搞懂Spring AOP的代理机制,差点影响了订单数据的一致性。
今天就结合我这些年的实战经验,把Spring Boot中使用事务的方式做个全面汇总,从最常用的@Transactional注解,到灵活的编程式事务,再到事务不生效的常见原因和优化方案,每一点都有实际代码和踩坑经历,没有空洞的理论,只有可直接复用的经验。
一、最常用:@Transactional注解式事务 @Transactional是Spring Boot中使用事务的主流方式,简单方便,只需在方法或类上加个注解,Spring就会自动帮我们管理事务的开启、提交和回滚。但这个注解看似简单,里面的坑却最多,先从它的核心属性讲起。
-
基本使用 在需要事务的方法或类上添加@Transactional注解即可:
@Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private ProductMapper productMapper; /** * 创建订单:扣减库存 + 插入订单 */ @Transactional(rollbackFor = Exception.class) public void createOrder(Order order) { // 1. 扣减商品库存 productMapper.decreaseStock(order.getProductId(), order.getQuantity()); // 2. 插入订单 orderMapper.insert(order); // 模拟异常,测试回滚 if (order.getQuantity() > 100) { throw new RuntimeException("库存不足"); } } }
如果在类上加@Transactional,类里所有public方法都会开启事务; 如果在方法上加,只对当前方法生效,方法级的优先级高于类级。 2. 核心属性详解(实战必看) @Transactional的属性很多,我只讲几个实战中最常用、最容易出问题的:
(1)rollbackFor:指定回滚异常 这是最容易踩坑的属性!默认情况下,Spring只对RuntimeException和Error回滚,对Exception(比如IOException、SQLException)不回滚。我之前就遇到过:
// 错误写法:抛出IOException,事务不会回滚
@Transactional
public void createOrder(Order order) throws IOException {
productMapper.decreaseStock(order.getProductId(), order.getQuantity());
orderMapper.insert(order);
throw new IOException("文件操作异常"); // 事务不回滚!
}
解决方法:显式指定rollbackFor = Exception.class,让所有异常都回滚:
// 正确写法
@Transactional(rollbackFor = Exception.class)
public void createOrder(Order order) throws IOException {
// ...
}
(2)propagation:事务传播行为 传播行为决定了“当一个事务方法被另一个事务方法调用时,事务该如何处理”,常用的有以下几种:
传播行为 含义 适用场景 Propagation.REQUIRED(默认) 如果当前有事务,就加入;如果没有,就新建一个事务 大多数场景,比如订单创建、库存扣减 Propagation.REQUIRES_NEW 不管当前有没有事务,都新建一个事务,挂起当前事务 需要独立事务的场景,比如记录操作日志(不管主事务是否成功,日志都要记录) Propagation.NESTED 如果当前有事务,就嵌套一个子事务;如果没有,就新建一个 嵌套事务场景,比如订单创建失败,只回滚订单,不回滚之前的库存预扣(需要数据库支持保存点) Propagation.SUPPORTS 如果当前有事务,就加入;如果没有,就以非事务方式运行 查询方法,可加可不加 Propagation.NOT_SUPPORTED 以非事务方式运行,挂起当前事务 不需要事务的耗时操作,比如文件上传 举个REQUIRES_NEW的例子:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private LogService logService;
@Transactional(rollbackFor = Exception.class)
public void createOrder(Order order) {
// 1. 扣减库存、插入订单(主事务)
productMapper.decreaseStock(order.getProductId(), order.getQuantity());
orderMapper.insert(order);
// 2. 记录操作日志(独立事务,不管主事务是否成功,日志都要记录)
logService.insertLog("创建订单:" + order.getId());
// 3. 模拟主事务失败
throw new RuntimeException("主事务失败");
}
}
@Service
public class LogService {
@Autowired
private LogMapper logMapper;
// REQUIRES_NEW:新建独立事务
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void insertLog(String content) {
logMapper.insert(content);
}
}
这个例子中,主事务失败回滚,但日志的独立事务已经提交,日志会正常记录,符合业务需求。
(3)isolation:事务隔离级别 隔离级别决定了“多个事务并发执行时,数据的一致性程度”,Spring默认使用数据库的默认隔离级别(MySQL默认是REPEATABLE_READ),常用的隔离级别:
隔离级别 含义 解决的问题
Isolation.READ_UNCOMMITTED 可以读取未提交的数据 无,会出现脏读
Isolation.READ_COMMITTED 只能读取已提交的数据 解决脏读,会出现不可重复读
Isolation.REPEATABLE_READ(MySQL默认) 同一个事务内多次读取同一数据,结果一致 解决脏读、不可重复读,会出现幻读
Isolation.SERIALIZABLE 串行化执行事务,最高隔离级别 解决所有问题,但性能最差
一般情况下,用数据库的默认隔离级别即可,不需要手动修改。
(4)readOnly:只读事务 如果方法只有查询操作,没有增删改,可以设置readOnly = true,优化数据库性能:
@Transactional(readOnly = true)
public Order getOrderById(Long id) {
return orderMapper.selectById(id);
}
只读事务会告诉数据库“这个事务不会修改数据”,数据库可以做一些优化,比如不加锁,提升查询速度。
(5)timeout:事务超时时间 设置事务的最大执行时间,超过时间自动回滚,避免事务长时间占用数据库连接:
// 超时时间30秒,单位秒
@Transactional(timeout = 30, rollbackFor = Exception.class)
public void createOrder(Order order) {
// ...
}
二、灵活可控:编程式事务 注解式事务虽然方便,但在某些复杂场景下不够灵活,比如需要手动控制事务的开启和提交、根据业务逻辑动态决定是否回滚,这时候就需要用编程式事务。Spring提供了两种编程式事务的方式:TransactionTemplate和PlatformTransactionManager。
-
TransactionTemplate(推荐) TransactionTemplate是Spring封装好的编程式事务工具类,使用起来比较简洁:
@Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private ProductMapper productMapper; @Autowired private TransactionTemplate transactionTemplate; public void createOrder(Order order) { // execute方法里的代码会在事务中执行 transactionTemplate.execute(status -> { try { // 1. 扣减库存 productMapper.decreaseStock(order.getProductId(), order.getQuantity()); // 2. 插入订单 orderMapper.insert(order); // 3. 模拟异常,手动回滚 if (order.getQuantity() > 100) { throw new RuntimeException("库存不足"); } return null; // 正常返回 } catch (Exception e) { // 手动回滚事务 status.setRollbackOnly(); throw e; // 重新抛出异常 } }); } }
execute方法的参数是TransactionCallback,里面的代码在事务中执行; 如果需要回滚,调用status.setRollbackOnly()即可。 2. PlatformTransactionManager(底层方式) PlatformTransactionManager是Spring事务的底层接口,需要手动管理事务的开启、提交和回滚,代码比较繁琐,但最灵活:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private ProductMapper productMapper;
@Autowired
private PlatformTransactionManager transactionManager;
public void createOrder(Order order) {
// 1. 定义事务属性
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
def.setTimeout(30);
// 2. 开启事务
TransactionStatus status = transactionManager.getTransaction(def);
try {
// 3. 执行业务逻辑
productMapper.decreaseStock(order.getProductId(), order.getQuantity());
orderMapper.insert(order);
if (order.getQuantity() > 100) {
throw new RuntimeException("库存不足");
}
// 4. 提交事务
transactionManager.commit(status);
} catch (Exception e) {
// 5. 回滚事务
transactionManager.rollback(status);
throw e;
}
}
}
3. 注解式 vs 编程式:怎么选? 对比项 注解式@Transactional 编程式TransactionTemplate 代码复杂度 简单,只需加注解 稍复杂,需要写模板代码 灵活性 一般,适合固定场景 高,适合复杂业务逻辑 可读性 好,事务逻辑一目了然 稍差,事务逻辑和业务逻辑混在一起 适用场景 大多数常规业务场景 需要手动控制事务边界、动态回滚的复杂场景 建议:优先用注解式,简单方便;只有在注解式搞不定的复杂场景下,再用编程式。
三、最容易踩坑:事务不生效的常见原因 事务不生效是Spring Boot项目里最常见的问题,我整理了5个最容易踩的坑,每个都有错误代码和正确代码,帮你快速排查。
-
方法不是public @Transactional只能作用于public方法,非public方法(private、protected、default)事务不会生效:
@Service public class OrderService { // 错误写法:private方法,事务不生效 @Transactional(rollbackFor = Exception.class) private void createOrder(Order order) { // ... } }
解决方法:把方法改成public。
-
同一个类中方法调用 这是我踩过的最坑的一个!Spring的@Transactional是基于AOP代理实现的,只有通过代理对象调用方法,事务才会生效;如果是同一个类里的方法直接调用(比如this.createOrder()),用的是原对象,不是代理对象,事务不会生效:
@Service public class OrderService { @Autowired private OrderMapper orderMapper; public void processOrder(Order order) { // 错误写法:同一个类里直接调用this.createOrder(),事务不生效 this.createOrder(order); } @Transactional(rollbackFor = Exception.class) public void createOrder(Order order) { orderMapper.insert(order); throw new RuntimeException("异常"); } }
解决方法:
方法1:把createOrder()放到另一个类里,通过依赖注入调用; 方法2:在当前类里注入自己,通过注入的对象调用: @Service public class OrderService {
@Autowired
private OrderMapper orderMapper;
// 注入自己
@Autowired
private OrderService orderService;
public void processOrder(Order order) {
// 正确写法:通过代理对象调用
orderService.createOrder(order);
}
@Transactional(rollbackFor = Exception.class)
public void createOrder(Order order) {
// ...
}
}
方法3:用AopContext.currentProxy()获取代理对象:
// 启动类上添加@EnableAspectJAutoProxy(exposeProxy = true)
@EnableAspectJAutoProxy(exposeProxy = true)
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// 调用时用代理对象 ((OrderService) AopContext.currentProxy()).createOrder(order);
-
异常被catch了,没有抛出 如果在方法里把异常catch了,没有重新抛出,Spring就不知道发生了异常,事务不会回滚:
@Service public class OrderService { @Transactional(rollbackFor = Exception.class) public void createOrder(Order order) { try { productMapper.decreaseStock(order.getProductId(), order.getQuantity()); orderMapper.insert(order); throw new RuntimeException("异常"); } catch (Exception e) { // 错误写法:只打印日志,没有抛出异常,事务不回滚 e.printStackTrace(); } } }
解决方法:catch后重新抛出异常,或者手动回滚:
// 方法1:重新抛出异常
@Transactional(rollbackFor = Exception.class)
public void createOrder(Order order) {
try {
// ...
} catch (Exception e) {
e.printStackTrace();
throw e; // 重新抛出
}
}
// 方法2:手动回滚(编程式事务)
@Autowired
private TransactionTemplate transactionTemplate;
public void createOrder(Order order) {
transactionTemplate.execute(status -> {
try {
// ...
} catch (Exception e) {
status.setRollbackOnly(); // 手动回滚
throw e;
}
});
}
4. 数据库引擎不支持事务 MySQL的MyISAM引擎不支持事务,只有InnoDB引擎支持,如果你的表是MyISAM引擎,事务不会生效:
-- 查看表的引擎
SHOW CREATE TABLE `order`;
-- 如果是MyISAM,改成InnoDB
ALTER TABLE `order` ENGINE = InnoDB;
5. rollbackFor配置不对 前面讲过,默认只对RuntimeException和Error回滚,如果抛出的是Exception,要显式指定rollbackFor = Exception.class,这个坑我就不重复写代码了,大家一定要注意。
四、生产级优化:让事务更稳、更快
-
减少事务范围 事务范围越大,占用数据库连接的时间越长,并发性能越差。要尽量把不需要事务的操作(比如查询、文件上传、网络请求)放到事务外面:
@Service public class OrderService {
@Autowired private OrderMapper orderMapper; @Autowired private ProductMapper productMapper; @Autowired private FileService fileService; // 错误写法:文件上传在事务里,占用连接时间长 @Transactional(rollbackFor = Exception.class) public void createOrder(Order order, MultipartFile file) { // 不需要事务的文件上传 fileService.upload(file); // 需要事务的业务逻辑 productMapper.decreaseStock(order.getProductId(), order.getQuantity()); orderMapper.insert(order); } // 正确写法:文件上传放到事务外面 public void createOrder(Order order, MultipartFile file) { // 1. 先做不需要事务的操作 fileService.upload(file); // 2. 再做需要事务的操作 doCreateOrder(order); } @Transactional(rollbackFor = Exception.class) public void doCreateOrder(Order order) { productMapper.decreaseStock(order.getProductId(), order.getQuantity()); orderMapper.insert(order); }}
-
避免在事务中做耗时操作 事务中不要做网络请求、文件IO、远程调用等耗时操作,这些操作会占用数据库连接,导致连接池耗尽:
// 错误写法:事务里有远程调用 @Transactional(rollbackFor = Exception.class) public void createOrder(Order order) { productMapper.decreaseStock(order.getProductId(), order.getQuantity()); orderMapper.insert(order); // 耗时的远程调用,占用数据库连接 remoteService.notifyWarehouse(order); } // 正确写法:远程调用放到事务外面,或者用异步 @Transactional(rollbackFor = Exception.class) public void createOrder(Order order) { productMapper.decreaseStock(order.getProductId(), order.getQuantity()); orderMapper.insert(order); } // 事务提交后,异步调用远程服务 @Async public void notifyWarehouse(Order order) { remoteService.notifyWarehouse(order); }
-
-
合理设置事务超时时间 根据业务逻辑的执行时间,合理设置timeout,避免事务长时间占用数据库连接,一般设置为30秒-60秒即可。
-
用只读事务优化查询 查询方法设置readOnly = true,让数据库做优化,提升查询速度。
五、总结 Spring Boot中使用事务,核心就是三点:
优先用注解式:@Transactional简单方便,注意配置rollbackFor = Exception.class和传播行为; 复杂场景用编程式:TransactionTemplate灵活可控,适合需要手动控制事务的场景; 避坑是关键:注意事务不生效的5个常见原因,尤其是同一个类中方法调用、异常被catch这两个坑; 优化不能少:减少事务范围、避免耗时操作、合理设置超时,提升事务性能。 事务是保证数据一致性的核心,一定要重视,不要等线上出了问题才来排查。希望这篇文章能帮你少走弯路,把事务用得更稳、更好。 ———————————————— 版权声明:本文为CSDN博主「Java程序员威哥」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:blog.csdn.net/2601_948715…