转载

SpringBoot事务使用及注意事项

感谢你百忙之中抽出时间阅读我这篇笔记。如果有错误的地方,劳烦批评指正。如果有地方和我持不同意见,很高兴和你一起探讨。最后,如果觉得这篇笔记对你有帮助的话,麻烦点个赞,谢谢~

1.简介

数据库事务的存在是为了保证“多个数据库操作”的“原子性”。举个最简单的银行汇款业务的场景,A向B汇款1000元。这个汇款动作主要有两个,①是A的银行账户上扣去1000元,②是B的银行账户上增加两千元。假如操作①成功了,而操作②失败了,这样A的账户上就白白少了1000元,而B的账户上却没有增加1000。所以我们需要用技术来保证操作①和操作②整体的原子性(即让操作①和②要么同时成功,要么同时失败),数据库的事务就是为此而生的。

在我们使用Springboot框架来开发时,Springboot已经帮我们封装好对底层数据库事务的操作,降低了我们学习、操作使用数据库事务的成本。这篇笔记就简单的记录下,在Springboot框架中(Springboot版本2.3.1.RELEASE,整合了mybatis,数据库使用MySQL)如何配置使用事务,以及在使用Springboot事务时遇见的坑。

2.Springboot实现事务支持的3种技术

Springboot想让某个方法使用数据库事务,只需要在对应的方法上加上@Transactional就可以。(有关于@Transactional注解的各个参数的配置,可以 去网上查下 ,或者看下@Transactional源代码上的注释。)

Springboot有3种技术方式来实现让加了@Transactional的方法能使用数据库事务,分别是"动态代理(运行时织入)"、“编译期织入”和“类加载期织入”。这3种技术都是基于AOP(Aspect Oriented Programming,面向切面编程)思想。(在网上看了很多文章,大家伙儿都把AOP称之为一种技术,其实不然,AOP并不特指一种技术,而是一种编程范式,基于AOP编程范式,不同的编程语言都有自己的实现。)

下面我们就来讲讲,如何配置Springboot,让它分别基于“动态代理”和“编译期织入(使用AspectJ)”来实现对@Transactional开启数据库事务的支持。(基于"动态代理"的方式(支持@Transactional)在使用上会有些坑需要注意,在后文中会指出。)

2.1.基于动态代理支持@Transactional

2.1.1.配置

  1. pom中添加spring-tx依赖

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-tx</artifactId>
        <version>5.2.7.RELEASE</version>
    </dependency>
    复制代码
  2. 通过注解的方式(也可以通过xml或者Java配置类的方式,不过没有使用注解的方式快)开启你的SpringBoot应用对事务的支持。使用@EnableTransactionManagement注解(来自于上面引入的spring-tx包)

    @SpringBootApplication
    @EnableTransactionManagement
    public class Application {
    
    	public static void main(String[] args) {
    		SpringApplication.run(Application.class, args);
    	}
        
    }
    
    复制代码

    Spring推荐的方式,是将@EnableTransactionManagement加到被@Configuration注解的类上,而@SpringBootApplication被@SpringBootConfiguration注解,@SpringBootConfiguration又被@Configuration,所以这里我们可以将@EnableTransactionManagement注解加到被@SpringBootApplication注解的类上。

