百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

SpringBoot 下 Mybatis 的缓存(springboot mybatis二级缓存)

toyiye 2024-09-07 20:26 4 浏览 0 评论

“IT魔幻屋”致力于让你遇见更好的自己!

说起 mybatis,作为 Java 程序员应该是无人不知,它是常用的数据库访问框架。与 Spring 和 Struts 组成了 Java Web 开发的三剑客— SSM。当然随着 Spring Boot 的发展,现在越来越多的企业采用的是 SpringBoot + mybatis 的模式开发,我们公司也不例外。而 mybatis 对于我也仅仅停留在会用而已,没想过怎么去了解它,更不知道它的缓存机制了,直到那个生死难忘的 BUG。故事的背景比较长,但并不是啰嗦,只是让读者知道这个 BUG 触发的场景,加深记忆。在遇到类似问题时,可以迅速定位。


先说下故事的前提,为了防止用户在动态中输入特殊字符,用户的动态都是编码后发到后台,而后台在存入到 DB 表之前会解码以方便在 DB 中查看以及上报到搜索引擎。在查询用户动态的时候先从 DB 表中读取并在后台做一次编码再传到前端,前端再解码就可以正常展示了。流程如下图:

有一天后端预发环境发布完毕后,用户的动态页面有的动态显示正常,而有的却是被编码过的。看到现象后的第一个反应就是有问题的动态被编码了两次,但是编码操作只会在 service 层的 findById 中有。理论不会在上层犯这种低级错误。话不多说便开始排查新增加的代码,发现只要进入了新增加代码中的某个 if 分支则被编码了两次。分支中除了再次调用 findById(必要性不讨论),也无其他特殊代码了。百思不得其解后请教了旁边的老司机,老司机说可能是 mybatis 缓存。于是看了下我代码,将编码的操作从 findById 中移出来后再次发布到预发,正常了,心想老司机不愧是老司机。本次 BUG 触发的有两个条件需要注意:

  • 整个操作过程都在一个函数中,而函数上面加了 @Transactional 的注解(对 mybatis 来说是在同一个 SESSION 中)
  • 一般只会调用 findByIdy 一次,如果进入分支则会调用两次 (第一次调用后做了编码后被缓存,第二次从缓存读后继续被编码)

便开始谷歌 mybatis 的缓存机制,搜到了一篇非常不错的文章《 聊聊 mybatis 的缓存机制 》,推荐大家看一下。但是这篇文章讲到了源码,涉及的比较深。而且并没讲 SpringBoot 下 mybatis 下的缓存知识点,遂作此篇,以作补充。

缓存的配置

SpringBoot + mybatis 环境搭建很简单而且网上一堆教程,这里不班门弄斧了,记得在项目中将 mytatis 的源码下载下来即可。mybaits 一共有两级缓存:一级缓存的配置 key 是 localCacheScope,而二级缓存的配置 key 是 cacheEnabled,从名字上可以得出以下信息:

  • 一级缓存是本地或者说局部缓存,它不能被关闭,只能配置缓存范围。SESSION 或者 STATEMENT。

  • 二级缓存才是 mybatis 的正统,功能会更强大些。

先来看下在 SpringBoot中 如何配置 mybatis 缓存的相关信息。默认情况下 SpringBoot 下的 mybatis 一级缓存为 SESSION 级别,二级缓存也是打开的,可以在 mybatis 源码中的 org.apache.ibatis.session.Configuration.class 文件中看到(idea中打开),如下图:

也可以通过以下测试程序查看缓存开启情况:

@RunWith(SpringRunner.class)
@SpringBootTest
public class LearnApplicationTests {
 private SqlSessionFactory factory;
 @Before
 public void setUp() throws Exception {
 InputStream inputStream = Resources.getResourceAsStream("mybatis/mybatis-config.xml");
 factory = new SqlSessionFactoryBuilder().build(inputStream);
 }
 @Test
 public void showDefaultCacheConfiguration() {
 System.out.println("一级缓存范围: " + factory.getConfiguration().getLocalCacheScope());
 System.out.println("二级缓存是否被启用: " + factory.getConfiguration().isCacheEnabled());
 }
}

