侧边栏壁纸
博主头像
七字节 博主等级

行动起来,活在当下

  • 累计撰写 11 篇文章
  • 累计创建 43 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

Spring事务失效的11种场景

七字节
2025-06-29 / 0 评论 / 0 点赞 / 0 阅读 / 0 字

Spring事务失效的11种场景

在Spring Boot中,使用事务只需要@Transactional​注解,但是在以下场景中,事务会失效。

一、事务不生效

1.访问权限问题

在Java中,访问权限有4种,public,protected,default,private,他们的访问权限,从左至右依次增大。

在Spring中,要求被代理的方法必须是public​的,如果不是,则代理方法不会生效,事务自然也不会生效。

2.方法用final或static修饰

在spring事务中,底层通过AOP动态代理帮我们生成代理类,在代理类中实现了事务,如果方法被final或static修饰,则spring无法对其生成代理方法,自然也就无法添加事务功能了。

3.方法内部调用

spring能使用事务的原因是Spring Aop生成了代理对象,在代理对象中实现的事务,而下面代码中add方法直接调用了updateStatus事务方法,这种在方法内直接调用实际调用的是this​对象的方法,不是代理对象,事务自然不会生效。

@Service
public class UserService {
 
    @Autowired
    private UserMapper userMapper;
 
  
    public void add(UserModel userModel) {
        userMapper.insertUser(userModel);
        updateStatus(userModel);
    }
 
    @Transactional
    public void updateStatus(UserModel userModel) {
        doSomeThing();
    }
}

遇到这个问题该如何解决呢?可以使用以下几种方法:

3.1将事务方法转移到其他代理对象里

可以新建一个@Service类,将事务方法转移到此类里。

@Servcie
public class ServiceA {
   @Autowired
   prvate ServiceB serviceB;
 
   public void save(User user) {
         queryData1();
         queryData2();
         serviceB.doSave(user);
   }
 }
 
 @Servcie
 public class ServiceB {
 
    @Transactional(rollbackFor=Exception.class)
    public void doSave(User user) {
       addData1();
       updateData2();
    }
 
 }

3.2自己注入自己

可以选择自己注入自己,然后使用注入的代理对象调用事务方法。

@Servcie
public class ServiceA {
   @Autowired
   prvate ServiceA serviceA;
 
   public void save(User user) {
         queryData1();
         queryData2();
         serviceA.doSave(user);
   }
 
   @Transactional(rollbackFor=Exception.class)
   public void doSave(User user) {
       addData1();
       updateData2();
    }
 }

虽然有Spring Ioc三层缓存机制保证了不会出现循环依赖的问题,但还是不建议用此方法。

3.3通过AopContext​获取代理对象

我们可以通过AopContext.currentProxy();​来获取当前对象的代理对象(要求当前对象被spring管理),再通过代理对象调用事务方法。

    @Override
    public Result seckillVoucher(Long voucherId) {
		//......代码块......
        //获取代理对象
        proxy = (IVoucherOrderService) AopContext.currentProxy();
		proxy.createVoucherOrder(voucherOrder);
		//......代码块......
        //返回订单id
        return Result.ok(orderId);
    }

4.类未被Spring管理

如果类含有@Service​,@Component​,@RestController​等注解,那这个类就被Spring管理了,Spring会生成此类的代理对象,就可以通过@Autowired​等方式注入代理对象,使用此代理对象进行事务处理,但是如果事务方法的类没有被Spring代理,那么事务自然也就不能生效了。

5.多线程调用

见下列代码:

@Slf4j
@Service
public class UserService {
 
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RoleService roleService;
 
    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        new Thread(() -> {
            roleService.doOtherThing();
        }).start();
    }
}
 
@Service
public class RoleService {
 
    @Transactional
    public void doOtherThing() {
        System.out.println("保存role表数据");
    }
}

在add方法里面新建线程调用方法roleService.doOtherThing() 方法,事务将会失效。为什么?因为Spring​的事务管理是通过ThreadLocal​实现的,新线程无法继承原线程的事务上下文。事务就会失效,即使doOtherThing​方法也有@Transactional​注解,但是也是一个新的事务,当此方法中出现异常,add​方法也不会回滚。

6.表不支持事务

在mysql5之前,默认的数据库引擎是myisam​,myisam​便不支持事务。如果你的表使用的是myisam​,那么就不能使用事务。不过在mysql5后使用的数据库引擎是innodb​,可以支持事务。

二、事务不回滚

1.错误的传播特性

在使用@Transactional​的时候,可以指定propagation​参数,此参数的作用是指定事务的传播特性。