2.1.2.测试

  1. 创建测试用的TransactionController

    package com.huang.spring.practice.transaction;
    
    import com.huang.spring.practice.user.dao.UserMapper;
    import com.huang.spring.practice.user.dto.User;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.transaction.annotation.Transactional;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     *
     */
    @RestController
    @RequestMapping("/api/transaction")
    public class TransactionController {
    
        @Autowired
        private UserMapper userMapper;
    
        /**
         * 测试Spring事务
         * 插入一个新user之后,故意抛出运行时异常,Spring事务会回滚,插入user失败
         */
        @Transactional
        @PostMapping("/testTransactionThrowExcep")
        public User testTransactionThrowExcep() {
            User user = new User();
            user.setName("小李");
            user.setAge((short) 13);
            user.setCity("北京");
            userMapper.insert(user);
    
            throw new RuntimeException("故意抛出一个运行时异常");
        }
    
        /**
         * 测试Spring事务
         * 成功插入user
         */
        @Transactional
        @PostMapping("/testTransactionNoExcep")
        public User testTransactionNoExcep() {
            User user = new User();
            user.setName("小李");
            user.setAge((short) 13);
            user.setCity("北京");
            userMapper.insert(user);
            return user;
        }
    
    }
    
    复制代码
    1. 先调用/api/transaction/testTransactionThrowExcep接口

      由于我们在testTransactionThrowExcep接口最后抛出了一个RuntimeException,所以接口返回500.

    SpringBoot事务使用及注意事项

    因为有异常抛出,所以testTransactionThrowExcep接口内的事务会回滚,我们插入“小李”的用户信息就不会落到数据库中,查看数据库user表中现在的数据,不存在“小李”的数据,说明Spring的事务生效了

    mysql> select id, name, age,city from user order by id desc;
    +----+--------+-----+--------+
    | id | name   | age | city   |
    +----+--------+-----+--------+
    |  1 | 小明   |  18 | 深圳   |
    +----+--------+-----+--------+
    1 row in set (0.00 sec)
    复制代码
    1. 再调用/api/transaction/testTransactionNoExcep接口,接口成功执行,返回200HTTP状态码以及往数据库中插入的新用户“小李”的信息:
    SpringBoot事务使用及注意事项

    查看数据库user表中现在的数据,用户“小李”的信息成功的插入到了user表中:

    mysql> select id, name, age,city from user order by id desc;
    +----+--------+-----+--------+
    | id | name   | age | city   |
    +----+--------+-----+--------+
    |  4 | 小李   |  13 | 北京   |
    |  1 | 小明   |  18 | 深圳   |
    +----+--------+-----+--------+
    2 rows in set (0.00 sec)
    
    复制代码

2.1.3.事务失效的坑

在使用基于动态代理支持的@Transactional的时候,遇见了一些@Transactional不生效的场景,大家在使用的时候要特别注意,最好是写个单元测试,测试下自己添加了@Transactional的方法,事务是否如我们预期的生效了。

具体的@Transactional事务失效的场景可以参考这篇文章 Spring事务失效的 8 大原因! ,写的还是挺详细的。我就这篇文章中提到的“被@Transactional注解的方法不是public”以及“被@Transactional注解的方法是通过同一个类中的其他方法的自调用”这两个场景,事务之所以失效,还是因为“动态代理”的原因。上文中我们已经提到@Transactional注解是Spring框架基于AOP的编程范式,通过动态代理技术来实现被@Transactional注解的方法能实现数据库事务。假设类A中有个方法a被@Transactional注解,但是方法a的访问权限是private的时候,Spring框架将类A的实例注入到Spring容器中成为bean的过程中,使用“动态代理”将bean A增加的时候,会忽略private方法,因为在实例外部,你是无法通过实例对象直接去调用它的private方法,比如下面这个例子,TransactionService的updateUserAgeByIdTransactional方法是private,在TransactionController中是无法被直接调用的:

SpringBoot事务使用及注意事项
SpringBoot事务使用及注意事项

所以动态代理也就没法代理private方法,自然加在private方法上面的@Transactional注解就会失效了。

而“被@Transactional注解的方法是通过同一个类中的其他方法的自调用”时事务没法生效的问题,其实也是“动态代理”的原因,看下下面的例子,①就是我们所说的类内部方法自调用,它等价于②。所以当我们通过类内部方法自调用的时候,是通过这个类的实例(这个类在Spring中的真正的原始的没有被动态代理过的bean)去调用被@Transactional注解的方法,而不是通过被Spring用动态代理增强过(解析支持了@Transactional注解)之后的实例对象去调用,所以自然@Transactional注解无法生效。

public void updateUserAgeById(long userId, short age) {
        updateUserAgeByIdTransactional(userId, age); //①
        this.updateUserAgeByIdTransactional(userId, age); //②
    }

@Transactional
public void updateUserAgeByIdTransactional(long userId, short age) {
    User user = new User();
    user.setId(userId);
    user.setAge(age);
    userMapper.updateByPrimaryKeySelective(user);
    throw new RuntimeException();
}
复制代码

