连接:SpringBoot事务使用及注意事项 - 掘金
SpringBoot事务使用及注意事项
2020年07月21日 15:41 · 阅读 4054
关注
0.前言
感谢你百忙之中抽出时间阅读我这篇笔记。如果有错误的地方,劳烦批评指正。如果有地方和我持不同意见,很高兴和你一起探讨。最后,如果觉得这篇笔记对你有帮助的话,麻烦点个赞,谢谢~
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.配置
-
pom中添加spring-tx依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.2.7.RELEASE</version> </dependency> 复制代码
-
通过注解的方式(也可以通过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.测试
-
创建测试用的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; } } 复制代码
-
先调用/api/transaction/testTransactionThrowExcep接口
由于我们在testTransactionThrowExcep接口最后抛出了一个RuntimeException,所以接口返回500.
因为有异常抛出,所以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) 复制代码
- 再调用/api/transaction/testTransactionNoExcep接口,接口成功执行,返回200HTTP状态码以及往数据库中插入的新用户“小李”的信息:
查看数据库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中是无法被直接调用的:
所以动态代理也就没法代理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:
启动应用:
在IDEA内启动应用去测试的话,@Transactional注解的方法事务还是没有生效,推测IDEA拿来启动应用的那份“代码”没有经过AspectJ的编译期织入,平时聪明智能的IDEA在这个时候犯了傻。
所以我们要自己使用maven命令构建项目打出jar包,在maven构建我们项目的compile阶段的时候,会根据我们我们在pom文件中的配置,调用aspectj-maven-plugin,进行编译期织入:
maven package 复制代码
maven构建完成之后,在项目的target目录下生成我们springboot应用的jar包:
为了验证AspectJ已经将事务织入到使用了@Transactional的方法上,我们可以用反编译工具来反编译我们刚刚打出来的jar包,看看TransactionService.java是否已经被织入了。反编译工具,我使用的是“java-decompiler”,还是挺好用的,大家有兴趣可以去他们的官网下载来玩玩看。
反编译应用包之后可以看到TransactionService.java经过AspectJ插件处理之后生成了三个class文件
查看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注解是否有生效,
bingo~,刷新了user表的数据,小明的age还是11,说明我们的@Transactional注解生效了,AspectJ编译期注入的方式来支持的@Transactional注解的路子走通了,我们以后给方法添加@Transactiona就不用考虑方法的访问权限(private)以及调用该方法时是否是类内部自调用了!
2.3要注意的坑
在使用Springboot+MyBatis+事务(@Transactional)的过程中,发现有个小坑:
在Spring事务中,从数据库查询某条数据,返回这条数据的java对象,这个java对象的A成员变量的值为a,然后修改这个java对象的A成员变量的值为b,但是修改完之后,不将修改后的结果更新到数据库中。然后在同一个事务中,再次查询这条数据,返回的这条数据的java对象的A成员变量的值居然是之前修改后的值b。具体测试代码如下:
2.3.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; } } 复制代码
-
创建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; } } 复制代码
-
调用/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进去红框中的这一行代码:
一行行debug进来,发现在MyBatis的BaseExecutor.java代码的query方法中,图中1处,会去localCache中来尝试获取当前sql的执行结果,如果有当前sql的执行结果,则返回当前的结果(list)键见图中2处,如果list为null,则执行图中3处的queryFromDatabase方法在数据库中执行sql查询结果。
我们再来看下这个用来查询缓存的CacheKey中存放了什么东西
如上图所示,这个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的一个实例
进入到PerpatualCache类中,可以看到,缓存是保存在其中的名字叫做cache的HashMap中
我们在PerpatualCache类中操作cache变量的三个方法putObject、removeObject、clear上添加断点,然后重新再来debug一下,还是先debug下图中的红框的这个sql查询的过程。
Debug到PerpatualCache类的putObject方法时,我们查看到方法的调用栈,可以很清楚的看到,在执行完了BaseExecutor的queryFromDatabase方法之后,就会将从db查询到的结果保存到localCache(PrepetualCache)中
接着继续debug到下图中的第二个红框(和第一个红框中的sql是一样的),在这期间并没有调用到PerpatualCache的clear方法,说明第一个红框中的查询结果的缓存还被保存在PerpatualCache中,我们继续debug进入到下图中的第二个红框中
第二个红框中的sql查询,如我们上面预料的一样,直接从localCache中拿到了第一个红框中的查询结果,并返回,
到这里就能解释上文中提到的这个现象了:
在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变量再次指向这个内存地址
然后继续debug,从事务里面的这个方法出来
会调用到PerpetualCache的clear方法(机智的我们提前就在这打好了断点),清空所有的缓存。查看方法的调用栈可以看到调用了很多类的commit方法,是因为事务方法执行结束了,spring要将事务期间的sql提交到数据库中,这样我们在事务期间内的数据操作才会最终落到DB上。
其实这个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
复制代码