ToB企服应用市场:ToB评测及商务社交产业平台
标题:
Spring事务(二)-@Transactional事务失效的场景
[打印本页]
作者:
我可以不吃啊
时间:
2022-9-17 23:31
标题:
Spring事务(二)-@Transactional事务失效的场景
有时候,我们明明在类或者方法上添加了@Transactional注解,却发现方法并没有按事务处理。其实,以下场景会导致Spring的@Transactional事务失效。
1、事务方法所在的类没有加载到Spring IOC容器中。
@Transactional是Spring的注解,未被Spring管理的类中的方法不受@Transactional注解控制,这个应该很好理解。
2、方法没有被public修饰。
众所周知,java的访问权限修饰符有:private、default、protected、public四种,但是@Transactional注解只能作用于public修饰的方法上。之所以会失效是因为在Spring AOP 代理时,TransactionInterceptor (事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute 方法,获取Transactional 注解的事务配置信息。
protected TransactionAttribute computeTransactionAttribute(Method method,
Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
复制代码
此方法会检查目标方法的修饰符是否为 public,不是 public则不会获取@Transactional 的属性配置信息。
注意:protected、private 修饰的方法上使用 @Transactional 注解,虽然事务无效,但不会有任何报错,这是我们很容犯错的一点。
3、在同一个类中的方法调用。
假如在同一个类中有A、B两个方法,如下:
@Service
public class UserServiceImpl {
@Autowired
UserMapper userMapper;
public void A() {
B();
}
@Transactional
public void B() {
userMapper.deleteById(1);
int i = 10 / 0; //模拟发生异常
}
}
复制代码
像上面的代码,B方法使用@Transactional注解标注,在A方法中调用了B方法,在外部调用A方法时,B方法的事务不会生效。这是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。
那么如果确实在同一类中调用事务方法怎么办呢?有以下3种方法解决:
引入自身bean
@Service
public class UserServiceImpl {
@Autowired
UserMapper userMapper;
@Autowired
UserServiceImpl userServiceImpl;
public void A() {
userServiceImpl.B();
}
@Transactional
public void B() {
userMapper.deleteById(1);
int i = 10 / 0; //模拟发生异常
}
}
复制代码
通过 ApplicationContext 引入bean
@Service
public class UserServiceImpl {
@Autowired
UserMapper userMapper;
@Autowired
ApplicationContext applicationContext;
public void A() {
((UserServiceImpl) applicationContext.getBean("userServiceImpl")).B();
}
@Transactional
public void B() {
userMapper.deleteById(1);
int i = 10 / 0; //模拟发生异常
}
}
复制代码
通过 AopContext 获取当前代理类
在启动类上添加注解@EnableAspectJAutoProxy(exposeProxy = true),表示是否对外暴露代理对象,即是否可以获取AopContext。
@Service
public class UserServiceImpl {
@Autowired
UserMapper userMapper;
public void A() {
((UserServiceImpl) AopContext.currentProxy()).B();
}
@Transactional
public void B() {
userMapper.deleteById(1);
int i = 10 / 0; //模拟发生异常
}
}
复制代码
4、方法的事务传播类型不支持事务。
若propagation属性设置如下三种事务传播行为,事务将不会发生回滚。
1、SUPPORTS:如果当前存在事务,则加入该事务;
如果当前没有事务,则以非事务的方式继续运行。
2、NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
3、NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
Spring事务传播行为可以查阅我之前的一篇文章:
Spring事务(一)-事务传播行为
5、不正确地捕获异常。
使用了try-catch代码块将异常捕捉了,没有向上抛出异常,事务不会回滚。
@Transactional
private void A() throws Exception {
@Autowired
UserMapper userMapper;
try {
//A方法插入数据
User u = New User();
u.setId(1);
u.setName("张三");
userMapper.insert(u);
/**
* B方法也插入数据
*/
b.insert();
} catch (Exception e) {
e.printStackTrace();
}
}
复制代码
上面的代码,如果B方法内部抛了异常,而A方法此时try catch了B方法的异常,那么,事务并不会回滚,而且会抛出如下异常:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
复制代码
因为当ServiceB中抛出了一个异常以后,ServiceB标识当前事务需要rollback。但是ServiceA中由于你手动的捕获这个异常并进行处理,ServiceA认为当前事务应该正常commit。此时就出现了前后不一致,也就是因为这样,抛出了前面的UnexpectedRollbackException异常。
6、属性rollbackFor 设置错误。
rollbackFor 可以指定能够触发事务回滚的异常类型。Spring默认抛出了未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定 rollbackFor 属性。
7、数据库不支持事务。
以MySQL为例,InnoDB引擎是支持事务的,而像MyISAM、MEMORY等是不支持事务的。 从MySQL5.5.5开始默认的存储引擎是InnoDB,之前默认都是MyISAM。
8、方法使用final修饰。
如果一个方法不想被子类重写,那么我们就可以把他写成final修饰的方法。如果事务方法使用final修饰,那么AOP就无法在代理类中重写该方法,事务就不会生效。同样的,static修饰的方法也无法通过代理变成事务方法。
9、未开启事务。
如果是SpringBoot项目,那么SpringBoot通过DataSourceTransactionManagerAutoConfiguration自动配置类帮我们开启了事务。如果是传统的Spring项目,则需要我们自己配置。在Spring配置文件配置如下:
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="insert*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>
<tx:method name="delete*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<aop:config proxy-target-class="true" expose-proxy="true">
<aop:pointcut id="txPointcut"
expression="execution( * com.posun..service.impl..*+.*(..)) || execution(* com.posun..task..*+.*(..))
|| execution(* com.posun.report.ReportJdbc.*(..)) "/>
<aop:advisor id="txAdvisor" advice-ref="txAdvice" pointcut-ref="txPointcut"/>
</aop:config>
复制代码
或者,使用注解的方式。
[/code][size=4][b] 10、多线程调用[/b][/size]
[code]@Service
public class UserServiceImpl {
@Autowired
UserMapper userMapper;
@Transactional
public void A() {
userMapper.deleteById(1);
new Thread(()->{
userMapper.deleteById(2);
int i = 10/0; //模拟发生异常
}).start();
}
}
复制代码
以上代码,A方法中,启动了一个新的线程,并在新的线程中发生了异常,这样A方法是不会发生回滚的。因为两个操作不在一个线程中,获取到的数据库连接不一样,从而是两个不同的事务,所以也不会回滚。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/)
Powered by Discuz! X3.4