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返回的查询结果放置到缓存中,然后在返回给用户。

CachingExecutorExecutor的装饰者,以增强Executor的功能,使其具有缓存查询的功能,这里用到了设计模式中的装饰者模式,

CachingExecutorExecutor的接口的关系如下类图所示:

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缓存实现类,并提供了各种缓存刷新策略如LRUFIFO等等;另外,MyBatis还允许用户自定义Cache接口实现,用户是需要实现org.apache.ibatis.cache.Cache接口,然后将Cache实现类配置在<cache type="">节点的type属性上即可;除此之外,MyBatis还支持跟第三方内存缓存库如Memecached的集成,总之,使用MyBatis的二级缓存有三个选择:

1.MyBatis自身提供的缓存实现;
2. 用户自定义的Cache接口实现;
3.跟第三方内存缓存库的集成;

以上是关于MyBatis缓存专题-一文彻底搞懂MyBatis二级缓存的主要内容,如果未能解决你的问题,请参考以下文章

MyBatis缓存专题-一文彻底搞懂MyBatis二级缓存

带你彻底搞懂MyBatis的底层实现之缓存模块(Cache)-吊打面试官必备技能

带你彻底搞懂MyBatis的底层实现之缓存模块(Cache)-吊打面试官必备技能

手写Mybatis,彻底搞懂框架原理

两张图彻底搞懂MyBatis的Mapper原理!

一文搞懂Mybatis架构与工作原理