但是,如果我们非要让@Transactional注解能放到private方法上、让类内部方法自调用时@Transactional能生效的话,我们可以采用“编译期织入”或“类加载期织入”的方式,在运行代码前,将我们的目标类的方法增强,无需管用“动态代理”实现时的种种限制。本文就接下来就讲下,如何使用AspectJ来实现在编译期对@Transactional注解的方法进行织入。

2.2.基于AspectJ编译期织入来支持@Transactional

AspectJ的编译期织入的原理,其实就是动态生成class字节码的技术,修改我们原本要生成的class文件,在其上添加我们想要的功能代码。

2.2.1.配置

将@EnableTransactionManagement中的mode设置为AdviceMode.ASPECTJ(默认为AdviceMode.PROXY,也就是我们的动态代理~)

package com.huang.spring.practice;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.AdviceMode;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.Transactional;

@SpringBootApplication(scanBasePackages = {"com.huang.*"})
//@EnableTransactionManagement
@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
@MapperScan({"com.huang.spring.practice"})
public class Application {

   public static void main(String[] args) {
      SpringApplication.run(Application.class, args);
   }

}
复制代码

pom文件中加入相关依赖,以及配置AspectJ的maven插件(我们的项目是通过maven管理的)让项目在编译期间能通过AspectJ来修改、创建需要被织入的class文件:

<!-- aspectj代码织入 -->
		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjrt</artifactId>
			<version>1.9.5</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-aspects</artifactId>
			<version>5.2.7.RELEASE</version>
		</dependency>
		
		<!-- 添加AspectJ插件 -->
		<build>
            <plugins>
                <plugin>
				<groupId>org.codehaus.mojo</groupId>
				<artifactId>aspectj-maven-plugin</artifactId>
				<version>1.11</version>
				<configuration>
					<aspectLibraries>
						<aspectLibrary>
							<groupId>org.springframework</groupId>
							<artifactId>spring-aspects</artifactId>
						</aspectLibrary>
					</aspectLibraries>
					<complianceLevel>1.8</complianceLevel>
					<source>1.8</source>
					<target>1.8</target>
					<showWeaveInfo>true</showWeaveInfo>
				</configuration>
				<executions>
					<execution>
						<goals>
							<goal>compile</goal>
						</goals>
					</execution>
				</executions>
                </plugin>
			</plugins>
		</build>

复制代码

2.2.2.测试

下面我们测试,将@Transactional注解添加到private方法上,并通过类内部自调用,看看事务能否生效,代码如下:

在TransactionController中提供要测试的接口/api/transaction/updateUserAgeById

package com.huang.spring.practice.transaction.controller;

import com.huang.spring.practice.transaction.service.TransactionService;
import com.huang.spring.practice.user.dao.UserMapper;
import com.huang.spring.practice.user.dto.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/transaction")
public class TransactionController {

    @Autowired
    private TransactionService transactionService;

    @PostMapping("/updateUserAgeById/{userId}/{age}")
    public void updateUserAgeById(@PathVariable("userId") long userId, @PathVariable("age") short age) {
        transactionService.updateUserAgeById(userId, age);
    }

}
复制代码

TransactionService.updateUserAgeById方法如下,通过类内部自调用,调用添加了@Transactional注解的private方法updateUserAgeByIdTransactional,这个方法会更新指定id的用户的age,并且在方法最后抛出RuntimeException。假如我们调用/api/transaction/updateUserAgeById之后,用户的age有被更新掉,说民事务没有生效,反之事务生效了。

package com.huang.spring.practice.transaction.service;

import com.huang.spring.practice.user.dao.UserMapper;
import com.huang.spring.practice.user.dto.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PathVariable;

@Service
public class TransactionService {

    @Autowired
    private UserMapper userMapper;

    public void updateUserAgeById(long userId, short age) {
        this.updateUserAgeByIdTransactional(userId, age);
    }

    @Transactional
    private void updateUserAgeByIdTransactional(long userId, short age) {
        User user = new User();
        user.setId(userId);
        user.setAge(age);
        userMapper.updateByPrimaryKeySelective(user);
        throw new RuntimeException();
    }

}
复制代码

