Spring入门心经之第四章 事务详解

什么是事务

从逻辑上来说事务就是把一组动作看成一个操作,要么都执行,要么都不执行。例如下面这段代码,如果这段代码不在同一事务的话,那么执行结束时只有老王被插入到数据库,小明没有被插入,这个操作不符合业务上的一致性。另外补充一点,使用事务进行增删改查时,必须保证所使用的数据库引擎支持事务;例如常用的MySQL5.5后版本默认使用支持事务的 innodb引擎,MySQL5.5前数据库引擎则为myisam,那么程序也就不再支持事务了。

java 复制代码
    public void test1() {
        Country country = new Country("老王", "male", 23);
        countryService.save(country);
        int i = 1 / 0;
        Country country2 = new Country("小明", "male", 28);
        countryService.save(country);
    }

事务的特性是什么

  • 原子性(Atomicity):事务是最小的执行单位,不允许分割;要么都执行,要么都不执行。
  • 一致性(Consistency):在事务操作之前和之后,数据都是保持一个相同的状态,数据完整性没有被破坏。
  • 隔离性(Isolation):并发操作时,多个事务之间互不影响;而隔离级别有:读未提交(Read uncommitted)、读已提交(read committed)、可重复读(repeatable read)、串行化(Serializable)。
  • 持久性(Durability):一个事务执行后,他对数据库的改变是持久的,即使系统发生故障。

Spring支持的两种事务管理

编程式事务

分别是TransactionTemplateTransactionManager,实际开发中比较少应用。 使用TransactionTemplate的示例如下:

java 复制代码
 	@Autowired
    private TransactionTemplate transactionTemplate;

    public void testTransactionTemplate() {
        transactionTemplate.execute(new TransactionCallback<Object>() {
            @Override
            public Object doInTransaction(TransactionStatus transactionStatus) {

                try {
                    //业务代码
                    return null;
                } catch (Exception e) {
                    //回滚
                    transactionStatus.setRollbackOnly();
                }
                return null;
            }
        });
    }

使用TransactionManager的示例如下:

java 复制代码
 	
   @Autowired
   private PlatformTransactionManager transactionManager;

   public void testTransactionManger(){
        TransactionStatus transactionStatus = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            // 业务代码
            transactionManager.commit(transactionStatus);
        } catch (TransactionException e) {
            transactionManager.rollback(transactionStatus);
        }

    }

注解式事务

@Transactional在实际开发中比较常应用,基于注解,示例如下所示:

java 复制代码
 @Transactional(propagation = Propagation.REQUIRED)
    public void addRequired(SysUser user){
        sysUserMapper.insert(user);
    }

Spring事务的隔离级别及传播行为

隔离级别

  • default(默认):PlatfromTransactionManager的默认隔离级别,使用数据库默认的事务隔离级别,除了default 其它几个Spring事务隔离级别与JDBC事务隔离级别相对应。
  • read_uncommited(读未提交):一个事务可以读取另外一个事务未提交的数据,这可能出现脏读、不可重复读、幻读。
  • read_commited(读已提交):只有已提交的事务才可以被读取,在并发场景下可以避免脏读,但是无法避免不可重复读和幻读。
  • repeatTable_read(可重复读):多次查询结果是一致的,除非当前事务修改并提交,可以避免脏读的前提下,避免不可重复读,但是仍然无法避免幻读。
  • serializable(串行化):这是一个花费较高但是比较可靠的事务隔离级别,可以避免脏读、不可重复读和幻读。

传播行为

  • Required(默认属性):如果存在一个事务则支持当前事务,如果不存在则创建一个事务;当前方法及被调用方法属于同一事务,一个回滚全部回滚。
  • Mandatory:如果当前存在事务,则支持当前事务;如果不存在则抛出异常。
  • Never:以非事务方式执行;如果当前存在事务,则抛出异常。
  • Supports:如果当前存在事务,则支持当前事务;如果不存在事务,则以非事务方式执行。
  • Not_Supports:以非事务方式执行;如果存在事务,则挂起当前事务。
  • Required_new:无论当前存不存在事务,Propagation.REQUIRES_NEW修饰的内部方法会独立新建事务,且内部方法之间、内部方法和外部方法事务均相互独立,互不干扰。
  • Nested:嵌套,支持当前事务,内层事务的执行失败并不会影响到外层事务的回滚,但外层事务的回滚会影响内层事务导致内层事务随外层事务一同回滚。

