SpringBooot之事务失效的场景
在Spring Boot中,你可以通过使用@Transactional
注解来给方法增加事务支持,但是有时候使用不当会导致事务失败,会产生数据无法一致性这种严重的数据问题,可能出现的场景如下。
一、实例背景介绍
1、数据库数据表sys_user中description字段“要求非空”,但是我们新增时候传null,继而导致保存失败,事务被触发导致批量数据会全部会被回滚。
2、Springboot中使用事务需要在启动类中开启事务@EnableTransactionManagement和在方法或者类上使用注解@Transactional
二、数据库引擎不支持事务
这里以 MySQL 为例,其 MyISAM 引擎是不支持事务操作的,InnoDB 才是支持事务的引擎,一般要支持事务都会使用 InnoDB。
从 MySQL 5.5.5 开始的默认存储引擎是:InnoDB,之前默认的都是:MyISAM,所以这点要值得注意,底层引擎不支持事务再怎么操作都是无济于事。
三、没有被 Spring 管理
// @Service
public class OrderServiceImpl implements OrderService {
@Transactional
public void updateOrder(Order order) {
// update order
}
}
上方例子中,@Service
注解被注释了。这个类就不会被加载成一个 Bean,那这个类就不会被 Spring 管理了,事务自然就失效了。
四、事务方法非public修饰
由于Spring的事务是基于AOP的方式结合动态代理来实现的。因此事务方法一定要是public的,这样才能便于被Spring做事务的代理和增强。
而且,在Spring内部也会有一个 org.springframework.transaction.interceptor.AbstractFallbackTransactionAttributeSource类,去检查事务方法的修饰符。
@ApiOperation("添加")
@PostMapping("/add")
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
protected void addUser(@RequestBody User user) {
user.setUserName("火箭");
user.setDescription("Rocket");
iUserService.save(user);
user.setUserName("巫师");
user.setDescription(null);
iUserService.save(user);
}
五、非事务方法调用事务方法
Spring会给带有@Transactional注解类生成一个动态代理对象,对方法做增加实现事务效果。
非事务方法直接调用,就会导致对象无法被Spring代理对象获取到,继而导致事务失败。
@ApiOperation("添加")
@PostMapping("/add")
//@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void addUser(@RequestBody User user) {
this.saveUser(user);
}
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void saveUser(User user) {
user.setUserName("火箭");
user.setDescription("Rocket");
iUserService.save(user);
user.setUserName("巫师");
user.setDescription(null);
iUserService.save(user);
}
六、事务方法的异常被捕获
而Spring的事务管理就是要感知业务方法的异常,当捕获到异常后才会回滚事务,
现在异常被捕获,且没有抛出,相当于异常被程序员私下处理掉了,就会导致Spring无法感知事务异常,自然不会回滚,事务就失效了。
@ApiOperation("添加")
@PostMapping("/add")
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void addUser(@RequestBody User user) {
try{
user.setUserName("火箭");
user.setDescription("Rocket");
iUserService.save(user);
user.setUserName("巫师");
user.setDescription(null);
iUserService.save(user);
}catch (Exception e){
e.printStackTrace();
}
}
七、事务异常类型不对
Spring的事务管理默认感知的异常类型是RuntimeException
,当事务方法内部抛出了一个IOException
时,不会被Spring捕获,因此就不会触发事务回滚,事务就失效了。
1、异常被处理,没有正确抛出
@Service
public class OrderServiceImpl implements OrderService {
@Transactional
public void updateOrder(Order order) {
try {
// update order
} catch {
}
}
}
try..catch..
捕获了一场,然而catch
中还没有将异常抛出,所以springboot捕获不到异常,就无法进行回滚!
2、异常抛出类型不对
@Service
public class OrderServiceImpl implements OrderService {
@Transactional
public void updateOrder(Order order) {
try {
// update order
} catch {
throw new Exception("更新错误");
}
}
}
try..catch..
虽然捕获了异常且跑出了Exception
异常,但是springboot只有捕获到RuntimeException
异常时才会触发回滚。
八、事务传播行为设置不对
1、因为事务之间的传播会相互影响,一定要慎用传播行为,注意外部事务与内部事务之间的关系。
@Transactional
public void createOrder(){
// 生成订单
insertOrder();
// 扣减库存
reduceStock();
throw new RuntimeException("业务异常");
}
@Transactional // 默认的是如果当前没有事务,自己创建事务,如果有事务则加入
public void insertOrder() {
}
// 不管当前方法所在方法有没有都开启一个事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void reduceStock() {}
在示例代码中,事务的入口是createOrder()
方法,会开启一个事务,可以成为外部事务。在createOrder()方法内部又调用了insertOrder()
方法和reduceStock()
方法。这两个都是事务方法。
不过,reduceStock()
方法的事务传播行为是REQUIRES_NEW
,这会导致在进入reduceStock()
方法时会创建一个新的事务,可以成为子事务。insertOrder()
则是默认,因此会与createOrder()
合并事务。
因此,当createOrder
方法最后抛出异常时,只会导致insertOrder
方法回滚,而不会导致reduceStock
方法回滚,因为reduceStock
是一个独立事务。
2、 Propagation.NOT_SUPPORTED
: 表示不以事务运行,当前若存在事务则挂起。这是主动配置了事务不生效,所以事务就不存在。
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void updateOrder(Order order) {
// update order
}