概述

Mybatis提供查询缓存,如果缓存中有数据就不用从数据库中获取,用于减轻数据压力,提高系统性能。 Mybatis的查询缓存总共有两级,我们称之为一级缓存和二级缓存,如图:

  • 一级缓存是SqlSession级别的缓存。在操作数据库时需要构造
    • sqlSession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。
    • 不同的sqlSession之间的 缓存数据区域(HashMap)是互相不影响的。
    • 一级缓存默认开启。
  • 二级缓存是Mapper(namespace)级别的缓存。
    • 多个SqlSession去操作同一个Mapper的sql语句,多个SqlS ession可以共用二级缓存,二级缓存是跨SqlSession的。
    • 二级缓存默认关闭

代码案例

测试一级缓存

用户Mapper

  1. User findUserById(Integer id);

用户Mapper.xml

  1. <select id="findUserById" resultType="com.example.kkbstudy.pojo.User">
  2. select * from `user` where id = #{id}
  3. </select>

编写单元测试

  1. @Test
  2. void test4() {
  3. SqlSession sqlSession = sqlSessionFactory.openSession();
  4. UserMapper mapper = sqlSession.getMapper(UserMapper.class);
  5. // 第一次查询ID为1的用户,去缓存找,找不到就去查找数据库
  6. User user1 = mapper.findUserById(1);
  7. System.out.println(user1);
  8. // 第二次查询ID为1的用户
  9. User user2 = mapper.findUserById(1);
  10. System.out.println(user2); sqlSession.close();
  11. }

输出

  1. 2021-05-21 11:44:38.468 INFO 31100 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
  2. 2021-05-21 11:44:38.724 INFO 31100 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
  3. 2021-05-21 11:44:38.731 DEBUG 31100 --- [ main] c.e.k.mapper.UserMapper.findUserById : ==> Preparing: select * from `user` where id = ?
  4. 2021-05-21 11:44:38.757 DEBUG 31100 --- [ main] c.e.k.mapper.UserMapper.findUserById : ==> Parameters: 1(Integer)
  5. 2021-05-21 11:44:38.777 DEBUG 31100 --- [ main] c.e.k.mapper.UserMapper.findUserById : <== Total: 1
  6. User(id=1, name=张三)
  7. User(id=1, name=张三)

这里可以看到两次查询只查询了一遍数据库。
这里为什么要用最基本的SqlSession去操作数据库测试呢,因为一级缓存是SqlSession级别的,接下来我们看一下加入使用我们常用的方式去调用两次。

编写单元测试

  1. @Test
  2. void test5() {
  3. // 第一次查询ID为1的用户,去缓存找,找不到就去查找数据库
  4. User user1 = userMapper.findUserById(1);
  5. System.out.println(user1);
  6. // 第二次查询ID为1的用户
  7. User user2 = userMapper.findUserById(1);
  8. System.out.println(user2);
  9. }
  1. 2021-05-21 11:50:55.757 DEBUG 23720 --- [ main] c.e.k.mapper.UserMapper.findUserById : ==> Preparing: select * from `user` where id = ?
  2. 2021-05-21 11:50:55.790 DEBUG 23720 --- [ main] c.e.k.mapper.UserMapper.findUserById : ==> Parameters: 1(Integer)
  3. 2021-05-21 11:50:55.820 DEBUG 23720 --- [ main] c.e.k.mapper.UserMapper.findUserById : <== Total: 1
  4. User(id=1, name=张三)
  5. 2021-05-21 11:50:55.828 DEBUG 23720 --- [ main] c.e.k.mapper.UserMapper.findUserById : ==> Preparing: select * from `user` where id = ?
  6. 2021-05-21 11:50:55.829 DEBUG 23720 --- [ main] c.e.k.mapper.UserMapper.findUserById : ==> Parameters: 1(Integer)
  7. 2021-05-21 11:50:55.832 DEBUG 23720 --- [ main] c.e.k.mapper.UserMapper.findUserById : <== Total: 1
  8. User(id=1, name=张三)

可以发现还是查询了两次数据库,因为我们将UserMapper交给了spring管理。事务也控制在spring手中,在两次查询的时候分别做了开启sqlSession和close,如下

  1. service{
  2. //开始执行时,开启事务,创建SqlSession对象
  3. //第一次调用mapper的方法findUserById(1)
  4. //第一次调用结束,sqlSession关闭
  5. //第二次调用时,开启事务,创建SqlSession对象
  6. //第二次调用mapper的方法findUserById(1)
  7. //第二次调用结束,sqlSession关闭
  8. }

测试二级缓存

配置开启缓存
application.yml配置

  1. mybatis:
  2. configuration:
  3. cache-enabled: true

UserMapper.xml配置

  1. <cache/>

cache标签主要包含以下属性:

  1. type: 自定义缓存或者整合第三方缓存时使用
  2. eviction: 缓存的回收策略,默认是LRU
  3. LRU - 最近最少使用的:移除最长时间不被使用的对象
  4. FIFO - 先进先出策略:按对象进入缓存的顺序来移除它们
  5. SOFT - 软引用:移除基于垃圾回收器状态和软引用规则的对象
  6. WEAK - 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象
  7. flushInterval: 缓存的刷新间隔,默认是不刷新的
  8. size:缓存中的对象个数
  9. readOnly:缓存的只读设置,默认是false
  10. blocking:是否使用阻塞缓存,默认为false,当指定为true时将采用BlockingCache进行封装,blocking,阻塞的意思,使用BlockingCache会在查询缓存时锁住对应的Key,如果缓存命中了则会释放对应的锁,否则会在查询数据库以后再释放锁这样可以阻止并发情况下多个线程同时查询数据,详情可参考BlockingCache的源码。