如果要设置一级缓存的缓存级别和开关二级缓存,在 mybatis-config.xml (当然也可以在 application.xml/yml 中配置)加入如下配置即可:

<settings>
 <setting name="cacheEnabled" value="true/false"/>
 <setting name="localCacheScope" value="SESSION/STATEMENT"/>
</settings>

但需要注意的是二级缓存 cacheEnabled 只是个总开关,如果要让二级缓存真正生效还需要在 mapper xml 文件中加入 。一级缓存只在同一 SESSION 或者 STATEMENT 之间共享,二级缓存可以跨 SESSION,开启后它们默认具有如下特性:

  • 映射文件中所有的 select 语句将被缓存
  • 映射文件中所有的 insert/update/delete 语句将刷新缓存

一二级缓存同时开启的情况下,数据的查询顺序是 二级缓存 -> 一级缓存 -> 数据库。一级缓存比较简单,而二级缓存可以设置更多的属性,只需要在 mapper 的 xml 文件中的 中配置即可,具体如下:

<cache
 type = "org.mybatis.caches.ehcache.LoggingEhcache" //指定使用的缓存类,mybatis默认使用HashMap进行缓存,可以指定第三方缓存
 eviction = "LRU" //默认是 LRU 淘汰缓存的算法,有如下几种:
 //1.LRU – 最近最少使用的:移除最长时间不被使用的对象。 
 //2.FIFO – 先进先出:按对象进入缓存的顺序来移除它们。 
 //3.SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。 
 //4.WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象
 flushInterval = "1000" //清空缓存的时间间隔,单位毫秒,可以被设置为任意的正整数。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。
 size = "100" //缓存对象的个数,任意正整数,默认值是1024。
 readOnly = "true" //缓存是否只读,提高读取效率
 blocking = "true" //是否使用阻塞缓存,默认为false,当指定为true时将采用BlockingCache进行封装,blocking,
 //阻塞的意思,使用BlockingCache会在查询缓存时锁住对应的Key,如果缓存命中了则会释放对应的锁,
 //否则会在查询数据库以后再释放锁这样可以阻止并发情况下多个线程同时查询数据,详情可参考BlockingCache的源码。 
/>

触发缓存

  1. 配置一级缓存为 SESSION 级别

Controller 中调用两次 getOne,代码如下:

@RequestMapping("/getUser")
public UserEntity getUser(Long id) {
 //第一次调用
 UserEntity user1=userMapper.getOne(id);
 //第二次调用
 UserEntity user2=userMapper.getOne(id);
 return user1;
}

调用: http://localhost:8080/getUser?id=1,打印结果如下:

从图中的 1/2/3/4 可以看出每次 mapper 层的一次接口调用如 getOne 就会创建一个 session,并且在执行完毕后关闭 session。所以两次调用并不在一个 session 中,一级缓存并没有发生作用。开启事务,Controller 层代码如下:

@RequestMapping("/getUser")
@Transactional(rollbackFor = Throwable.class)
public UserEntity getUser(Long id) {
 //第一次调用
 UserEntity user1=userMapper.getOne(id);
 //第二次调用
 UserEntity user2=userMapper.getOne(id);
 return user1;
}

由于在同一个事务中,虽然调用了 select 操作两次但是只执行了一次 sql ,缓存发挥了作用。这就跟一开始我遇到的那个 BUG 场景一样:同一 session 且 select 调用 > 1 次。如果在两次调用中间插入 update 操作,缓存会立即失效。只要 session 中有 insert、update 和 delete 语句,该 session 中的缓存会立即被刷新。但是注意这只是在同一 session 之间。不同 session 之间如 session1 和 session2,session1 里的 insert/update/delete 并不会影响 session 2 下的缓存,这在高并发或者分布式的情况下会产生脏数据。所以建议将一级缓存级别调成 statement。

  1. 配置一级缓存为 STATEMENT 级别