Spring​现在支持7种不同的传播特性,分别是:

  • REQUIRED​ 如果当前上下文中存在事务,则加入该事务,如果不存在事务,则创建一个事务,这是默认的传播属性值。
  • SUPPORTS​ 如果当前上下文中存在事务,则支持事务加入事务,如果不存在事务,则使用非事务的方式执行。
  • MANDATORY​ 当前上下文中必须存在事务,否则抛出异常。
  • REQUIRES_NEW​ 每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
  • NOT_SUPPORTED​ 如果当前上下文中存在事务,则挂起当前事务,然后新的方法在没有事务的环境中执行。
  • NEVER​ 如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行代码。
  • NESTED​ 如果当前上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。

如果在使用@Transactional​的时候使用了错误的传播特性,就有可能事务失效或者不回滚,例如

@Transactional(propagation = Propagation.SUPPORTS)
public void updateBalance(User user, BigDecimal amount) {
    userRepository.reduceBalance(user, amount); // 扣款操作
    if (someCondition) {
        throw new RuntimeException("操作失败!");
    }
}
  • 问题:当 updateBalance​ 被非事务方法调用时,会以非事务执行。若抛出异常,扣款操作不会回滚,导致数据不一致。
  • 解决:若方法需要事务,应使用 REQUIRED​(默认值)。

2.未抛出异常

一个很常见的原因是开发者自己处理了异常,没有抛出异常,例如:

@Service
public class OrderService {
    @Autowired
    private OrderRepository orderRepository;

    // 事务注解:期望在异常时回滚订单创建操作
    @Transactional
    public void createOrder(Order order) {
        try {
            orderRepository.save(order); // 插入订单记录
            validateInventory(order);    // 校验库存(可能抛出异常)
        } catch (Exception e) {
            // 捕获异常但未重新抛出!
            log.error("创建订单失败", e);
        }
    }

    private void validateInventory(Order order) {
        // 模拟业务校验:库存不足时抛出运行时异常
        if (inventoryService.getStock(order.getProductId()) < order.getQuantity()) {
            throw new RuntimeException("库存不足!");
        }
    }
}

因为并未抛出异常,所以Spring认为程序是正常的,也就不会进行事务回滚。

3.抛出了别的异常

如果开发者抛出的异常不正确,Spring也不会进行回滚。例如:

@Slf4j
@Service
public class UserService {
    
    @Transactional
    public void add(UserModel userModel) throws Exception {
        try {
             saveData(userModel);
             updateData(userModel);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new Exception(e);
        }
    }
}

在catch代码块里,抛出了Exception​异常,此时Spring不会进行回滚,因为Spring事务默认情况下之后回滚RuntimeException​和Error​,而Exception​是不会进行回滚的。

4.自定义了回滚异常

我们可以自定义Spring事务的回滚异常,只需要设置rollbackFor​参数就可以了。例如:

@Slf4j
@Service
public class UserService {
    
    @Transactional(rollbackFor = MyException.class)
    public void add(UserModel userModel) throws Exception {
       saveData(userModel);
       updateData(userModel);
    }
}

如果在执行上面代码的时候程序报错了,例如报了SQLException​等异常,由于此异常不属于我们给rollbackFor​定义的异常,所以事务不会回滚。

建议将rollbackFor​设置成Exception​或Throwable​,这样可以避免因为抛出Exception​异常而导致程序出现不可知的BUG。

5.嵌套事务回滚过多

首先我们先了解什么是嵌套事务,嵌套事务是指在一个事务里创建子事务,核心是通过数据库的保存点(SavePoint)实现的,当子事务出现故障进行回滚时,只会回滚子事务的内容,而不会全部回滚,这样可以实现更细粒度的事务控制。

但是如果代码写的存在问题,则可能造成嵌套事务回滚多了,例如:

@Service
public class TransactionService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    // 外层事务方法
    @Transactional
    public void methodA() {
        // 操作1:插入日志
        jdbcTemplate.update("INSERT INTO user_log (action) VALUES ('操作1: methodA执行开始')");
        
        // 调用嵌套事务方法(未捕获异常)
        methodB(); 

        // 此处代码不会执行,因为methodB抛出的异常已传播到methodA
    }

    // 嵌套事务方法
    @Transactional(propagation = Propagation.NESTED)
    public void methodB() {
        // 操作B:插入日志
        jdbcTemplate.update("INSERT INTO user_log (action) VALUES ('操作B: methodB执行开始')");
        
        // 模拟子事务抛出异常
        throw new RuntimeException("methodB发生异常");
    }
}

我们本来的目的是若methodB方法出现异常,则只回滚methodB方法,但是因为methodB方法没有对异常进行try-catch处理,导致异常抛出到methodA方法,从而导致事务回滚次数多了。

0

评论区