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 同一类中方法调用(重要)

如果同一个类中的两个方法分别为AB,方法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方法的事务并不会开启。

解决方案:

  1. 新增service,把updateOrder移动新的service,然后注入再调用。
  2. 在自己类中注入自己,用注入的对象再调用另外一个方法(需要开启循环依赖)。
  3. 通过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的事务是通过数据库连接来实现的。当前线程中保存了一个mapkey是数据源,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
    }
}

如果在执行上面这段代码,更新数据时,程序报错了,抛了SqlExceptionDuplicateKeyException等异常。而BusinessException是自定义的异常,报错的异常不属于BusinessException,所以事务也不会回滚。

即使rollbackFor有默认值,但《阿里巴巴开发者规范》中,还是要求开发者重新指定该参数。这是为什么呢?

因为如果使用默认值,一旦程序抛出了Exception,事务不会回滚,这会出现很大的bug。所以,建议一般情况下,将该参数设置成:ExceptionThrowable

总结

本文总结了事务失效的场景,其实发生最多就是同一类中方法调用、不正确的捕获异常、异常类型错误这3个了

posted @ 2022-04-24 11:29  夏尔_717  阅读(636)  评论(0编辑  收藏  举报