再次将(1)中的无事务和有事务的代码分别执行一遍,打印结果始终如下:

配置成 SATEMENT 后,一级缓存相当于被关闭了。STATEMENT 级别暂时不好模拟,但是我猜测 STATEMENT 级别即在同一执行 sql 的接口中(如上面的 getOne 中)缓存,出了 getOne 缓存即失效。

  1. 配置二级缓存,同时为了避免一级缓存的干扰,将一级缓存设置为 STATEMENT

Controller 中去掉 @Transactional 注解代码如下:

@RequestMapping("/getUser")
public UserEntity getUser(Long id) {
 UserEntity user1=userMapper.getOne(id);
 UserEntity user2=userMapper.getOne(id);
 return user1;
}

当然二级缓存开关保证打开,在 mapper xml 文件中加入 ,整个文件代码如下:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.binggle.learn.dao.mapper.UserMapper" >
<resultMap id="BaseResultMap" type="com.binggle.learn.dao.entity.UserEntity" >
 <id column="id" property="id" jdbcType="BIGINT" />
 <result column="name" property="name" jdbcType="VARCHAR" />
 <result column="sex" property="sex"/>
</resultMap>
<sql id="Base_Column_List" >
 id, name, sex
</sql>
<select id="getOne" parameterType="java.lang.Long" resultMap="BaseResultMap" >
 SELECT
 <include refid="Base_Column_List" />
 FROM users
 WHERE id = #{id};
</select>
<cache />
</mapper>

执行 http://localhost:8080/getUser?id=1,打印结果如下:

从图中红框可以看出第二次查询命中缓存,0.5 是命中率。再次执行

http://localhost:8080/getUser?id=1

打印结果如下:

这次一次 sql 也没执行了,缓存命中率上升到 0.75了,所以说二级缓存全局缓存。但它的缓存范围也是有限的,一级缓存在同一个 session 中。二级缓存虽然可以跨 session 但也只能在同一 namespace 中,所谓 namespace 即 mapper xml 文件。具体实验请看《聊聊 mybatis 的缓存机制》中的关于二级缓存的实验 4 和 5。再看下二级缓存配置对二级缓存的影响,为了明显的看出效果,只改如下配置:

<cache
 size="1" //一次只能缓存一个对象
 flushInterval="5000" //刷新时间为 5s
/>
/****/
@RequestMapping("/getUser")
public UserEntity getUser(Long id, Long id2) {
 //第一个对象 1
 System.out.println("================缓存对象 1=================");
 UserEntity user1 = userMapper.getOne(id);
 //另一个对象 2
 System.out.println("========缓存对象 2,剔除缓存中的对象 1=======");
 UserEntity user2=userMapper.getOne(id2);
 user2 = userMapper.getOne(id2);
 //再次读取第一个对象
 System.out.println("==========缓存被剔除,执行查询 sql===========");
 user1 = userMapper.getOne(id);
 //暂停 5s
 try {
 sleep(5000);
 }catch (Exception e){
 e.printStackTrace();
 }
 System.out.println("============5s 后再次查询对象 2=============");
 user2 = userMapper.getOne(id2);
 return user1;
}

执行 http://localhost:8080/getUser?id=1&id2=2 最后打印的结果如下:

可以看出二级缓存只能缓存一个对象且 5s 后就失效了,配置生效。缓存配置中还有一个重要的配置 type,该配置可以配置第三方的 cache,特别在高并发和分布式情况下。当然,使用更专业的分布式缓存才是王道,例如 redis 等。

关注“IT魔幻屋”,学习更多前沿技术,来这里你将遇见更好的自己!

相关推荐

# Python 3 # Python 3字典Dictionary(1)