查看数据库的user表,id=1的用户“小明”的age是11:

SpringBoot事务使用及注意事项

启动应用:

在IDEA内启动应用去测试的话,@Transactional注解的方法事务还是没有生效,推测IDEA拿来启动应用的那份“代码”没有经过AspectJ的编译期织入,平时聪明智能的IDEA在这个时候犯了傻。

所以我们要自己使用maven命令构建项目打出jar包,在maven构建我们项目的compile阶段的时候,会根据我们我们在pom文件中的配置,调用aspectj-maven-plugin,进行编译期织入:

maven package
复制代码

maven构建完成之后,在项目的target目录下生成我们springboot应用的jar包:

SpringBoot事务使用及注意事项

为了验证AspectJ已经将事务织入到使用了@Transactional的方法上,我们可以用反编译工具来反编译我们刚刚打出来的jar包,看看TransactionService.java是否已经被织入了。反编译工具,我使用的是“java-decompiler”,还是挺好用的,大家有兴趣可以去他们的 官网 下载来玩玩看。

反编译应用包之后可以看到TransactionService.java经过AspectJ插件处理之后生成了三个class文件

SpringBoot事务使用及注意事项

查看TransactionService.class的代码如下:

package com.huang.spring.practice.transaction.service;

import com.huang.spring.practice.user.dao.UserMapper;
import com.huang.spring.practice.user.dto.User;
import java.io.PrintStream;
import org.aspectj.lang.JoinPoint.StaticPart;
import org.aspectj.runtime.internal.Conversions;
import org.aspectj.runtime.reflect.Factory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.aspectj.AbstractTransactionAspect;
import org.springframework.transaction.aspectj.AnnotationTransactionAspect;

@Service
public class TransactionService
{
@Autowired
private UserMapper userMapper;
private static final JoinPoint.StaticPart ajc$tjp_0;
private static final JoinPoint.StaticPart ajc$tjp_1;

private static void ajc$preClinit()
{
  Factory localFactory = new Factory("TransactionService.java", TransactionService.class);ajc$tjp_0 = localFactory.makeSJP("method-execution", localFactory.makeMethodSig("1", "testTransactionModifyDto", "com.huang.spring.practice.transaction.service.TransactionService", "", "", "", "com.huang.spring.practice.user.dto.User"), 17);ajc$tjp_1 = localFactory.makeSJP("method-execution", localFactory.makeMethodSig("1", "updateUserAgeByIdTransactional", "com.huang.spring.practice.transaction.service.TransactionService", "long:short", "userId:age", "", "void"), 38);
}
 
public void updateUserAgeById(long userId, short age)
{
  updateUserAgeByIdTransactional(userId, age);
  updateUserAgeByIdTransactional(userId, age);
}

@Transactional
public void updateUserAgeByIdTransactional(long userId, short age)
{
  long l = userId;
  short s = age;
  Object[] arrayOfObject = new Object[3];
  arrayOfObject[0] = this;
  arrayOfObject[1] = Conversions.longObject(l);
  arrayOfObject[2] = Conversions.shortObject(s);
      AnnotationTransactionAspect.aspectOf().ajc$around$org_springframework_transaction_aspectj_AbstractTransactionAspect$1$2a73e96c(this, new TransactionService.AjcClosure3(arrayOfObject), ajc$tjp_1);
}

static final void updateUserAgeByIdTransactional_aroundBody2(TransactionService ajc$this, long userId, short age)
{
  User user = new User();
  user.setId(Long.valueOf(userId));
  user.setAge(Short.valueOf(age));
  ajc$this.userMapper.updateByPrimaryKeySelective(user);
  throw new RuntimeException();
}

static {}
}

复制代码

可以看到TransactionService.class中的代码已经被AspectJ织入了,正如上文所说的,AspectJ正是使用了“动态生成class字节码”的技术,来帮我们在代码中指定的位置上自动修改生成class字节码,按照我们的期望“增强”代码。这确实释放了我们不少人力和减弱了开发难度,如果上面AspectJ自动生成的代码要让我们自己来手动来写的话,那可要累死了。

