spring中,可以通过@Transaction注解进行事务的使用,但是在使用过程中常常会存在某些情况,导致事务失效,为了避免事务失效引发的问题,我们可以记录以下一些事务失效的常见场景。
事务失效会分为两个大的类型,一是事务不生效,二是事务不回滚。
事务不生效情况:
一、访问权限非公开:
@Transactional(rollbackFor = Exception.class)
private int save(PaymentEntity entity) {
return paymentDao.insert(entity);
}
当我们所使用事务的方法中,如果其访问权限是非公开的(protected、private、default),那么事务会失效。
其底层是因为:@Transaction注解的方法会执行一个事务判断,在AbstractFallbackTransactionAttributeSource类的computeTransactionAttribute方法中,判断目标方法是否为public,如果否,则直接返回null,即不支持事务:
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// The method may be on an interface, but we need attributes from the target class.
// If the target class is null, the method will be unchanged.
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
// First try is the method in the target class.
TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
if (txAttr != null) {
return txAttr;
}
// Second try is the transaction attribute on the target class.
txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
return txAttr;
}
if (specificMethod != method) {
// Fallback is to look at the original method.
txAttr = findTransactionAttribute(method);
if (txAttr != null) {
return txAttr;
}
// Last fallback is the class of the original method.
txAttr = findTransactionAttribute(method.getDeclaringClass());
if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
return txAttr;
}
}
return null;
}
二、方法被final或static修饰
@Transactional(rollbackFor = Exception.class)
public static int save(PaymentEntity entity) {
return paymentDao.insert(entity);
}
spring事务的源码中,spring事务底层是通过使用了AOP,即通过jdk动态代理或cglib,帮我们生成了代理类,在代理类中实现事务功能。
如果,事务的目标方法中使用了final修饰,则会导致其代理类无法重写方法,无法添加事务功能。
@Transactional(rollbackFor = Exception.class)
public final int save(PaymentEntity entity) {
return paymentDao.insert(entity);
}
同样道理,被static修饰的静态方法,也是无法通过动态代理,添加事务功能。
三、事务方法被内部调用
@Override
public PaymentEntity getById(String id) {
doCheck();
return paymentDao.getById(id);
}
@Transactional(rollbackFor = Exception.class)
public void doCheck(){
System.out.println("假设该方法用于进检查参数有效性");
}
}
在同一个类中,内部某一个方法,调用了本类中的事务方法,会导致事务失效。
因为内部调用方式,会默认使用this调用,this指代的是当前类的实例,使用实例调用时,同上道理,事务通过AOP所生成的代理对象,未被使用,导致事务功能实际没有被添加。
-- 解决方式:
1、取消掉内部的调用,即分开两个类进行正常调用;
2、当前类中注入自身,通过调用自身bean对象的方法,实现aop代理:
@Service
public class PaymentService implements IPaymentService {
@Resource
private PaymentDao paymentDao;
@Autowired
private PaymentService service;
@Override
public PaymentEntity getById(String id) {
service.doCheck();
return paymentDao.getById(id);
}
@Transactional(rollbackFor = Exception.class)
public void doCheck(){
System.out.println("假设该方法用于进检查参数有效性");
}
}
3、通过ApplicationContext引入bean,而后通过bean对象调用方法,使用代理类:
这个方法是在配置有bean的情况下使用,目前通过最新的springcloud形式开发的情况,springboot中无需再自行配置,则直接使用类名首字母小写(默认是spring boot的bean注册名),当然,如果对此不太了解的,还是少用这种方法:
@Service
public class PaymentService implements IPaymentService {
@Resource
private PaymentDao paymentDao;
@Autowired
ApplicationContext applicationContext;
@Override
public PaymentEntity getById(String id) {
((PaymentService)applicationContext.getBean("paymentService")).doCheck();
return paymentDao.getById(id);
}
@Transactional(rollbackFor = Exception.class)
public void doCheck(){
System.out.println("假设该方法用于进检查参数有效性");
}
}
4、其他方式。。。其实说白了这种失效情况,只需要你能通过代理去调用这个事务方法即可生效,还有其他的方式可以在当前类中取得代理,基本上则都可以获取代理之后去使用方法即可解决。
四、当前类未被spring管理
即当前service类忘记加@Service这些spring的注解,会导致这个类不被spring管理,使用@Transactional自然不会生效,但是这个注解是可以使用的。
public class PaymentService implements IPaymentService {
@Resource
private PaymentDao paymentDao;
@Override
@Transactional
public PaymentEntity getById(String id) {
return paymentDao.getById(id);
}
}
五、多线程调用
如果目标事务方法中,调用一个其他线程的方法,会导致事务失效。
原因其实很简单,本身事务的出现是基于同一个数据库连接,即同一个线程下,通过同时提交或同时回滚,达成事务的目的(数据库事务),当出现多线程时,其事务特性本身已经被破坏。
六、表不支持事务
如果数据库引擎是myisam
的话,是不支持事务的,当然这个情况比较少见。
七、未开启事务配置
事务的使用,在spring中实际上是通过配置生效的,而不是默认生效,如果没有给项目配置事务开启,即使使用了注解也不会生效的。我们需要区分几个情况去决定:
1、spring boot项目:
spring boot项目中,会通过DataSourceTransactionManagerAutoConfiguration类,默认开启事务,所以不需要额外配置;
2、传统spring项目:
需要在applicationContext.xml文件中,手动配置事务参数,启用事务:
<!-- 配置事务管理器 -->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<tx:advice id="advice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!-- 用切点把事务切进去 -->
<aop:config>
<aop:pointcut expression="execution(* com.susan.*.*(..))" id="pointcut"/>
<aop:advisor advice-ref="advice" pointcut-ref="pointcut"/>
</aop:config>
八、事务方法的多嵌套:
import javax.annotation.Resource;
import java.util.List;
public class PaymentService implements IPaymentService {
@Resource
private PaymentDao paymentDao;
@Override
public int save(PaymentEntity entity) {
//当前被事务方法调用的非事务方法,实际存在多个事务
getById(entity.getId());
return paymentDao.insert(entity);
}
@Override
@Transactional
public List<PaymentEntity> queryPage(PaymentEntity entity) {
save(entity);
return paymentDao.queryPage(entity);
}
@Override
public PaymentEntity getById(String id) {
return paymentDao.getById(id);
}
}
在上面代码中,queryPage是事务目标方法,执行时内部存在2个其他方法,save和queryPage,而同时,save中又存在2个方法getById和insert,那么此时,由于save方法是没有使用事务注解,但是却相当于执行了2个不同事务(一个独立方法就等同一个单独的事务),当save下执行的方法出现问题,则不会反馈到queryPage中,导致事务失效。
这个情况就是事务多嵌套使用其他方法时需要注意的失效。
事务不回滚情况:
一、错误的传播特性
使用@Transactional注解时,是可以指定propagation参数的,此参数作用是指定事务的传播特性,spring目前支持7种传播特性:
1、REQUIRED 如果当前上下文中存在事务,则加入该事务,如果不存在事务,则创建一个事务,这是默认的传播属性值。
2、SUPPORTS 如果当前上下文中存在事务,则支持事务加入事务,如果不存在事务,则使用非事务的方式执行。
3、MANDATORY 当前上下文中必须存在事务,否则抛出异常。
4、REQUIRES_NEW 每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
5、NOT_SUPPORTED 如果当前上下文中存在事务,则挂起当前事务,然后新的方法在没有事务的环境中执行。
6、NEVER 如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行代码。
7、NESTED 如果当前上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。
在一般情况下我们是使用默认的传播特性,即不需要修改参数,但是如果有某一个需求需要你去手动设置该propagation参数了,而你设置的参数值为NEVER时,就会导致事务失效无法回滚。
二、在目标事务方法中手动捕获异常
@Override
@Transactional
public PaymentEntity getById(String id) {
try{
paymentDao.getById(id);
}catch (Exception e){
e.printStackTrace();
}
return null;
}
手动try catch了你需要执行事务的内容,而又不返回异常,此时事务不会回滚,因为没有任何异常返回时,spring会认为执行正常。
三、手动抛出异常时类型不符合事务捕获类型
@Transactional
public PaymentEntity getById(String id) throws Exception {
try{
paymentDao.getById(id);
}catch (Exception e){
e.printStackTrace();
throw new Exception(e);
}
return null;
}
默认情况下,spring事务只回滚RuntimeException(运行时异常)和Error(错误),对于普通异常是不符合回滚标准,不会执行回滚。
如果需要对指定异常都进行回滚,我们通常需要配置回滚标准参数 rollbackfor = 回滚标准类.class,用于令符合该参数的异常都回滚,通常我们会直接回滚所有异常,例如:
//当指定为Exception.class时,Exception及其子类都会被捕获回滚
@Transactional(rollbackFor = Exception.class)
public PaymentEntity getById(String id) throws Exception {
try{
paymentDao.getById(id);
}catch (Exception e){
e.printStackTrace();
throw new Exception(e);
}
return null;
}
四、事务方法嵌套使用时,事务标准不同
当事务方法A内部会使用到事务方法B时,如果这2个方法的回滚标准不同(rollbackfor参数),则容易导致回滚失效。
其他
一、大事务问题
如果事务目标方法中存在多个方法,则容易出现大事务,比如常见query方法,有些时候可能会出现许多深层的各个方法调用才会产生结果,这种情况容易造成性能问题,我们也尽可能不要随意添加一些不必要的事务。一般事务常用于增删改中比较多而已。
二、编程式事务
上述内容是基于@Transactional注解的,主要说明的是它的事务问题,这种事务称之为:声明式事务。
而spring实际还提供另一种创建事务的方式,通过手动编写代码实现的事务,这种称之为:编程式事务。
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {
queryData1();
queryData2();
transactionTemplate.execute((status) => {
addData1();
updateData2();
return Boolean.TRUE;
})
}
spring中提供一个TransactionTemplate类,在它的execute方法中实现事务功能。
编程式事务有以下有点:
1、能避免springAOP问题导致的事务失效;
2、能更小粒度控制事务范围,更直观。
如果在开发中,开发者是精确明白事务需要点的情况下,其实可以直接使用编程式事务会更好。