Python3字典字典是另一种可变容器模型,且可存储任意类型对象。字典的每个键值(key=>value)对用冒号(:)分割,每个对之间用逗号(,)分割,整个字典包括在花括号({})中,格式如...

Python第八课:数据类型中的字典及其函数与方法

Python3字典字典是另一种可变容器模型,且可存储任意类型对象。字典的每个键值...

Python中字典详解(python 中字典)

字典是Python中使用键进行索引的重要数据结构。它们是无序的项序列(键值对),这意味着顺序不被保留。键是不可变的。与列表一样,字典的值可以保存异构数据,即整数、浮点、字符串、NaN、布尔值、列表、数...

Python3.9又更新了:dict内置新功能,正式版十月见面

机器之心报道参与:一鸣、JaminPython3.8的热乎劲还没过去,Python就又双叒叕要更新了。近日,3.9版本的第四个alpha版已经开源。从文档中,我们可以看到官方透露的对dic...

Python3 基本数据类型详解(python三种基本数据类型)

文章来源:加米谷大数据Python中的变量不需要声明。每个变量在使用前都必须赋值,变量赋值以后该变量才会被创建。在Python中,变量就是变量,它没有类型,我们所说的"类型"是变...

一文掌握Python的字典(python字典用法大全)

字典是Python中最强大、最灵活的内置数据结构之一。它们允许存储键值对,从而实现高效的数据检索、操作和组织。本文深入探讨了字典,涵盖了它们的创建、操作和高级用法,以帮助中级Python开发...

超级完整|Python字典详解(python字典的方法或操作)

一、字典概述01字典的格式Python字典是一种可变容器模型,且可存储任意类型对象,如字符串、数字、元组等其他容器模型。字典的每个键值key=>value对用冒号:分割,每个对之间用逗号,...

Python3.9版本新特性:字典合并操作的详细解读

处于测试阶段的Python3.9版本中有一个新特性:我们在使用Python字典时,将能够编写出更可读、更紧凑的代码啦!Python版本你现在使用哪种版本的Python?3.7分?3.5分?还是2.7...

python 自学,字典3(一些例子)(python字典有哪些基本操作)

例子11;如何批量复制字典里的内容2;如何批量修改字典的内容3;如何批量修改字典里某些指定的内容...

Python3.9中的字典合并和更新,几乎影响了所有Python程序员

全文共2837字,预计学习时长9分钟Python3.9正在积极开发,并计划于今年10月发布。2月26日,开发团队发布了alpha4版本。该版本引入了新的合并(|)和更新(|=)运算符,这个新特性几乎...

Python3大字典:《Python3自学速查手册.pdf》限时下载中

最近有人会想了,2022了,想学Python晚不晚,学习python有前途吗?IT行业行业薪资高,发展前景好,是很多求职群里严重的香饽饽,而要进入这个高薪行业,也不是那么轻而易举的,拿信工专业的大学生...

python学习——字典(python字典基本操作)

字典Python的字典数据类型是基于hash散列算法实现的,采用键值对(key:value)的形式,根据key的值计算value的地址,具有非常快的查取和插入速度。但它是无序的,包含的元素个数不限,值...

324页清华教授撰写【Python 3 菜鸟查询手册】火了,小白入门字典

如何入门学习python...

Python3.9中的字典合并和更新,了解一下

全文共2837字,预计学习时长9分钟Python3.9正在积极开发,并计划于今年10月发布。2月26日,开发团队发布了alpha4版本。该版本引入了新的合并(|)和更新(|=)运算符,这个新特性几乎...

python3基础之字典(python中字典的基本操作)

字典和列表一样,也是python内置的一种数据结构。字典的结构如下图:列表用中括号[]把元素包起来,而字典是用大括号{}把元素包起来,只不过字典的每一个元素都包含键和值两部分。键和值是一一对应的...

取消回复欢迎 发表评论:

请填写验证码