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方法,从而导致事务回滚次数多了。
评论区