代码示例

为了更好的理解传播行为,我们用代码来演示下,这里先给出sys_order表的SysOrderServiceImpl代码:

java 复制代码
@Service
public class SysOrderServiceImpl extends ServiceImpl<SysOrderMapper, SysOrder>
        implements SysOrderService {
    @Autowired
    private SysOrderMapper sysOrderMapper;

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void addRequired(SysOrder order) {
        sysOrderMapper.insert(order);
    }

    @Override
    @Transactional(propagation = Propagation.MANDATORY)
    public void addMandatory(SysOrder order) {
        sysOrderMapper.insert(order);
    }

    @Override
    @Transactional(propagation = Propagation.SUPPORTS)
    public void addSupports(SysOrder order) {
        sysOrderMapper.insert(order);
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void addRequiresNew(SysOrder order) {
        sysOrderMapper.insert(order);
    }


    @Override
    @Transactional(propagation = Propagation.NESTED)
    public void addNested(SysOrder order) {
        sysOrderMapper.insert(order);
    }


}

country表的CountryServiceImpl代码:

java 复制代码
@Service
public class CountryServiceImpl extends ServiceImpl<CountryMapper, Country>
        implements CountryService {

    @Autowired
    private CountryMapper countryMapper;

   
    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void addRequired(Country country) {
        countryMapper.insert(country);
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void addRequiredException(Country country) {
        countryMapper.insert(country);
        throw new RuntimeException();
    }

    @Override
    @Transactional(propagation = Propagation.MANDATORY)
    public void addMandatory(Country country) {
        countryMapper.insert(country);
    }

    @Override
    @Transactional(propagation = Propagation.SUPPORTS)
    public void addSupports(Country country) {
        countryMapper.insert(country);
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void addRequiresNew(Country country) {
        countryMapper.insert(country);
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void addRequiresNewException(Country country) {
        countryMapper.insert(country);
        throw new RuntimeException();
    }


    @Override
    @Transactional(propagation = Propagation.NESTED)
    public void addNested(Country country) {
        countryMapper.insert(country);
    }

    @Override
    @Transactional(propagation = Propagation.NESTED)
    public void addNestedException(Country country) {
        countryMapper.insert(country);
        throw new RuntimeException();
    }
}

TransactionDefinition.PROPAGATION_REQUIRED

它是Spring的默认传播行为,如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务。 如下场景,外部方法没有加事务,且SysOrderServiceImplCountryServiceImpl的方法都加了Propagation.REQUIRED级别传播行为,所以外部方法异常不影响内部方法插入。

java 复制代码
    @GetMapping("/add1")
    public void test_propagation_required() {
        countryService.addRequired(new Country("1", "1", 1));
        orderService.addRequired(new SysOrder("1", "1", 0L, 0, new Date()));
        throw new RuntimeException();
    }

场景2,外部方法加了事务,此时两个内部方法加入到外部方法的事务中,因此当外部方法报错时,内部方法插入操作都失败。

java 复制代码
 	@GetMapping("/add2")
    @Transactional(propagation = Propagation.REQUIRED)
    public void test_transactional_propagation_required() {
        countryService.addRequired(new Country("1", "1", 1));
        orderService.addRequired(new SysOrder("1", "1", 0L, 0, new Date()));
        throw new RuntimeException();
    }

场景3,外部方法没有加事务,内部方法之间事务互不影响,可以看到CountryService调用addRequiredException方法抛出异常,而SysOrderService正常插入。

java 复制代码
    @GetMapping("/add3")
    public void test_transactional_propagation_required_exception() {
        orderService.addRequired(new SysOrder("1", "1", 0L, 0, new Date()));
        countryService.addRequiredException(new Country("1", "1", 1));
        throw new RuntimeException();
    }

场景4,代码如场景3差不多,只不过外部方法加了事务,但结果却截然不同,这次两条插入语句都没有执行成功。

java 复制代码
    @GetMapping("/add4")
    @Transactional(propagation = Propagation.REQUIRED)
    public void test_transactional_propagation_required_exception() {
        orderService.addRequired(new SysOrder("1", "1", 0L, 0, new Date()));
        countryService.addRequiredException(new Country("1", "1", 1));
        throw new RuntimeException();
    }

场景5,在场景3的基础上,捕获异常;但即使异常被捕获报错不被感知,两条插入语句仍旧没有执行成功,这是因为外部内部方法的事务融成一体。

java 复制代码
@GetMapping("/add5")
    @Transactional(propagation = Propagation.REQUIRED)
    public void test_transactional_propagation_required_catch_exception() {
        orderService.addRequired(new SysOrder("1", "1", 0L, 0, new Date()));
        try {
            countryService.addRequiredException(new Country("1", "1", 1));
        } catch (Exception e) {
            System.out.println("方法回滚");
        }
        throw new RuntimeException();
    }

TransactionDefinition.PROPAGATION_MANDATORY

如果当前存在事务,则支持当前事务;如果不存在事务则抛出异常。 场景1,外部方法没有加事务,且SysOrderServiceImplCountryServiceImpl的方法都加了Propagation.MANDATORY级别传播行为,如果当前不存在事务,则会直接抛出异常,所以两条插入语句都没有执行成功。

java 复制代码
	@GetMapping("/add6")
    @Transactional(propagation = Propagation.MANDATORY)
    public void test_propagation_mandatory() {
        orderService.addMandatory(new SysOrder("1", "1", 0L, 0, new Date()));
        countryService.addMandatory(new Country("1", "1", 1));
        throw new RuntimeException();
    }

TransactionDefinition.PROPAGATION_SUPPORTS

如果当前存在事务,则支持当前事务;如果不存在事务,则以非事务方式执行。 场景1,外部方法没有加事务,且SysOrderServiceImplCountryServiceImpl的方法都加了Propagation.SUPPORTS级别传播行为,外部方法抛异常并不会影响内部方法执行,所以两条记录都新增成功。

java 复制代码
 	@GetMapping("/add7")
    @Transactional
    public void test_propagation_supports() {
        orderService.addSupports(new SysOrder("1", "1", 0L, 0, new Date()));
        countryService.addSupports(new Country("1", "1", 1));
        throw new RuntimeException();
    }

TransactionDefinition.PROPAGATION_REQUIRES_NEW

无论当前存不存在事务,Propagation.REQUIRES_NEW修饰的内部方法会独立新建事务,且内部方法之间、内部方法和外部方法事务均相互独立,互不干扰。 场景1,外部方法不加事务,两个内部方法加了REQUIRES_NEW级别事务,外部方法抛异常,两个内部方法事务彼此独立不受影响,所以执行插入语句成功。

java 复制代码
 	@GetMapping("/add8")
    public void test_propagation_required_new() {
        orderService.addRequiresNew(new SysOrder("1", "1", 0L, 0, new Date()));
        countryService.addRequiresNew(new Country("1", "1", 1));
        throw new RuntimeException();
    }

场景2,外部方法依旧不加事务,但countryService调用内部方法中抛异常,所以只有 orderService.addRequiresNew插入了数据。

java 复制代码
 	@GetMapping("/add9")
    public void test_propagation_required_new_exception() {
        orderService.addRequiresNew(new SysOrder("1", "1", 0L, 0, new Date()));
        countryService.addRequiresNewException(new Country("1", "1", 1));
    }

场景3,外部开启事务,其他不变,countryService内部抛出错误,不影响orderService插入数据。

java 复制代码
    @GetMapping("/add10")
    @Transactional
    public void test_transactional_propagation_required_new_exception() {
        orderService.addRequiresNew(new SysOrder("1", "1", 0L, 0, new Date()));
        countryService.addRequiresNewException(new Country("1", "1", 1));
        throw new RuntimeException();
    }

TransactionDefinition.PROPAGATION_NESTED

以非事务方式执行;如果当前存在事务,则抛出异常。 场景1,外部不开启事务,两个插入语句彼此独立事务,外部异常不影响插入。

java 复制代码
    @GetMapping("/add11")
    public void test_propagation_nested() {
        orderService.addNested(new SysOrder("1", "1", 0L, 0, new Date()));
        countryService.addNested(new Country("1", "1", 1));
        throw new RuntimeException();
    }

场景2,外部依旧不开启事务,但countryService内部抛异常,不影响orderService插入数据。

java 复制代码
    @GetMapping("/add12")
    public void test_propagation_nested_exception() {
        orderService.addNested(new SysOrder("1", "1", 0L, 0, new Date()));
        countryService.addNestedException(new Country("1", "1", 1));
    }

场景3,外部开启事务,countryService内部抛异常,影响orderService插入数据。

java 复制代码
 	@GetMapping("/add13")
    @Transactional
    public void test_transactional_propagation_nested_exception() {
        orderService.addNested(new SysOrder("1", "1", 0L, 0, new Date()));
        countryService.addNestedException(new Country("1", "1", 1));
    }

场景4,外部方法开启事务,外部方法抛异常,两个内部方法一同回滚。

java 复制代码
    @GetMapping("/add14")
    @Transactional
    public void test_transactional_exception_propagation_nested() {
        orderService.addNested(new SysOrder("1", "1", 0L, 0, new Date()));
        countryService.addNested(new Country("1", "1", 1));
        throw new RuntimeException();
    }

Transactional 注解详解

@Transactionl可以作用在接口、类、类方法上。如果作用于类上,该类中的所有public方法都具有该类型的事务属性;最常用在方法上,如果在方法上加上该注解,就会覆盖类级别的定义;如果作用于接口上,则只有在使用基于接口的代理时它才会生效,即JDK动态代理时,这种不建议这么用。

它的常见参数配置如下:

  • 传播属性(propagation):用于设置事务传播行为,默认值为REQUIRED
  • 隔离级别(isolation):用于设置事务隔离级别,默认值为DEFAULT
  • 超时时间(timeout):用于设置事务的超时秒数,默认值为-1表示永不超时;如果超过该时间限制但事务还没完成,则会自动回滚。
  • 只读属性(readOnly): 用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false
  • 回滚规则(rollbackFor):用于设置需要进行回滚的异常类数组。

自定义异常怎么使用Transactional

在源码中rollbackFor的注释可以看出,事务将对RuntimeException和Error进行回滚,但不会对已检查的异常 (业务异常) 进行回滚。

java 复制代码
	/**
	 * Defines zero (0) or more exception {@link Class classes}, which must be
	 * subclasses of {@link Throwable}, indicating which exception types must cause
	 * a transaction rollback.
	 * <p>By default, a transaction will be rolling back on {@link RuntimeException}
	 * and {@link Error} but not on checked exceptions (business exceptions). See
	 * {@link org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable)}
	 * for a detailed explanation.
	 * <p>This is the preferred way to construct a rollback rule (in contrast to
	 * {@link #rollbackForClassName}), matching the exception class and its subclasses.
	 * <p>Similar to {@link org.springframework.transaction.interceptor.RollbackRuleAttribute#RollbackRuleAttribute(Class clazz)}.
	 * @see #rollbackForClassName
	 * @see org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable)
	 */

那么在日常开发中会遇到一些公司自己封装的自定义异常,那么怎么使用Transactional来进行必要回滚呢?如下所示,在注解上声明我们自己定义的异常即可:

java 复制代码
@Transactional(rollbackFor= CommException.class)

Spring中this调用导致AOP失效

现象及源码分析

在我们使用Spring开发时,经常会有人说在bean中不要使用this来调用被@Async、@Transactional、@Cacheable等注解标注的方法,this下注解是不生效的。 这里我们以@Transactional为例,通过源码来分析。问题的表象是在加事务的method1方法中执行插入语句,然后在不加事务的method2中调用method1方法,抛异常后事务不回滚。

java 复制代码
 	@GetMapping("/add15")
    @Transactional
    public void method1(){
        orderService.insert(new SysOrder("1", "1", 0L, 0, new Date()));
        int x = 1 / 0;
    }

从上图中可以看出method1执行对象是由Spring中BeanPostProcessor处理过的Cglib对象。所以当method1方法被调用时,代码会执行到DynamicAdvisedInterceptor内部类的intercept代码:

java 复制代码
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
	Object oldProxy = null;
	boolean setProxyContext = false;
	Class<?> targetClass = null;
	Object target = null;
	try {
		if (this.advised.exposeProxy) {
		    // 需要则暴露
			// Make invocation available if necessary.
			oldProxy = AopContext.setCurrentProxy(proxy);
			setProxyContext = true;
		}
		// May be null. Get as late as possible to minimize the time we
		// "own" the target, in case it comes from a pool...
		// 重点:获取被代理的目标对象
		target = getTarget();
		if (target != null) {
			targetClass = target.getClass();
		}
		// 获取拦截器链
		List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
		Object retVal;
		// Check whether we only have one InvokerInterceptor: that is,
		// no real advice, but just reflective invocation of the target.
		if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
			// We can skip creating a MethodInvocation: just invoke the target directly.
			// Note that the final invoker must be an InvokerInterceptor, so we know
			// it does nothing but a reflective operation on the target, and no hot
			// swapping or fancy proxying.
			// 如果链是空且是public方法,则直接调用
			Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
			retVal = methodProxy.invoke(target, argsToUse);
		}
		else {
			// We need to create a method invocation...
			// 否则创建一个CglibMethodInvocation以便驱动拦截器链
			retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
		}
		// 处理返回值,同JDK动态代理
		retVal = processReturnType(proxy, target, method, retVal);
		return retVal;
	}
	finally {
		if (target != null) {
			releaseTarget(target);
		}
		if (setProxyContext) {
			// Restore old proxy.
			AopContext.setCurrentProxy(oldProxy);
		}
	}
}

那么基于上面的核心概念后,我们来说说嵌套调用导致事务失效的问题,代码如下所示,我们发现method1方法执行报错了,但仍插入数据到数据库中,这是为什么呢?因为method1方法的执行者并不是cglib中生成代理对象,而是this

java 复制代码
	@GetMapping("/add15")
    @Transactional
 	public void method1(){
        orderService.insert(new SysOrder("1", "1", 0L, 0, new Date()));
        int x = 1 / 0;
    }

    @GetMapping("/add16")
    public void method2(){
        method1();
    }

这就导致调用method1的调用者是this,而不是cglib代理生成的增强类,如下图所示 ,调用者不是this,导致cglib代理无法知晓method1被调用,也就无法产生事务,导致事务失效。

解决this调用的方法

方法一,通过ApplicationContext 找到动态代理对象来调用该方法。

java 复制代码
 	@Autowired
    private ApplicationContext applicationContext;
    
	@GetMapping("/add15")
    @Transactional
 	public void method1(){
        orderService.insert(new SysOrder("1", "1", 0L, 0, new Date()));
        int x = 1 / 0;
    }
    
    @GetMapping("/add16")
    public void method2() {
        TestTransactionController testTransactionController = applicationContext.getBean(TestTransactionController.class);
        testTransactionController.method1();
    }

方法2,使用AopContext获取动态代理对象,不过需要配置@EnableAspectJAutoProxy(proxyTargteClass = true, exposeProxy = true)才能有效

java 复制代码
	@GetMapping("/add15")
    @Transactional
    public void method1() {
        orderService.insert(new SysOrder("1", "1", 0L, 0, new Date()));
        int x = 1 / 0;
    }

    @GetMapping("/add16")
    public void method2() {
        ((TestTransactionController) AopContext.currentProxy()).method1();
    }

参考文献

blog.csdn.net/LwinnerG/ar... zhuanlan.zhihu.com/p/91779567 juejin.cn/post/684490...

相关推荐
草莓base5 小时前
【手写一个spring】spring源码的简单实现--bean对象的创建
java·spring·rpc
乌啼霜满天2495 小时前
Spring 与 Spring MVC 与 Spring Boot三者之间的区别与联系
java·spring boot·spring·mvc
Elaine2023916 小时前
零碎04 MybatisPlus自定义模版生成代码
java·spring·mybatis
.生产的驴8 小时前
SpringCloud OpenFeign用户转发在请求头中添加用户信息 微服务内部调用
spring boot·后端·spring·spring cloud·微服务·架构
码蜂窝编程官方11 小时前
【含开题报告+文档+PPT+源码】基于SpringBoot+Vue的虎鲸旅游攻略网的设计与实现
java·vue.js·spring boot·后端·spring·旅游
AuroraI'ncoding11 小时前
时间请求参数、响应
java·后端·spring
计算机毕设指导614 小时前
基于 SpringBoot 的作业管理系统【附源码】
java·vue.js·spring boot·后端·mysql·spring·intellij-idea
ExiFengs15 小时前
实际项目Java1.8流处理, Optional常见用法
java·开发语言·spring
瓜牛_gn16 小时前
依赖注入注解
java·后端·spring
一元咖啡17 小时前
SpringCloud Gateway转发请求到同一个服务的不同端口
spring·spring cloud·gateway