在开发过程中会遇到事务失效的问题,所以在开发中要特别注意,下面我自己总结了事务不生效的场景,提醒自己。
一般出现问题分为几大类:
- 配置问题
- spring aop代理问题
- 底层数据库不支持事务问题
- @Transactional 配置错误
- 开发过程中使用错误问题
1.配置问题
1. 你的service类没有被Spring管理
//@Service
public class TransactionalUserServiceImpl implements TransactionalUserService {
@Resource
TransactionalUserMapper transactionalUserMapper;
@Override
@Transactional
public void transactionalError() throws Exception {
TransactionalUser transactionalUser = new TransactionalUser();
transactionalUser.setAsMsg("测试事务失效,修改成功");
transactionalUser.setId("1");
transactionalUserMapper.updateById(transactionalUser);
throw new Exception();
}
}
- 失败原因:@Service注解注释之后,spring事务(@Transactional)没有生效,因为Spring事务是由AOP机制实现的,也就是说从Spring IOC容器获取bean时,Spring会为目标类创建代理,来支持事务的。但是@Service被注释后,你的service类都不是spring管理的,所以无法创建代理。
- 解决方法:加上@service注解,但是一般不会犯这种错误。
2. 没有开启事务管理器
- 单独使用@Transactional,其实是不会生效的,因为它需要配置事务管理器开启事务。
- 解决方法:springboot项目启动类要加入:@EnableTransactionManagement ,当然新版本它默认会自动配置事务管理器并开启事务支持。
2.spring aop代理问题
1.方法被final、static关键字修饰
@Transactional
public final void add(Addreq req) {
//保存实体数据库记录
addMapper.save(req);
//保存流水数据库记录
addFlowMapper.saveFlow(buildFlowById(req));
}
- 原因 :如果一个方法被声明为final或者static,则该方法不能被子类重写,也就是说无法在该方法上进行动态代理,这会导致Spring无法生成事务代理对象来管理事务。
- 解决方法 :add事务方法 不要用final修饰或者static修饰。
2.同一个类中,方法内部调用
@Override
public void transactionalErrorV4() {
this.transactionalUser();
}
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void transactionalUser() {
TransactionalUser transactionalUser = new TransactionalUser();
transactionalUser.setAsMsg("测试事务失效");
transactionalUser.setId("2");
transactionalUserMapper.updateById(transactionalUser);
}
- 原因 : 事务是通过Spring AOP代理来实现的,而在同一个类中,一个方法调用另一个方法时,调用方法直接调用目标方法的代码,而不是通过代理类进行调用 。即以上代码,调用目标transactionalUser方法不是通过代理类进行的,因此事务不生效。
- 解决方法 :
- 可以新建多一个类,让这两个方法分开,分别在不同的类中。
- 也可以把主方法加上@Transactional,使他们用一个事务
3.方法的访问权限不是public
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
private void transactionalUser() {
TransactionalUser transactionalUser = new TransactionalUser();
transactionalUser.setAsMsg("测试事务失效");
transactionalUser.setId("2");
transactionalUserMapper.updateById(transactionalUser);
}
- 原因 :spring事务方法transactionalUser的访问权限不是public,所以事务就不生效啦,因为Spring事务是由AOP机制实现的,AOP机制的本质就是动态代理,而代理的事务方法不是public的话,computeTransactionAttribute()就会返回null,也就是这时事务属性不存在了。大家可以看下AbstractFallbackTransactionAttributeSource的源码
3.底层数据库不支持事务问题
Spring事务的底层,还是依赖于数据库本身的事务支持。在MySQL中,MyISAM存储引擎是不支持事务的,InnoDB引擎才支持事务。因此开发阶段设计表的时候,确认你的选择的存储引擎是支持事务的 。
4.@Transactional 配置错误
1.配置成只读事务
@Transactional(readOnly = true)
public void updateUser(User user) {
userDao.updateUser(user);
}
- 原因 :虽然使用了@Transactional注解,但是注解中的readOnly=true属性指示这是一个只读事务,因此在更新User实体时会抛出异常。
- 解决方法:将readOnly属性设置为false,或者移除了@Transactional注解中的readOnly属性。
2.事务超时时间设置过短
@Transactional(timeout = 1)
public void doSomething() {
}
- 原因 :在上面的例子中,timeout属性被设置为1秒,这意味着如果事务在1 秒内无法完成,则报事务超时了。
3.使用了错误的事务传播机制
//以非事务方式执行操作,如果当前存在一个事务,则将该事务挂起。
@Transactional(propagation = Propagation.NOT_SUPPORTED)
这里要科普下Spring提供了七种事务传播机制:
- REQUIRED(默认):如果当前存在一个事务,则加入该事务;否则,创建一个新事务。该传播级别表示方法必须在事务中执行。
- SUPPORTS:如果当前存在一个事务,则加入该事务;否则,以非事务的方式继续执行。
- MANDATORY:如果当前存在一个事务,则加入该事务;否则,抛出异常。
- REQUIRES_NEW:创建一个新的事务,并且如果存在一个事务,则将该事务挂起。
- NOT_SUPPORTED:以非事务方式执行操作,如果当前存在一个事务,则将该事务挂起。
- NEVER:以非事务方式执行操作,如果当前存在一个事务,则抛出异常。
- NESTED:如果当前存在一个事务,则在嵌套事务内执行。如果没有事务,则按REQUIRED传播级别执行。嵌套事务是外部事务的一部分,可以在外部事务提交或回滚时部分提交或回滚。
4.rollbackFor属性配置错误
@Override
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void transactionalErrorV6() {
TransactionalUser transactionalUser = new TransactionalUser();
transactionalUser.setAsMsg("测试事务失效");
transactionalUser.setId("3");
transactionalUserMapper.updateById(transactionalUser);
throw new RuntimeException();
}
- 原因 : 其实rollbackFor属性指定的异常必须是Throwable或者其子类。默认情况下,RuntimeException和Error两种异常都是会自动回滚的。但是因为以上的代码例子,指定了rollbackFor = Exception.class,但是抛出的异常又是RuntimeException,因此事务就不生效。
- 解决方法:要rollbackFor属性指定的异常与抛出的异常匹配。
5.使用不当
1.事务注解被覆盖导致事务失效
public interface A{
@Transactional
void save(String data);
}
public class AImpl implements A{
@Override
public void save(String data) {
// 数据库操作
}
}
public class BService {
@Autowired
private A A;
@Transactional
public void doSomething(String data) {
A.save(data);
}
}
public class CService extends BService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void doSomething(String data) {
super.doSomething(data);
}
}
- 原因 :
- CService 是 BService 的子类,并且覆盖了doSomething()方法。
- 在该方法中,使用了不同的传播行为(REQUIRES_NEW)来覆盖父类的@Transactional注解。
- 在这种情况下,当调用CService 的doSomething()方法时,由于子类方法中的注解覆盖了父类的注解,Spring框架将不会在父类的方法中启动事务 。
- 因此,当A类的save()方法被调用时,事务将不会被启动,也不会回滚。
- 这将导致数据不一致的问题,因为在A的save()方法中进行的数据库操作将不会回滚。
- 解决:一般不建议业务调用链路太深,出现问题也不好排查。
2.嵌套事务
在开发中我们会执行两个及其以上的保存操作,有时候我们不需要全部回滚,但是如果没有特殊处理,出现问题都会回滚。
@Service
public class AService {
@Autowired
private BService bService;
@Autowired
private AMapper aMapper ;
@Transactional
public void addOrder(Order order) throws Exception {
aMapper.save(order);
bService.saveFlow(order);
}
}
@Service
public class BService {
@Autowired
private BMapper bMapper;
@Transactional(propagation = Propagation.NESTED)
public void saveFlow(Order order) {
bMapper.save(order);
throw new RuntimeException();
}
}
- 原因:以上代码使用了嵌套事务,如果saveFlow出现运行时异常,会继续往上抛,到外层addOrder的方法,导致 aMapper.save(order);也会回滚。
- 解决方法:如果不想因为被内部嵌套的事务影响 ,可以用try-catch包住,如下:
@Transactional
public void addOrder(Order order) throws Exception {
aMapper.save(order);
try {
bService.saveFlow(order);
} catch (Exception e) {
log.error("savefail,message:{}",e.getMessage());
}
}
3.事务多线程调用
@Override
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void transactionalAsV11() {
new Thread(() -> {
try {
transactionalUser();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
public void transactionalUser() throws Exception {
TransactionalUser transactionalUser = new TransactionalUser();
transactionalUser.setAsMsg("测试事务失效");
transactionalUser.setId("2");
transactionalUserMapper.updateById(transactionalUser);
throw new Exception();
}
- 原因 :这是因为Spring事务是基于线程绑定的,每个线程都有自己的事务上下文 ,而多线程环境下可能会存在多个线程共享同一个事务上下文的情况,导致事务不生效。Spring事务管理器通过使用线程本地变量(ThreadLocal)来实现线程安全。
- 解决方法:通过TransactionAspectSupport 来手动设置回滚
public void transactionalUser() throws Exception {
//设置回滚点,只回滚以下异常
Object savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
try {
TransactionalUser transactionalUser = new TransactionalUser();
transactionalUser.setAsMsg("测试事务失效");
transactionalUser.setId("2");
transactionalUserMapper.updateById(transactionalUser);
throw new Exception();
} catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savePoint);
}
}
还有一种方式是使用springboot的线程注解: @Async
@Override
@Async("MyExecutor")
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = RuntimeException.class)
public void transactionalAsV12() throws InterruptedException {
Thread.sleep(10000);
TransactionalUser transactionalUser = new TransactionalUser();
transactionalUser.setAsMsg("测试事务失效");
transactionalUser.setId("2");
transactionalUserMapper.updateById(transactionalUser);
throw new RuntimeException();
}
上面写法事务是生效的,但是使用注解有几个注意点需要注意一下:
如下方式会使@Async失效
- 异步方法使用static修饰
- 异步类没有使用@Component注解(或其他注解)导致spring无法扫描到异步类
- 异步方法不能与被调用的异步方法在同一个类中
- 类中需要使用@Autowired或@Resource等注解自动注入,不能自己手动new对象
- 如果使用SpringBoot框架必须在启动类中增加@EnableAsync注解
4.异常没有抛出
@Transactional
public void addOrder(Order order) {
try {
aMapper.save(order);
bFlowMapper.saveFlow(order);
} catch (Exception e) {
log.error("add error,id:{},message:{}", order.getId(),e.getMessage());
}
}
- 原因 : 事务中的异常已经被业务代码捕获并处理,而没有被正确地传播回事务管理器,事务将无法回滚。我们可以从spring源码(TransactionAspectSupport这个类)中找到答案:
在invokeWithinTransaction方法中,当Spring catch到Throwable异常的时候,就会调用completeTransactionAfterThrowing()方法进行事务回滚的逻辑。但是,在addOrder 事务方法中,直接把异常catch住了,并没有重新throw出来,因此 Spring自然就catch不到异常,因此事务回滚的逻辑就不会执行,事务就失效了。
解决方法:抛出异常就好了
@Override
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = RuntimeException.class)
public void transactionalAsV13() {
try {
TransactionalUser transactionalUser = new TransactionalUser();
transactionalUser.setAsMsg("测试事务失效");
transactionalUser.setId("2");
transactionalUserMapper.updateById(transactionalUser);
throw new RuntimeException();
} catch (RuntimeException e) {
System.out.println("吞并异常");
throw new RuntimeException();
}
}