背景
笔者最初使用spring事务时,主要通过@Transactional注解来管理事务,由于对spring事务原理了解不够清楚,一次程序出现异常时发现事务没有生效,场景是同一个类中被@Transactional修饰的方法A调用同样被@Transactional修饰的方法B时发现B中事务未生效,这才回过头学习了下spring事务管理的原理,找到了事务失效的原因:Spring的事务本质上是个代理类,而本类方法直接调用时其对象本身并不是织入事务的代理,所以事务切面并未生效
使用spring声明式事务时,除了同一类调用本身会使事务失效外,还有一些其他场景也容易掉入事务失效或者事务传播失效的坑里,这里简单做下总结:
1、数据库引擎不支持事务
常用的 oracle 直接就是支持事务的,而 mysql 的 innodb 支持事务,myIsam 的话,是不支持事务的。在 mysql5.1 版本之前,默认引擎是 myIsam ,而之后的版本则默认就是innodb 了~
建议检查项:mysql的数据库引擎
执行命令
show variables like ‘%storage_engine%’;
2、当前实体没有被spring管理
//@Service
public class UserService {
@Autowired
private UserDao userDao;
@Transactional
public void add(User user) {
userDao.save(user);
}
}
如果我们的类没有使用service注解,或者说没有交给spring管理bean实例,那么它的add方法也不会生成事务。
3、spring事务传播特性使用不对
@Service
public class UserService {
@Autowired
private UserDao userDao;
@Transactional(propagation = Propagation.NEVER)
public void add(User user) {
userDao.save(user);
}
}
如果add方法的事务传播特性定义成了Propagation.NEVER,这种类型的传播特性不支持事务,如果有事务则会抛异常。只有这三种传播特性才会创建新事务:PROPAGATION_REQUIRED,PROPAGATION_REQUIRES_NEW,PROPAGATION_NESTED,Spring默认使用的PROPAGATION_REQUIRED是天然支持事务的。
4、自己对异常做了捕捉且没有对外抛出
@Log4j2
@Service
public class UserService {
@Autowired
private UserDao userDao;
@Transactional
public void add(User user) {
userDao.save(user);
try {
userDao.save(user);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
如果我们在事务方法中手动try,catch住了异常,并且没有往外抛出,那么方法发生异常时事务不会回滚,只有把异常抛出,Spring事务的AOP捕获到异常,才能使事务得到回滚。
5、自己对异常进行捕捉并抛出,但是抛出的异常类型不对
@Log4j2
@Service
public class UserService {
@Autowired
private UserDao userDao;
@Transactional
public void add(User user) throws Exception {
try {
userDao.save(user);;
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new Exception(e);
}
}
}
相对上一种情况,咱们把异常往外抛出了,是不是Spring事务管理器就能处理异常并回滚呢?不一定!Spring的事务管理默认是针对unchecked exception回滚,也就是默认对Error异常和RuntimeException异常以及其子类进行事务回滚,上面代码抛出了Exception,Srping事务管理器并不会对该异常进行事务回滚,当然,抛出自定义异常也不会回滚了。
通常自己捕捉异常需要回滚时y有两种方式:
1、可以在catch中显式的调用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() 语句 进行手动回滚,
2、在事务注解中添加参数,eg: @Transactional(rollbackFor=MyException.class)
6、多线程调用
@Log4j2
@Service
public class UserService {
@Autowired
private UserDao userDao;
@Autowired
private RoleService roleService;
@Transactional
public void add(User user) throws Exception {
userDao.save(user);
new Thread(() -> {
roleService.save();
}).start();
}
}
@Service
public class RoleService {
@Transactional
public void save() {
System.out.println("插入一条角色信息");
}
}
在事务方法add中,调用了RoleService的事务方法save,但是save是在另外一个线程中调用的,这样会导致两个事务方法不在同一个线程中,获取到的数据库连接不一样,那么一定是在两个不同的事务中,那么为什么不在同一个线程中,获取到的数据库连接不一样?
因为spring的事务是通过数据库连接来实现的,每个线程中保存了一个map,key是数据源,value是数据库连接
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
通常我们说的事务,都是限定在同一个数据库连接下的,只有拥有同一个数据库连接才能同时提交和回滚,如果是不同的线程使用事务,拿到的数据库连接肯定是不一样的,所以可以说是不同的事务。
7、使用多层事务时结果不符合预期
public class UserService {
@Autowired
private UserDao userDao;
@Autowired
private RoleService roleService;
@Transactional
public void add(User user) throws Exception {
userDao.save(user);
roleService.save();
}
}
@Service
public class RoleService {
@Transactional(propagation = Propagation.NESTED)
public void save() {
System.out.println("插入一条角色信息");
}
}
以上代码使用了多层嵌套的内部事务,内层事务传播属性设置为NESTED,目的是调用roleService.save方法时,如果出现了异常,只回滚roleService.save方法里的内容,不回滚 userDao.save里的内容,但实际结果是userDao.save也回滚了。
原因是roleService.save方法出现了异常,没有手动捕获,会继续往上抛,到外层add方法的代理方法中捕获了异常。所以,这种情况是直接回滚了整个事务,不只回滚内层事务。
那应该怎么实现预期效果?
@Log4j2
@Service
public class UserService {
@Autowired
private UserDao userDao;
@Autowired
private RoleService roleService;
@Transactional
public void add(User user) throws Exception {
userDao.save(user);
try {
roleService.save();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
这里我们把内层事务包在了try,catch中,对内部事务的异常进行捕捉,这样就不影响外部事务了。