MyBatis缓存专题-一文彻底搞懂MyBatis二级缓存
Posted IT老刘
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MyBatis缓存专题-一文彻底搞懂MyBatis二级缓存相关的知识,希望对你有一定的参考价值。
文章目录
1.二级缓存概念
二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是namespace级别的,可以被多个SqlSession 共享(只要是同一个接口里面的相同方法,都可以共享),生命周期和应用同步。如果你的MyBatis使用了二级缓存,并且你的Mapper和select语句也配置使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:二级缓存 —> 一级缓存 —> 数据库。
二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是可以横跨跨SqlSession的。
示意图:
作为一个作用范围更广的缓存,它肯定是在SqlSession 的外层,否则不可能被多个SqlSession 共享。而一级缓存是在SqlSession 内部的,所以第一个问题,肯定是工作在一级缓存之前,也就是只有取不到二级缓存的情况下才到一个会话中去取一级缓存。第二个问题,二级缓存放在哪个对象中维护呢? 要跨会话共享的话,SqlSession 本身和它里面的BaseExecutor 已经满足不了需求了,那我们应该在BaseExecutor 之外创建一个对象。
实际上MyBatis 用了一个装饰器的类来维护,就是CachingExecutor。如果启用了二级缓存,MyBatis 在创建Executor 对象的时候会对Executor 进行装饰。CachingExecutor 对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接返回,如果没有委派交给真正的查询器Executor 实现类,比如SimpleExecutor 来执行查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户。
2.二级缓存使用
二级缓存区域是根据mapper的namespace划分的,相同namespace的mapper查询数据放在同一个区域,可以理解为二级缓存区域是根据mapper划分,也就是根据命名空间来划分的,如果两个mapper文件的命名空间一样,那样,不同的SqlSession之间就可以共享一个mapper缓存。
示意图:
2.1.配置二级缓存
在 mybatis 中,二级缓存有全局开关和分开关, 全局开关, 在 mybatis-config.xml
中如下配置:
<settings>
<!-- cacheEnabled是二是级缓存的总开关,置为false代表关闭二级缓存 -->
<setting name="cacheEnabled" value="true"/>
</settings>
默认是为 true, 即默认开启总开关。
2.2.分开关
由于mybaits的二级缓存是mapper范围级别,所以除了在SqlMapConfig.xml设置二级缓存的总开关外,还要在具体的mapper.xml中开启二级缓存。设置如下:
2.3.实体类实现序列化接口
![在这里插入图片描述](https://img-blog.csdnimg.cn/7cf4516b6ee14ca58ed16bcf2a2e43ca.png
2.4.测试方法
@Test
public void test2() throws Exception{
try {
String resources = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resources);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
UserMapper um1 = sqlSession1.getMapper(UserMapper.class);
User user1 = um1.findById(1);
User user2 = um1.findById(1);
System.out.println(user1==user2);
/*
这里要记住一个地方,第二次查询必须要等第一次的session关闭以后才可以。
否则二级缓存没有作用,依然还会执行二次select语句发送至数据库
*/
sqlSession1.close();
SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
UserMapper um2 = sqlSession2.getMapper(UserMapper.class);
User user3 = um2.findById(1);
System.out.println(user1==user3);
sqlSession2.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
}
}
运行结果:
以上结果, 分几个过程解释:
第一阶段:
- 在第一个 SqlSession 中, 查询出 user对象, 此时发送了 SQL 语句;
- SqlSession 再次查询出 user对象, 此时不发送 SQL 语句, 日志中打印了 「Cache Hit Ratio」, 代表二级缓存使用了, 但是没有命中。 因为一级缓存先作用了。
- 由于是一级缓存, 因此, 此时两个对象是相同的。
- 调用了 sqlSession.close(), 此时将数据序列化并保持到二级缓存中。
第二阶段:
- 新创建一个 sqlSession.close() 对象;
- 查询出 user对象,直接从二级缓存中拿了数据, 因此没有发送 SQL 语句, 此时查了 3 个对象,但只有一个命中, 因此 命中率 1/3=0.333333;
在缓存中出现了更新,二级缓存自动清除
@Test
public void test2() throws Exception{
try {
String resources = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resources);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
UserMapper um1 = sqlSession1.getMapper(UserMapper.class);
User user1 = um1.findById(1);
User user2 = um1.findById(1);
System.out.println(user1==user2);
/*
这里要记住一个地方,第二次查询必须要等第一次的session关闭以后才可以。
否则二级缓存没有作用,依然还会执行二次select语句发送至数据库
*/
sqlSession1.close();
System.out.println("====================================================");
SqlSession sqlSession3 = sqlSessionFactory.openSession(true);
UserMapper um3 = sqlSession3.getMapper(UserMapper.class);
User user3 =new User(1,"AA","2021-11-11","男","AA");
um3.updateById(user3);
sqlSession3.close();
System.out.println("====================================================");
//Thread.sleep(3000);
SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
UserMapper um2 = sqlSession2.getMapper(UserMapper.class);
User user4 = um2.findById(1);
sqlSession2.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
}
运行结果
Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
Cache Hit Ratio [com.bruce.mapper.UserMapper]: 0.0
Opening JDBC Connection
Created connection 1732502545.
==> Preparing: select * from tb_user where id=?
==> Parameters: 1(Integer)
<== Columns: id, username, birthday, sex, address
<== Row: 1, 王五, 2018-09-06, 女, 北京
<== Total: 1
Cache Hit Ratio [com.bruce.mapper.UserMapper]: 0.0
true
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6743e411]
Returned connection 1732502545 to pool.
====================================================
Opening JDBC Connection
Checked out connection 1732502545 from pool.
==> Preparing: update tb_user set username=?,birthday=?,sex=?,address=? where id=?
==> Parameters: AA(String), 2021-11-11(String), 男(String), AA(String), 1(Integer)
<== Updates: 1
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6743e411]
Returned connection 1732502545 to pool.
====================================================
Cache Hit Ratio [com.bruce.mapper.UserMapper]: 0.0
Opening JDBC Connection
Checked out connection 1732502545 from pool.
==> Preparing: select * from tb_user where id=?
==> Parameters: 1(Integer)
<== Columns: id, username, birthday, sex, address
<== Row: 1, AA, 2021-11-11, 男, AA
<== Total: 1
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6743e411]
Returned connection 1732502545 to pool.
Process finished with exit code 0
3.cache有一些可选的属性
3.1.type
type 用于指定缓存的实现类型, 默认是PERPETUAL
, 对应的是 mybatis 本身的缓存实现类org.apache.ibatis.cache.impl.PerpetualCache
。
3.2.eviction
eviction 对应的是回收策略, 默认为 LRU
。
LRU
: 最近最少使用, 移除最长时间不被使用的对象。FIFO
: 先进先出, 按对象进入缓存的顺序来移除对象。SOFT
: 软引用, 移除基于垃圾回收器状态和软引用规则的对象。WEAK
: 弱引用, 移除基于垃圾回收器状态和弱引用规则的对象
3.3.flushInterval
flushInterval
对应刷新间隔, 单位毫秒
, 默认值不设置, 即没有刷新间隔, 缓存仅仅在刷新语句时刷新。
如果设定了之后, 到了对应时间会过期, 再次查询需要从数据库中取数据。
3.4.size
size对应为引用的数量,即最多的缓存对象数据, 默认为 1024
。
3.5 readOnly
readOnly 为只读属性, 默认为 false
-
false: 可读写, 在创建对象时, 会通过反序列化得到缓存对象的拷贝。 因此在速度上会相对慢一点, 但重在安全。
-
true: 只读, 只读的缓存会给所有调用者返回缓存对象的相同实例。 因此性能很好, 但如果修改了对象, 有可能会导致程序出问题。
3.6 blocking
blocking 为阻塞, 默认值为 false。 当指定为 true 时将采用 BlockingCache
进行封装。
使用 BlockingCache
会在查询缓存时锁住对应的 Key,如果缓存命中了则会释放对应的锁,否则会在查询数据库以后再释放锁,这样可以阻止并发情况下多个线程同时查询数据。
请注意,缓存的配置和缓存实例会被绑定到 SQL 映射文件的命名空间中。 因此,同一命名空间中的所有语句和缓存将通过命名空间绑定在一起。 每条语句可以自定义与缓存交互的方式,或将它们完全排除于缓存之外,这可以通过在每条语句上使用两个简单属性来达成。 默认情况下,语句会这样来配置:
<select ... flushCache="false" useCache="true"/>
<insert ... flushCache="true"/>
<update ... flushCache="true"/>
<delete ... flushCache="true"/>
鉴于这是默认行为,显然你永远不应该以这样的方式显式配置一条语句。但如果你想改变默认的行为,只需要设置 flushCache 和 useCache 属性。比如,某些情况下你可能希望特定 select 语句的结果排除于缓存之外,或希望一条 select 语句清空缓存。类似地,你可能希望某些 update 语句执行时不要刷新缓存。
4.MyBatis的缓存机制整体设计以及二级缓存的工作模式
如上图所示,当开一个会话时,一个SqlSession
对象会使用一个Executor
对象来完成会话操作,MyBatis的二级缓存机制的关键就是对这个Executor对象做文章。如果用户配置了"cacheEnabled=true
",那么MyBatis在为SqlSession
对象创建Executor对象时,会对Executor对象加上一个装饰者:CachingExecutor
,这时SqlSession
使用CachingExecutor
对象来完成操作请求。CachingExecutor
对于查询请求,会先判断该查询请求在Application级别的二级缓存中是否有缓存结果,如果有查询结果,则直接返回缓存结果;如果缓存中没有,再交给真正的Executor对象来完成查询操作,之后CachingExecutor
会将真正Executor
返回的查询结果放置到缓存中,然后在返回给用户。
CachingExecutor
是Executor
的装饰者,以增强Executor
的功能,使其具有缓存查询的功能,这里用到了设计模式中的装饰者模式,
CachingExecutor
和Executor
的接口的关系如下类图所示:
5.使用二级缓存,必须要具备的条件
MyBatis对二级缓存的支持粒度很细,它会指定某一条查询语句是否使用二级缓存。
虽然在Mapper中配置了<cache>
,并且为此Mapper
分配了Cache
对象,这并不表示我们使用Mapper中定义的查询语句查到的结果都会放置到Cache对象之中,我们必须指定Mapper中的某条选择语句是否支持缓存,即如下所示,在<select>
节点中配置useCache="true"
,Mapper才会对此Select的查询支持缓存特性,否则,不会对此Select查询,不会经过Cache
缓存。如下所示,Select语句配置了useCache="true"
,则表明这条Select语句的查询会使用二级缓存。
<select id="selectByMinSalary" resultMap="BaseResultMap" parameterType="java.util.Map" useCache="true">
总之,要想使某条Select查询支持二级缓存,你需要保证:
1. MyBatis支持二级缓存的总开关:全局配置变量参数 cacheEnabled=true
2. 该select语句所在的Mapper,配置了<cache> 或<cached-ref>节点,并且有效
3. 该select语句的参数 useCache=true
6.一级缓存和二级缓存的使用顺序
请注意,如果你的MyBatis使用了二级缓存,并且你的Mapper和select语句也配置使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:
二级缓存 ———> 一级缓存——> 数据库
7.二级缓存实现的选择
MyBatis对二级缓存的设计非常灵活,它自己内部实现了一系列的Cache缓存实现类,并提供了各种缓存刷新策略如LRU
,FIFO
等等;另外,MyBatis还允许用户自定义Cache接口实现,用户是需要实现org.apache.ibatis.cache.Cache
接口,然后将Cache实现类配置在<cache type="">
节点的type属性上即可;除此之外,MyBatis还支持跟第三方内存缓存库如Memecached的集成,总之,使用MyBatis的二级缓存有三个选择:
1.MyBatis自身提供的缓存实现;
2. 用户自定义的Cache接口实现;
3.跟第三方内存缓存库的集成;
以上是关于MyBatis缓存专题-一文彻底搞懂MyBatis二级缓存的主要内容,如果未能解决你的问题,请参考以下文章
带你彻底搞懂MyBatis的底层实现之缓存模块(Cache)-吊打面试官必备技能