避坑指南:为什么你的@Transactional突然不工作了?

日常开发中,我们绝大多数情况下都是使用 @Transactional 注解来开启和管理事务的。它通过声明式的方式,极大地简化了事务编程,让我们能从繁琐的 try-catch-finally 和手动 commit/rollback 中解放出来,专注于业务逻辑。这正是Spring AOP(面向切面编程)的魅力所在。

然而,正是因为 @Transactional 如此方便,我们有时会忽略其背后的工作原理,从而在不经意间踩到一些“坑”,导致事务“悄无声息”地失效了。

今天,我们来聊一个老生常谈但又总有人踩坑的话题——Spring的 @Transactional 事务。

@Transactional 注解就像一把瑞士军刀,轻巧、方便、功能强大。我们只需要在方法上轻轻一标,就能享受到事务带来的数据一致性保障。但你是否遇到过这样的场景:明明加了注解,代码也看似没问题,可数据库里的数据却“我行我素”,事务压根没生效?

别慌,这通常不是Spring的Bug,而是我们不小心绕过了它的“游戏规则”。下面,我们就来盘点一下导致事务失效的五大元凶,帮你彻底搞懂它,告别踩坑!

背后原理速览:AOP代理

在开始之前,我们必须先理解 @Transactional 的核心魔法:AOP代理

当你为一个Bean(通常是 @Service 注解的类)的public方法标注了 @Transactional 后,Spring并不会直接把这个Bean给你,而是会为它创建一个代理对象。当你调用这个方法时,实际上是调用了代理对象的方法。这个代理对象就像一个“保安”,它会在你的业务方法执行前,开启事务;在方法执行后,根据执行情况(是否抛出异常)来决定是提交(Commit)还是回滚(Rollback)事务。

核心:事务的生效与否,关键在于你是否通过代理对象调用了方法。 记住这一点,我们就能轻松理解下面的所有失效场景。


元凶一:方法内部调用(自调用)

这是最常见也最隐蔽的失效场景。

场景描述: 在同一个类中,一个没有事务注解的方法A,调用了另一个有事务注解的方法B。

失效原因: 当你调用方法A时,你操作的是真实的this对象,而不是Spring的代理对象。因此,this.方法B() 的调用,本质上是对象内部的普通方法调用,完全绕过了代理对象的“保安”,AOP切面自然无法介入,事务也就失效了。

错误示例:

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
@Service
public class OrderServiceImpl implements OrderService {

@Autowired
private OrderMapper orderMapper;

// 外部调用这个方法,它没有事务注解
public void createOrder(Order order) {
// ... 一些前置处理 ...
System.out.println("准备创建订单...");

// 内部通过 this 调用了有事务的方法
// 这是导致事务失效的罪魁祸首!
this.insertOrder(order);
}

@Transactional
public void insertOrder(Order order) {
orderMapper.insert(order);
// 假设这里抛出运行时异常,我们期望它回滚
if (order.getAmount() > 1000) {
throw new RuntimeException("订单金额过大,模拟异常!");
}
}
}

在上面的例子中,即使 insertOrder 执行时抛出异常,数据库中的订单数据也不会回滚

解决方案:

让调用方从 this 变成 代理对象

  1. 【推荐】注入自己,通过代理对象调用:
    在类中注入自身的代理对象,然后用这个代理对象去调用事务方法。

    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
    @Service
    public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderMapper orderMapper;

    // 注入自己的代理对象
    // @Lazy可以解决循环依赖问题
    @Autowired
    @Lazy
    private OrderService self;

    public void createOrder(Order order) {
    System.out.println("准备创建订单...");
    // 使用代理对象调用,事务生效!
    self.insertOrder(order);
    // 或者 ((OrderService)AopContext.currentProxy()).insertOrder(order);
    // 后者需要额外配置 @EnableAspectJAutoProxy(exposeProxy = true)
    }

    @Transactional
    public void insertOrder(Order order) {
    orderMapper.insert(order);
    if (order.getAmount() > 1000) {
    throw new RuntimeException("订单金额过大,模拟异常!");
    }
    }
    }
  2. 【更推荐】重构代码,拆分到不同的Service:
    这是更符合单一职责原则的做法。将需要事务控制的方法拆分到另一个Service中,通过注入该Service来调用。

    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
    // OrderCreatorService.java
    @Service
    public class OrderCreatorService {
    @Autowired
    private OrderMapper orderMapper;

    @Transactional
    public void insertOrder(Order order) {
    orderMapper.insert(order);
    if (order.getAmount() > 1000) {
    throw new RuntimeException("订单金额过大,模拟异常!");
    }
    }
    }

    // OrderServiceImpl.java
    @Service
    public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderCreatorService orderCreatorService;

    public void createOrder(Order order) {
    System.out.println("准备创建订单...");
    // 通过注入的Bean调用,事务生效!
    orderCreatorService.insertOrder(order);
    }
    }

元凶二:方法访问权限不是public

场景描述:@Transactional 注解加在了 privateprotecteddefault 权限的方法上。

失效原因: Spring AOP的默认实现(无论是基于JDK动态代理还是CGLIB)都要求被代理的方法是 public 的。对于非 public 方法,AOP无法进行拦截,也就无法织入事务逻辑。