好,现在让我们来用打出来的jar包启动我们的应用。我们不在IDEA中启动项目,直接在本地电脑上使用java命令启动我们刚刚用maven构建出来的应用包:

java -jar spring.practice-0.0.1-SNAPSHOT.jar
复制代码

应用启动后,调用接口将“小明”的age更新成66,接口返回500,因为我们接口内是有抛出RuntimeException的,下面就再去查看下数据库的user表,看看“小明”的age是否有被更新吧,从而能知道我们的@Transactional注解是否有生效,

SpringBoot事务使用及注意事项

bingo~,刷新了user表的数据,小明的age还是11,说明我们的@Transactional注解生效了,AspectJ编译期注入的方式来支持的@Transactional注解的路子走通了,我们以后给方法添加@Transactiona就不用考虑方法的访问权限(private)以及调用该方法时是否是类内部自调用了!

SpringBoot事务使用及注意事项

2.3要注意的坑

在使用Springboot+MyBatis+事务(@Transactional)的过程中,发现有个小坑:

在Spring事务中,从数据库查询某条数据,返回这条数据的java对象,这个java对象的A成员变量的值为a,然后修改这个java对象的A成员变量的值为b,但是修改完之后,不将修改后的结果更新到数据库中。然后在同一个事务中,再次查询这条数据,返回的这条数据的java对象的A成员变量的值居然是之前修改后的值b。具体测试代码如下:

2.3.1.1场景再现

  1. 在上文中提到的TransactionController增加一个接口testTransactionModifyDto

    package com.huang.spring.practice.transaction.controller;
    
    import com.huang.spring.practice.transaction.service.TransactionService;
    import com.huang.spring.practice.user.dao.UserMapper;
    import com.huang.spring.practice.user.dto.User;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.transaction.annotation.Transactional;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/api/transaction")
    public class TransactionController {
    
        @Autowired
        private TransactionService transactionService;
    
        @PostMapping("/testTransactionModifyDto")
        public User testTransactionModifyDto() {
    
            transactionService.testTransactionModifyDto();
    
            User user2 = userMapper.selectByPrimaryKey(4L);
            System.out.println("在事务外,从DB查询id为4的用户,然后打印他的age  : " + user2.getAge());
    
            return user2;
        }
    
    }
    复制代码
    1. 创建TransactionService,在其中添加被事务@Transactional注解的testTransactionModifyDto()方法

      package com.huang.spring.practice.transaction.service;
      
      import com.huang.spring.practice.user.dao.UserMapper;
      import com.huang.spring.practice.user.dto.User;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.stereotype.Service;
      import org.springframework.transaction.annotation.Transactional;
      
      @Service
      public class TransactionService {
      
          @Autowired
          private UserMapper userMapper;
      
          @Transactional
          public User testTransactionModifyDto() {
              User user = userMapper.selectByPrimaryKey(4L);
      
              System.out.println("在事务中,从DB查询id为4的用户,然后打印他的age : " + user.getAge());
      
              user.setAge((short) 80);
              System.out.println("在事务中,将id为4的用户的age设置为80, 不执行update命令将其更新到数据库中。");
      
      
              user = userMapper.selectByPrimaryKey(4L);
              System.out.println("在事务中,从DB查询id为4的用户,然后再打印他的age  : " + user.getAge());
      
              return user;
          }
      
      }
      复制代码
    2. 调用/api/transaction/testTransactionModifyDto接口,接口调用过程中打印的日志如下:

    在事务中,从DB查询id为4的用户,然后打印他的age : 13
    在事务中,将id为4的用户的age设置为80, 不执行update命令将其更新到数据库中。
    在事务中,从DB查询id为4的用户,然后再打印他的age  : 80
    在事务外,从DB查询id为4的用户,然后打印他的age  : 13
    复制代码

2.3.2.2.问题定位

啥也不说了,debug就完事了。

先debug进去红框中的这一行代码:

SpringBoot事务使用及注意事项

