记录一次mybatis缓存和事务传播行为导致ut挂的排查过程
起因
rhea项目有两个ut一直都是挂的,之前也经过几个同事排查过,但是都没有找到解决办法,慢慢的这个问题就搁置了。因为之前负责rhea项目的同事离职,我临时接手了这个项目,刚好最近来了一个新同事在做新的功能开发的时候遇到了这个问题,于是我就接了一个锅,最终证明这个锅很好玩。
rhea是一个典型的使用mybatis orm的springboot项目,我们使用h2内存数据库做单元测试,每个单元测试都在一个事务内,都由Transactional进行注解。testGetBGWechatAccountByOpenid这个ut的核心调用链如下
调用深度较深,并且有多处使用到了事务,其中BasePlatformUserService.insert
这个方法用到了Propagation.REQUIRES_NEW
,也就是图中最右边的这个链路中最终插入了一个PlatformUser
ut代码如下:
@Test
@Transactional
public void testGetBGWechatAccountByOpenid() {
OpenidRo openidRo = OpenidRo.builder()
.openid(openidZmall)
.appId(appIdZmall)
.unionid(unionid)
.openAppId(openAppId)
.platformCategory(PlatformCategoriesEnum.Zmall.getValue())
.service(ServicesEnum.Server.getValue())
.serviceBusinessGroupId(serviceBusinessGroup2)
.alived(false)
.build();
RheaAccount rheaAccount = platformUserService.getAccountByOpenId(openidRo);
Assert.assertEquals(rheaAccount.getPhone(), phone2);
RheaPlatformUser platformUser = platformUserMapper.getByOpenIdAndBG(
openidZmall, appIdZmall, serviceBusinessGroup2, ServicesEnum.Server.getValue());
Assert.assertEquals(rheaAccount.getId(), platformUser.getAccountId());
}
但是在ut里面使用getByOpenIdAndBG查询platformUser却是null导致最终platformUser.getAccountId()这个方法抛出了NPE。
知识储备
排查这个问题会用到以下两个知识点
- 事务传播行为-Propagation
- mybatis缓存
- 事务和mybatis Session的关联
事务传播行为
Springboot的Transactional的实现包含两部分,一个部分是事务传播行为,一个部分是数据库隔离级别,代码如下:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";
@AliasFor("value")
String transactionManager() default "";
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
...
数据库隔离级别默认是Isolation.DEFAULT,也就是使用数据库自身的隔离级别,Mysql的默认隔离级别是REPEATABLE_READ可重复读,Oracle的默认事务隔离级别是读已提交READ_COMMITTED。具体的隔离级别不在此讨论。我们需要关注事务的传播行为,也就是Propagation。Propagation实现如下:
public enum Propagation {
REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);
private final int value;
private Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
这里我们只是用到了REQUIRED和REQUIRED_NEW,REQUIRED也是默认的传播行为,这两个传播行为的区别在于:
- REQUIRED:默认的spring事务传播级别,使用该级别的特点是,如果上下文中已经存在事务,那么就加入到事务中执行,如果当前上下文中不存在事务,则新建事务执行。所以这个级别通常能满足处理大多数的业务场景。
- REQUIRED_NEW:从字面即可知道,new,每次都要一个新事务,该传播级别的特点是,每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
- 只有在被调用方法中的数据库操作需要保存到数据库中,而不管覆盖事务的结果如何时,才应该使用
REQUIRES_NEW
事务属性 - 举个栗子:假设尝试的所有股票交易都必须被记录在一个审计数据库中。出于验证错误、资金不足或其他原因,不管交易是否失败,这条信息都需要被持久化。如果没有对审计方法使用
REQUIRES_NEW
属性,审计记录就会连同尝试执行的交易一起回滚。使用REQUIRES_NEW
属性可以确保不管初始事务的结果如何,审计数据都会被保存
- 只有在被调用方法中的数据库操作需要保存到数据库中,而不管覆盖事务的结果如何时,才应该使用
mybatis缓存
Mybatis-config.xml中可以配置mybatis的本地缓存范围localCacheScope。
mybatis官网解释:MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。 默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据。
用白话解释:
- SESSION范围的缓存:在同一个SqlSession中多次查询会缓存的mapper中的方法,经过验证,key是单个查询方法
- 连续查询则后续的查询会使用第一个查询的缓存结果——debug时无法找到查询的sql日志
- 间断的查询则会实际执行每个查询操作——可以找到每个查询的sql日志
- 连续的定义是:在当前Session中执行DML操作,或者开启了其他Session执行了DML操作,都认为是连续
- STATEMENT范围的缓存:本质是不使用缓存
在新版本的mysql中数据库自身有自己的缓存,我们并不需要Mybatis的缓存,而且Mybatis不是最底层的缓存,因为多个Session的存在,往往导致一些问题。
修改mybatis的默认缓存范围可以在Mybatis-config.xml中加入以下配置:
<!--设置缓存作用域,它决定是否使用mybatis的缓存。 系统默认值是SESSION,为了不使用mybatis缓存,设置为STATEMENT -->
<setting name="localCacheScope" value="STATEMENT"/>
使用以下配置可以打印出mybatis执行时的操作log和sql语句:
<setting name="logImpl" value="STDOUT_LOGGING" />
事务和mybatis Session的关联
开启一个新的事务并且在新的事务中首次执行mybatis操作时会开启新的mybatis Session,因此在REQUIRES_NEW
中执行mybatis操作一定会开启新的Session
排查过程
- 确保mapper方法对应的sql是对的
- 将使用
REQUIRES_NEW
的方法改为默认的REQUIRED
,发现能查询到platformUser - 在ut中使用其他方法查询插入的platformUser,发现能查询到
- mybatis配置加上日志,debug发现ut中的查询platformUserMapper.getByOpenIdAndBG发现没有打印sql
- 猜测可能是查询是用了mybatis缓存,取消缓存发现可以查询到真实记录
- 分析:
REQUIRES_NEW
开启的新事务中开启的新Session插入的记录并没有打破老Session缓存的查询结果,因此在老Session中使用相同的查询语句是查询不到真实记录的
具体的debug日志如下:
红框中的就是最外层的事务开启的老session,绿色框是中间REQUIRES_NEW
新事务中开启的新Session。所以对于红框这个Session而言,它并不知道已经发生了DML操作,因此在后续继续查询时会使用最开始的查询结果,也就是null。
这种问题通常发生在getOrCreate操作中。
解决
去掉Mybatis层面的缓存
<!--设置缓存作用域,它决定是否使用mybatis的缓存。 系统默认值是SESSION,为了不使用mybatis缓存,设置为STATEMENT -->
<setting name="localCacheScope" value="STATEMENT"/>
解决这个问题对于REQUIRES_NEW
这个传播行为的理解就更深刻了。
参考:
- 了解事务陷阱:https://www.ibm.com/developerworks/cn/java/j-ts1.html
- Spring五个事务隔离级别和七个事务传播行为:https://blog.csdn.net/caoxiaohong1005/article/details/79984912
- Innodb中的事务隔离级别和锁的关系:https://tech.meituan.com/2014/08/20/innodb-lock.html
- Mybatis XML配置:https://mybatis.org/mybatis-3/zh/configuration.html
记得帮我点赞哦!
精心整理了计算机各个方向的从入门、进阶、实战的视频课程和电子书,按照目录合理分类,总能找到你需要的学习资料,还在等什么?快去关注下载吧!!!
念念不忘,必有回响,小伙伴们帮我点个赞吧,非常感谢。
我是职场亮哥,YY高级软件工程师、四年工作经验,拒绝咸鱼争当龙头的斜杠程序员。
听我说,进步多,程序人生一把梭
如果有幸能帮到你,请帮我点个【赞】,给个关注,如果能顺带评论给个鼓励,将不胜感激。
职场亮哥文章列表:更多文章
本人所有文章、回答都与版权保护平台有合作,著作权归职场亮哥所有,未经授权,转载必究!
记录一次mybatis缓存和事务传播行为导致ut挂的排查过程的更多相关文章
- (四)mybatis缓存、事务、插件的基本知识
mybatis缓存.事务.插件的基础 一.缓存 (一)一级缓存与二级缓存 一级缓存 为了获得更好的性能,最重要的就是一级缓存.每个session对象维持一个一级缓存,session对象创建时缓存创建, ...
- 一天十道Java面试题----第五天(spring的事务传播机制------>mybatis的优缺点)
这里是参考B站上的大佬做的面试题笔记.大家也可以去看视频讲解!!! 文章目录 41.spring的事务传播机制 42 .spring事务什么时候会失效 43 .什么的是bean的自动装配.有哪些方式? ...
- 事务传播性、隔离性与MVCC
一.事务传播性 1.1 什么是事务的传播性 事务的传播性一般在事务嵌套时候使用,比如在事务A里面调用了另外一个使用事务的方法,那么这俩个事务是各自作为独立的事务执行提交,还是内层的事务合并到外层的事务 ...
- MyBatis6:MyBatis集成Spring事务管理(下篇)
前言 前一篇文章<MyBatis5:MyBatis集成Spring事务管理(上篇)>复习了MyBatis的基本使用以及使用Spring管理MyBatis的事务的做法,本文的目的是在这个的基 ...
- MyBatis(6):MyBatis集成Spring事务管理(下)
前一篇文章复习了MyBatis的基本使用以及使用Spring管理MyBatis的事务的做法,本文的目的是在这个的基础上稍微做一点点的进阶:多数据的事务处理.文章内容主要包含两方面: 1.单表多数据的事 ...
- Mybatis:缓存,动态SQL,注解SQL以及动态标签使用
1 转义字符 字符 转义 描述 < < 小于 <= <= 小于等于 > > 大于 >= >= 大于等于 <> <> 不等于 &a ...
- MyBatis学习总结(四)——MyBatis缓存与代码生成
一.MyBatis缓存 缓存可以提高系统性能,可以加快访问速度,减轻服务器压力,带来更好的用户体验.缓存用空间换时间,好的缓存是缓存命中率高的且数据量小的.缓存是一种非常重要的技术. 1.0.再次封装 ...
- 【转】MaBatis学习---源码分析MyBatis缓存原理
[原文]https://www.toutiao.com/i6594029178964673027/ 源码分析MyBatis缓存原理 1.简介 在 Web 应用中,缓存是必不可少的组件.通常我们都会用 ...
- 聊聊MyBatis缓存机制【美团-推荐】
聊聊MyBatis缓存机制 2018年01月19日 作者: 凯伦 文章链接 18778字 38分钟阅读 前言 MyBatis是常见的Java数据库访问层框架.在日常工作中,开发人员多数情况下是使用My ...
随机推荐
- UOJ 422 [集训队作业2018] 小Z的礼物 min-max容斥 期望 轮廓线dp
LINK:小Z的礼物 太精髓了 我重学了一遍min-max容斥 重写了一遍按位或才写这道题的. 还是期望多少时间可以全部集齐. 相当于求出 \(E(max(S))\)表示最后一个出现的期望时间. 根据 ...
- C++程序员容易走入性能优化误区!对此你怎么看呢?
有些C++ 程序员,特别是只写C++ 没有写过 Python/PHP 等慢语言的程序员,容易对性能有心智负担,就像着了魔一样,每写3 行代码必有一行代码因为性能考虑而优化使得代码变形(复杂而晦涩). ...
- 二维线段树->树套树
现在上真正的二维线段树 毕竟 刚刚那个是卡常 过题我们现在做一个更高级的做法二维线段树. 大体上维护一颗x轴线段树 然后在每个节点的下方再吊一颗维护y轴的线段树那么此时我们整个平面就被我们玩好了. 这 ...
- Maven知识记录(一)初识Maven私服
Maven知识记录(一)初识Maven私服 什么是maven私服 私服即私有的仓库.maven把存放文件的地方叫做仓库,我们可以理解成我门家中的储物间.而maven把存放文件的具体位置叫做坐标.我们项 ...
- 8月1日起全部无版号游戏下架,ios手游想上架看这里!
在苹果至中国游戏开发者的邮件中声明:如果开发者不能在7月31日前提交版号及相关文件,付费游戏将不可以在中国AppStore供应.也就是说: 从8月1日开始,苹果将正式下架全部.所有的ios付费 ...
- OpenCL 增强单work-item kernel性能策略
1.基于反馈的Optimization Report解决单个Work-item的Kernel相关性 在许多情况下,将OpenCL™应用程序设计为单个工作项内核就足以在不执行其他优化步骤的情况下最大化性 ...
- 029_go语言中的非阻塞通道
代码演示 package main import "fmt" func main() { messages := make(chan string) signals := make ...
- Java连接Redis,存储对象获取对象()byte和json),连接池
Java连接Redis Jedis连接Redis,Lettuce连接Redis Jedis连接Redis 1. 创建maven项目 2. 引入依赖 <dependencies> <d ...
- Socket 模拟HTTP客户端请求
import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import ja ...
- C#算法设计查找篇之03-插值查找
插值查找(Interpolation Search) 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/701 访问. 插值 ...