错误示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class UserService {
@Autowired
private UserMapper userMapper;

// private方法,@Transactional 将会失效!
@Transactional
private void registerUser(User user) {
userMapper.insert(user);
throw new RuntimeException("模拟异常");
}

// public方法,作为入口
public void processRegistration(User user) {
this.registerUser(user); // 即使这里用代理对象调用,registerUser本身也无法被代理
}
}

解决方案: 非常简单,将方法的访问权限修改为 public


元凶三:数据库引擎不支持事务

场景描述: 项目底层使用的数据库存储引擎本身就不支持事务。

失效原因: Spring的事务管理是建立在数据库本身支持事务的基础之上的。如果数据库引擎都不支持,Spring再怎么努力也是“巧妇难为无米之炊”。最典型的例子就是MySQL的 MyISAM 引擎。

检查与解决方案:

  1. 检查你的数据库表所使用的引擎。在MySQL中,可以使用 SHOW TABLE STATUS LIKE 'your_table_name'; 查看。
  2. 确保使用支持事务的引擎,如 InnoDB
  3. 如果使用的是旧表,可以通过 ALTER TABLE your_table_name ENGINE=InnoDB; 来修改。

元凶四:Bean没有被Spring容器管理

场景描述: 在一个没有被 @Component@Service 等注解标记的类中使用了 @Transactional

失效原因: @Transactional 的生效前提是,这个类的实例(Bean)必须是由Spring容器创建和管理的。只有这样,Spring才能为它创建代理对象。如果你自己 new 了一个对象,Spring对它一无所知,自然无法提供事务管理能力。

错误示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ManualService { // 注意:没有 @Service 或 @Component 注解

private UserMapper userMapper; // 假设通过某种方式手动注入了

@Transactional // 这个注解完全是“自娱自乐”,不会有任何效果
public void doSomething() {
// ...
throw new RuntimeException("模拟异常");
}
}

// 在某个Controller中
@RestController
public class MyController {

@GetMapping("/test")
public void test() {
// 手动new对象,Spring无法管理它
ManualService manualService = new ManualService();
manualService.doSomething(); // 调用此方法,事务100%不生效
}
}

解决方案:
将这个类交给Spring管理,添加 @Service@Component 等注解,并通过 @Autowired 注入使用。


元凶五:异常被“吃掉”或异常类型不匹配

场景描述:

  1. 在事务方法内部使用了 try-catch 块,捕获了异常但没有重新抛出。
  2. 抛出的异常类型不被Spring默认的回滚策略覆盖。

失效原因:
Spring判断事务是否回滚的默认依据是:方法在执行过程中,是否抛出了 RuntimeExceptionError

  • 如果你用 try-catch 捕获了异常并没有再往外抛,那么在Spring看来,这个方法是“正常执行”完成的,它自然会选择提交事务。
  • 如果你抛出的是一个受检异常(Checked Exception,如 IOException 或自定义的 Exception),Spring默认不会回滚事务。

错误示例1:异常被“吃掉”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class ProductService {
@Autowired
private ProductMapper productMapper;

@Transactional
public void updateStock(Long productId) {
try {
productMapper.deductStock(productId); // 减库存
// 模拟发生异常
if (true) {
throw new RuntimeException("网络波动,更新失败!");
}
} catch (Exception e) {
// 异常被捕获了,但没有抛出去,事务会认为一切正常并提交!
System.out.println("发生了一个小问题,但我们已经处理了:" + e.getMessage());
}
}
}

错误示例2:异常类型不匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class ProductService {
@Autowired
private ProductMapper productMapper;

// 默认只对RuntimeException回滚,MyCustomException是Exception子类,不会回滚
@Transactional
public void updateStock(Long productId) throws MyCustomException {
productMapper.deductStock(productId);
if (true) {
throw new MyCustomException("自定义的业务异常!");
}
}
}

class MyCustomException extends Exception { // 注意,它继承自Exception
public MyCustomException(String message) {
super(message);
}
}

解决方案:

  1. 对于被“吃掉”的异常:catch 块中处理完必要逻辑后,将异常重新抛出。throw new RuntimeException(e);

  2. 对于异常类型不匹配:

    • 方法一(推荐): 在业务代码中尽量使用或封装为 RuntimeException
    • 方法二(明确指定):@Transactional 注解中通过 rollbackFor 属性,明确指定需要回滚的异常类型。
    1
    2
    3
    4
    5
    6
    // 明确告诉Spring,遇到任何Exception都给我回滚!
    @Transactional(rollbackFor = Exception.class)
    public void updateStock(Long productId) throws MyCustomException {
    // ...
    throw new MyCustomException("自定义的业务异常!");
    }

总结

让我们再次回顾这五大“元凶”:

  1. 方法自调用this调用绕过了代理。
  2. public方法:AOP无法拦截。
  3. 数据库不支持:底层基础不具备。
  4. 非Spring Bean:对象未被Spring管理。
  5. 异常处理不当:异常被“吃掉”或类型不匹配。

掌握了这些,相信你对Spring事务的理解又上了一个台阶。记住,技术用得爽,原理不能忘。理解了AOP代理这个核心,很多问题都会迎刃而解。

希望这篇博客能帮你扫清知识盲区,在未来的开发中,让 @Transactional 成为你手中真正稳定可靠的神器!