一行行debug进来,发现在MyBatis的BaseExecutor.java代码的query方法中,图中1处,会去localCache中来尝试获取当前sql的执行结果,如果有当前sql的执行结果,则返回当前的结果(list)键见图中2处,如果list为null,则执行图中3处的queryFromDatabase方法在数据库中执行sql查询结果。

SpringBoot事务使用及注意事项

我们再来看下这个用来查询缓存的CacheKey中存放了什么东西

SpringBoot事务使用及注意事项
SpringBoot事务使用及注意事项

如上图所示,这个cachekey中存放了要执行的sql的Mapper信息、sql语句以及sql的入参。所以大胆猜测:

Mybatis会在一个sql查询过之后,就会使用这个sql的“Mapper信息、sql语句以及sql的入参”作为缓存的key,将这个sql的执行结果缓存起来,然后再下一次执行的时候,如果有这个sql的执行结果缓存就直接拿来使用。

不过localCache里面保存的sql执行结果缓存肯定只是在一个一定的作用域里面生校的,否则在应用运行的过程中,我们每次执行下面这个sql,查询id=4的用户的信息,返回的查询结果如果都从localCache中获取,那每次的查询结果都会一样,这就乱了套了,所以某个sql在localCache中保存的执行结果缓存一定是在一个有限的作用域中生效的。

User user = userMapper.selectByPrimaryKey(4L);
复制代码

接下来我们就要搞明白localCache里的缓存是什么时候添加,以及什么时候被删除的。

我们可以看到localCache是一个叫做PerpetualCache的一个实例

SpringBoot事务使用及注意事项

进入到PerpatualCache类中,可以看到,缓存是保存在其中的名字叫做cache的HashMap中

SpringBoot事务使用及注意事项

我们在PerpatualCache类中操作cache变量的三个方法putObject、removeObject、clear上添加断点,然后重新再来debug一下,还是先debug下图中的红框的这个sql查询的过程。

SpringBoot事务使用及注意事项

Debug到PerpatualCache类的putObject方法时,我们查看到方法的调用栈,可以很清楚的看到,在执行完了BaseExecutor的queryFromDatabase方法之后,就会将从db查询到的结果保存到localCache(PrepetualCache)中

接着继续debug到下图中的第二个红框(和第一个红框中的sql是一样的),在这期间并没有调用到PerpatualCache的clear方法,说明第一个红框中的查询结果的缓存还被保存在PerpatualCache中,我们继续debug进入到下图中的第二个红框中

SpringBoot事务使用及注意事项

第二个红框中的sql查询,如我们上面预料的一样,直接从localCache中拿到了第一个红框中的查询结果,并返回,

SpringBoot事务使用及注意事项

到这里就能解释上文中提到的这个现象了:

在Spring事务中,从数据库查询某条数据,返回这条数据的java对象,这个java对象的A成员变量的值为a,然后修改这个java对象的A成员变量的值为b,但是修改完之后,不将修改后的结果更新到数据库中。然后在同一个事务中,再次查询这条数据,返回的这条数据的java对象的A成员变量的值居然是之前修改后的值b。

简答的画个图来描述下:

①:从数据库中查询到id=4的user数据,在JVM的heap上开辟一块内存空间来存放这个user数据。

②③:从数据库中查出数据之后,mybatis的将结果缓存到PrepetualCache(localCache)中,PrepetualCache中有指向user数据所在的内存地址的指针。

④:testTransactionModifyDto方法中的user变量指向第①步中从数据库里查出来并存放在heap中的user数据的内存地址。

⑤:用user变量将heap中的user数据的age改成80

⑥⑦:还在同一个事务中,事务还未被提交,所以当前线程的PrepetualCache中的缓存还未被清空,执行同一个sql,从PrepetualCache中获取到上一次查询到的user数据在heap中的内存地址,testTransactionModifyDto方法中的user变量再次指向这个内存地址

SpringBoot事务使用及注意事项

然后继续debug,从事务里面的这个方法出来

SpringBoot事务使用及注意事项

会调用到PerpetualCache的clear方法(机智的我们提前就在这打好了断点),清空所有的缓存。查看方法的调用栈可以看到调用了很多类的commit方法,是因为事务方法执行结束了,spring要将事务期间的sql提交到数据库中,这样我们在事务期间内的数据操作才会最终落到DB上。