注: LRU算法参考 LRU算法
BlockingCache源码解读见BlockingCache源码解读

确保pojo对象实现序列化标记

由于二级缓存的数据不一定都是存储到内存中,它的存储介质多种多样,比如说存储到文件系统中,所以需要给缓存的如果该类存在父类,那么父类也要实现序列化。

  1. @Data
  2. public class User implements Serializable {
  3. private static final long serialVersionUID = 0L;
  4. private Integer id;
  5. private String name;
  6. }

编写单元测试

  1. @Test
  2. void test5() {
  3. // 第一次查询ID为1的用户,去缓存找,找不到就去查找数据库
  4. User user1 = userMapper.findUserById(1);
  5. System.out.println(user1);
  6. // 第二次查询ID为1的用户
  7. User user2 = userMapper.findUserById(1);
  8. System.out.println(user2);
  9. }

输出

  1. 2021-05-21 13:17:02.981 DEBUG 16088 --- [ main] com.example.kkbstudy.mapper.UserMapper : Cache Hit Ratio [com.example.kkbstudy.mapper.UserMapper]: 0.0
  2. 2021-05-21 13:17:02.988 INFO 16088 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
  3. 2021-05-21 13:17:03.263 INFO 16088 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
  4. 2021-05-21 13:17:03.273 DEBUG 16088 --- [ main] c.e.k.mapper.UserMapper.findUserById : ==> Preparing: select * from `user` where id = ?
  5. 2021-05-21 13:17:03.302 DEBUG 16088 --- [ main] c.e.k.mapper.UserMapper.findUserById : ==> Parameters: 1(Integer)
  6. 2021-05-21 13:17:03.331 DEBUG 16088 --- [ main] c.e.k.mapper.UserMapper.findUserById : <== Total: 1
  7. User(id=1, name=张三)
  8. 2021-05-21 13:17:03.342 WARN 16088 --- [ main] o.apache.ibatis.io.SerialFilterChecker : As you are using functionality that deserializes object streams, it is recommended to define the JEP-290 serial filter. Please refer to https://docs.oracle.com/pls/topic/lookup?ctx=javase15&id=GUID-8296D8E8-2B93-4B9A-856E-0A65AF9B8C66
  9. 2021-05-21 13:17:03.343 DEBUG 16088 --- [ main] com.example.kkbstudy.mapper.UserMapper : Cache Hit Ratio [com.example.kkbstudy.mapper.UserMapper]: 0.5
  10. User(id=1, name=张三)

可以看到,只查询了一次数据库,输出了两次结果
Cache Hit Ratio:缓存命中率
第一次缓存中没有记录,则命中率0.0;
第二次缓存中有记录,则命中率0.5(访问两次,有一次命中)

禁用二级缓存

默认二级缓存的粒度是Mapper级别的,但是如果在同一个Mapper文件中某个查询不想使用二级缓存的话,就需要对缓存 在select标签中设置useCache=false,可以禁用当前select语句的二级缓存,即每次查询都是去数据库中查询, 默认情况下是true,即该statement使用二级缓存。

刷新二级缓存

通过flushCache属性,可以控制select、insert、update、delete标签是否属性二级缓存

默认设置

默认情况下如果是select语句,那么flushCache是false。
如果是insert、update、delete语句,那么flushCache是true。

默认配置解读

如果查询语句设置成true,那么每次查询都是去数据库查询,即意味着该查询的二级缓存失效。
如果增删改语句设置成false,即使用二级缓存,那么如果在数据库中修改了数据,而缓存数据还是原来的,这 个时候就会出现脏读。

  1. <select id="findUserById" resultType="com.example.kkbstudy.pojo.User" useCache="false" flushCache="true">
  2. select * from `user` where id = #{id}
  3. </select>

应用场景

  • 使用场景:

对于访问响应速度要求高,但是实时性不高的查询,可以采用二级缓存技术。

  • 注意事项:

在使用二级缓存的时候,要设置一下刷新间隔(cache标签中有一个flashInterval 属性)来定时刷新二级缓存,这个刷新间隔根据具体需求来设置,比如设置30分钟、60分钟等, 单位为毫秒。

局限性

Mybatis二级缓存对细粒度的数据级别的缓存实现不好。

  • 场景:

对商品信息进行缓存,由于商品信息查询访问量大,但是要求用户每次查询都是最新的商品信息,此时 如果使用二级缓存,就无法实现当一个商品发生变化只刷新该商品的缓存信息而不刷新其他商品缓存信息 ,因为二级缓存是mapper级别的,当一个商品的信息发送更新,所有的商品信息缓存数据都会清空。

  • 解决方法

此类问题,需要在业务层根据需要对数据有针对性的缓存。 比如可以对经常变化的数据操作单独放到另一个namespace的mapper中。