Spring事务失效场景
用Spring
的@Transactional
注解控制事务有哪些不生效的场景?
不知道小伙伴们有没有这样的经历,在自己开心的编写业务代码时候,突然某一个方法里的事务好像失效了。然后debug
跟踪代码时发现,自己第一步的insert
或者update
的数据在语句执行完毕后,数据库中并没有立即出现更改或保存完的新数据。所以一度怀疑spring
的事务失效了。那么就来总结一下,造成“spring
事务失效”错觉的几个常见场景,然后对症下药。
一、事务不生效
1.1 数据库引擎不支持事务
这里以MySQL
为例,MyISAM
引擎是不支持事务操作的,InnoDB
才是支持事务的引擎,一般要支持事务都会使用InnoDB
。
根据MySQL
的官方文档从MySQL 5.5.5
开始的默认存储引擎是:InnoDB
,之前默认的都是:MyISAM
,所以这点要值得注意,底层引擎不支持事务再怎么搞都是白搭。
1.2 事务方法没有被Spring管理
如果事务方法所在的类没有加载到Spring IOC
容器中,也就是说,事务方法所在的类没有被Spring
管理,则Spring
事务会失效,示例如下:
// @Service
public class OrderServiceImpl implements OrderService {
@Transactional
public void updateOrder(Order order) {
// update order
}
}
如果此时把@Service
注解注释掉,这个类就不会被加载成一个Bean
,那这个类就不会被Spring
管理了,事务自然就失效了。
1.3 方法没有被public修饰
以下来自Spring
官方文档:
When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods.
大概意思就是@Transactional
只能用于public
的方法上,否则事务不会失效,如果要用在非public
方法上,可以开启AspectJ
代理模式。
@Service
public class OrderServiceImpl implements OrderService {
@Transactional
private void updateOrder(Order order) {
// update order
}
}
虽然OrderServiceImpl
上标注了@Service
注解,同时updateOrder()
方法上标注了@Transactional
注解。但是,由于updateOrder()
方法为内部的私有方法(使用private
修饰),那么此时updateOrder()
方法的事务在Spring
中会失效。
1.4 方法用final修饰
有时候,某个方法不想被子类重新,这时可以将该方法定义成final
的。普通方法这样定义是没问题的,但如果将事务方法定义成final
,例如:
@Service
public class OrderServiceImpl implements OrderService {
@Transactional
public final void update(Order order) {
// update order
}
}
可以看到update
方法被定义成了final
的,这样会导致事务失效。
如果看过spring
事务的源码,可能会知道spring
事务底层使用了aop
,也就是通过jdk
动态代理或者cglib
,生成了代理类,在代理类中实现的事务功能。
但如果某个方法用final
修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。
注意:如果某个方法是static的,同样无法通过动态代理变成事务方法。
1.5 同一类中方法调用(重要)
如果同一个类中的两个方法分别为A
和B
,方法A
上没有添加事务注解,方法B
上添加了@Transactional
事务注解,方法A
调用方法B
,则方法B
的事务会失效。例如,来看两个示例:
//示例1
@Service
public class OrderServiceImpl implements OrderService {
public void update(Order order) {
updateOrder(order);
}
@Transactional
public void updateOrder(Order order) {
// update order
}
}
//示例2
@Service
public class OrderServiceImpl implements OrderService {
@Transactional
public void update(Order order) {
updateOrder(order);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateOrder(Order order) {
// update order
}
}
示例1和示例2中,区别是update
方法上面有@Transactional
注解,然后调用有@Transactional
注解的updateOrder
方法,updateOrder
方法上的事务管用吗?
这两个例子的答案是:都不管用!
因为它们发生了自身调用,就调该类自己的方法,而没有经过Spring
的代理类,默认只有在外部调用事务才会生效,这也是老生常谈的经典问题了。
spring
框架是通过TransactionInterceptor
类来控制事务开启、提交、回滚等, 它会创建一个目标类的代理类。显然update
方法调用updateOrder
方法时,并不是通过代理类去调用,而是通过this
调用updateOrder
方法,所以update
方法的事务并不会开启。
解决方案:
- 新增
service
,把updateOrder
移动新的service
,然后注入再调用。 - 在自己类中注入自己,用注入的对象再调用另外一个方法(需要开启循环依赖)。
- 通过
ApplicationContextUtil
(推荐)获取到当前代理类或AopContext
创建代理,可以参考《Spring如何在一个事务中开启另一个事务?》。
@Service
public class OrderServiceImpl implements OrderService {
public void update(Order order) {
OrderServiceImpl orderServiceImpl = ApplicationContextUtil.getBean(OrderServiceImpl.class);
if (null != orderServiceImpl) {
orderServiceImpl.updateOrder(order);
}
}
@Transactional(rollbackFor = Exception.class)
public void updateOrder(Order order) {
// update order
}
}
ApplicationContextUtil
上下文工具类如下
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* spring 上下文工具类
* @author xiaer
*/
@Component
public class ApplicationContextUtil implements ApplicationContextAware {
/**
* 上下文对象实例
*/
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
ApplicationContextUtil.applicationContext = applicationContext;
}
/**
* 获取applicationContext
*
* @return
*/
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
/**
* 通过name获取 Bean.
*
* @param name
* @return
*/
public static Object getBean(String name) {
return getApplicationContext().getBean(name);
}
/**
* 通过class获取Bean.
*
* @param clazz
* @param <T>
* @return
*/
public static <T> T getBean(Class<T> clazz) {
return getApplicationContext().getBean(clazz);
}
/**
* 通过name以及Clazz返回指定的Bean
*
* @param name
* @param clazz
* @param <T>
* @return
*/
public static <T> T getBean(String name, Class<T> clazz) {
return getApplicationContext().getBean(name, clazz);
}
}
1.6 没有配置事务管理器
如果在项目中没有配置Spring
的事务管理器,即使使用了Spring
的事务管理功能,Spring
的事务也不会生效。
如下代码所示,当前数据源若没有配置事务管理器,那也是白搭!
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
此时,Spring
的事务就会失效。
1.7 多线程调用
在实际项目开发中,多线程的使用场景还是挺多的。如果spring
事务用在多线程场景中,会有问题吗?
@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
中,调用了事务方法doOtherThing
,但是事务方法doOtherThing
是在另外一个线程中调用的。
这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想doOtherThing
方法中抛了异常,add
方法也回滚是不可能的。
如果看过spring
事务源码,可能会知道spring
的事务是通过数据库连接来实现的。当前线程中保存了一个map
,key
是数据源,value
是数据库连接。
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。
1.8 事务扩展配置不支持
Propagation.NOT_SUPPORTED
:表示不以事务运行,当前若存在事务则挂起。这表示不支持以事务的方式运行,所以即使事务生效也是白搭!
@Service
public class OrderServiceImpl implements OrderService {
@Transactional
public void update(Order order) {
updateOrder(order);
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void updateOrder(Order order) {
// update order
}
}
由于updateOrder()
方法的事务传播类型为NOT_SUPPORTED
,不支持事务,则updateOrder()
方法的事务会在Spring
中失效。
二、事务不回滚
2.1 不正确的捕获异常
这个也是出现比较多的场景:把异常吃了,然后又不抛出来,事务也不会回滚!
@Service
public class OrderServiceImpl implements OrderService {
@Transactional
public void updateOrder(Order order) {
try {
// update order
} catch(Exception e) {
}
}
}
updateOrder()
方法中使用try-catch
代码块捕获了异常,即使updateOrder()
方法内部会抛出异常,但也会被catch
代码块捕获到,此时updateOrder()
方法的事务会提交而不会回滚,这就造成了Spring
事务的回滚失效问题。
2.2 异常类型错误
如果在@Transactional
注解中标注了错误的异常类型,则Spring
事务的回滚会失效,接上面的例子,再抛出一个异常。
@Service
public class OrderServiceImpl implements OrderService {
@Transactional
public void updateOrder(Order order) {
try {
// update order
} catch {
throw new Exception("更新错误");
}
}
}
这样事务也是不生效的,因为默认回滚的是:RuntimeException
,如果你想触发其他异常的回滚,需要在注解上配置一下,如:
@Transactional(rollbackFor = Exception.class)
这个配置仅限于Throwable
异常类及其子类。
2.3 自定义回滚异常
在使用@Transactional
注解声明事务时,有时想自定义回滚的异常,spring
也是支持的。可以通过设置rollbackFor
参数,来完成这个功能。
但如果这个参数的值设置错了,就会引出一些莫名其妙的问题,例如:
@Service
public class OrderServiceImpl implements OrderService {
@Transactional(rollbackFor = BusinessException.class)
public void updateOrder(Order order) throws Exception {
// update order
}
}
如果在执行上面这段代码,更新数据时,程序报错了,抛了SqlException
、DuplicateKeyException
等异常。而BusinessException
是自定义的异常,报错的异常不属于BusinessException
,所以事务也不会回滚。
即使rollbackFor
有默认值,但《阿里巴巴开发者规范》中,还是要求开发者重新指定该参数。这是为什么呢?
因为如果使用默认值,一旦程序抛出了Exception
,事务不会回滚,这会出现很大的bug
。所以,建议一般情况下,将该参数设置成:Exception
或Throwable
。
总结
本文总结了事务失效的场景,其实发生最多就是同一类中方法调用、不正确的捕获异常、异常类型错误这3个了