SpringBoot事务使用及注意事项

其实这个debug过程中还有很多东西可以讲,比如PrepetualCache是属于Mybatis框架的东西,但是,当属于Spring框架的事务结束之后,却会去调用Mybatis框架的PrepetualCache的clear方法,这里让Spring框架的代码调用Mybatis框架的代码是如何实现的呢?(针对这个问题我特意debug了下,发现是通过Mybatis的SqlSessionUtils和Spring的TransactionSynchronizationManager、TransactionSynchronizationUtils实现的,具体的过程要写成文字表达出来有点繁琐吃力。大伙儿有兴趣可以debug下看看,可以发现Spring的事务为了能让其他持久层框架整合进来,是提供一个接口TransactionSynchronization,第三方的持久层框架实现这个接口,并将自己的实现注册到Spring的TransactionSynchronizationManager中的synchronizations里面,这样Spring就可以通过第三方的持久层框架来处理事务里。类似的做法,只要留心注意,就不难发现很多项目软件都会采用。通过提供接口的方式,来将各种情况下的实现和框架代码解耦,然后根据实际的需要,往框架中注册相应的实现,这个编码的技巧(思想)我们可以多多体会,对于帮忙我们构建健壮、高可维护的项目是很有帮助的。)

2.3.2.3.处理方式

如果想避免mybatis的localCache带来的影响,让同一个SqlSession中sql(statment)的执行结果不被localCache缓存,可以将mybatis的localCacheScope设置为STATEMENT,详见 myatbis官方文档 :

Setting Description Valid Values Default
localCacheScope MyBatis uses local cache to prevent circular references and speed up repeated nested queries. By default (SESSION) all queries executed during a session are cached. If localCacheScope=STATEMENT local session will be used just for statement execution, no data will be shared between two different calls to the same SqlSession. SESSION | STATEMENT SESSION

具体配置如下(这里使用的是Springboot配置文件的方式配置mybatis的配置,大伙儿也可以用Java代码的方式来配置):

mybatis:
  configuration:
    local-cache-scope: statement
复制代码

亲测这样子设置了之后,localCache就不生效了。

如果各位不想将mybatis的localCache的作用域设置成statement,又想避免本文中2.3.1.1章节所描述的场景,则可以使用对象深拷贝的方式,具体代码如下:

@Transactional
public User testTransactionModifyDto() {
    User user = userMapper.selectByPrimaryKey(4L);

    System.out.println("在事务中,从DB查询id为4的用户,然后打印他的age : " + user.getAge());
        /**
         * 使用对象字节流的方式进行对象的深拷贝,
         * 具体实现的代码不用自己写,网上很多开源的工具包可以拿来直接用,
         * 我这里用的ObjectUtil是来自是hutool这个工具包,官网地址:https://www.hutool.cn/
         *
         * ObjectUtil.cloneByStream具体的源码很简单,实际上就是对ObjectOutputStream和ObjectIputStream的使用
         *
         * 需要注意的一点是用字节流的方式进行深拷贝的话,被拷贝的对象必须实现了Serializable接口,
         * 否则无法进行序列化、反序列化,拷贝会失败。
         */
    user = ObjectUtil.cloneByStream(user);

    user.setAge((short) 80);
    System.out.println("在事务中,将id为4的用户的age设置为80, 不执行update命令将其更新到数据库中。");
    
    user = userMapper.selectByPrimaryKey(4L);
    System.out.println("在事务中,从DB查询id为4的用户,然后再打印他的age  : " + user.getAge());

    return user;
}
复制代码

修改后打印的结果为:

在事务中,从DB查询id为4的用户,然后打印他的age : 13
在事务中,将id为4的用户的age设置为80, 不执行update命令将其更新到数据库中。
在事务中,从DB查询id为4的用户,然后再打印他的age  : 13
在事务外,从DB查询id为4的用户,然后打印他的age  : 13
复制代码
原文  https://juejin.im/post/5f1699e7f265da22ec609fe3
正文到此结束
Loading...