1、缓存机制介绍

2、一级缓存和二级缓存

查询的顺序是:

  • 先查询二级缓存,因为二级缓存中可能会有其他程序已经查出来的数据,可以拿来直接使用。
  • 如果二级缓存没有命中,再查询一级缓存
  • 如果一级缓存也没有命中,则查询数据库
  • SqlSession 关闭之前,一级缓存中的数据会写入二级缓存

从范围和作用域角度来说:

  • 一级缓存:SqlSession 级别
  • 二级缓存:SqlSessionFactory 级别

它们之间范围的大小参考下面图:

3、代码验证一级缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Test
public void testFirstLevelCache() {
SqlSession session = factory.openSession();

EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);

// 1.第一次查询
Employee employee1 = mapper.selectEmployeeById(2);

System.out.println("employee1 = " + employee1);

// 2.第二次查询
Employee employee2 = mapper.selectEmployeeById(2);

System.out.println("employee2 = " + employee2);

// 3.经过验证发现,两次查询返回的其实是同一个对象
System.out.println("(employee2 == employee1) = " + (employee2 == employee1));
System.out.println("employee1.equals(employee2) = " + employee1.equals(employee2));
System.out.println("employee1.hashCode() = " + employee1.hashCode());
System.out.println("employee2.hashCode() = " + employee2.hashCode());

session.commit();
session.close();
}

打印结果:

1
2
3
4
5
6
7
8
9
DEBUG 12-01 09:14:48,760 ==>  Preparing: select emp_id,emp_name,emp_salary,emp_gender,emp_age from t_emp where emp_id=?   (BaseJdbcLogger.java:145)
DEBUG 12-01 09:14:48,804 ==> Parameters: 2(Integer) (BaseJdbcLogger.java:145)
DEBUG 12-01 09:14:48,830 <== Total: 1 (BaseJdbcLogger.java:145)
employee1 = Employee{empId=2, empName='AAAAAA', empSalary=6666.66, empAge=20, empGender='male'}
employee2 = Employee{empId=2, empName='AAAAAA', empSalary=6666.66, empAge=20, empGender='male'}
(employee2 == employee1) = true
employee1.equals(employee2) = true
employee1.hashCode() = 1131645570
employee2.hashCode() = 1131645570

一共只打印了一条 SQL 语句,两个变量指向同一个对象。

4、一级缓存失效的情况

  • 不是同一个 SqlSession
  • 同一个 SqlSession 但是查询条件发生了变化
  • 同一个 SqlSession 两次查询期间执行了任何一次增删改操作
  • 同一个 SqlSession 两次查询期间手动清空了缓存

5、使用二级缓存

这里我们使用的是 Mybatis 自带的二级缓存。

① 开启二级缓存功能

在想要使用二级缓存的 Mapper 配置文件中加入 cache 标签

1
2
3
4
<mapper namespace="com.atguigu.mybatis.EmployeeMapper">

<!-- 加入cache标签启用二级缓存功能 -->
<cache/>

② 让实体类支持序列化

1
public class Employee implements Serializable {

③junit 测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Test
public void testSecondLevelCacheExists() {
SqlSession session = factory.openSession();

EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);

Employee employee = mapper.selectEmployeeById(2);

System.out.println("employee = " + employee);

// 在执行第二次查询前,关闭当前SqlSession
session.close();

// 开启一个新的SqlSession
session = factory.openSession();

mapper = session.getMapper(EmployeeMapper.class);

employee = mapper.selectEmployeeById(2);

System.out.println("employee = " + employee);

session.close();

}

打印效果:

1
2
3
4
5
6
7
DEBUG 12-01 09:44:27,057 Cache Hit Ratio [com.atguigu.mybatis.EmployeeMapper]: 0.0  (LoggingCache.java:62)
DEBUG 12-01 09:44:27,459 ==> Preparing: select emp_id,emp_name,emp_salary,emp_gender,emp_age from t_emp where emp_id=? (BaseJdbcLogger.java:145)
DEBUG 12-01 09:44:27,510 ==> Parameters: 2(Integer) (BaseJdbcLogger.java:145)
DEBUG 12-01 09:44:27,536 <== Total: 1 (BaseJdbcLogger.java:145)
employee = Employee{empId=2, empName='AAAAAA', empSalary=6666.66, empAge=20, empGender='male'}
DEBUG 12-01 09:44:27,622 Cache Hit Ratio [com.atguigu.mybatis.EmployeeMapper]: 0.5 (LoggingCache.java:62)
employee = Employee{empId=2, empName='AAAAAA', empSalary=6666.66, empAge=20, empGender='male'}

④ 缓存命中率

日志中打印的 Cache Hit Ratio 叫做缓存命中率

1
2
3
4
5
Cache Hit Ratio [com.atguigu.mybatis.EmployeeMapper]: 0.00/1)
Cache Hit Ratio [com.atguigu.mybatis.EmployeeMapper]: 0.51/2
Cache Hit Ratio [com.atguigu.mybatis.EmployeeMapper]: 0.66666666666666662/3
Cache Hit Ratio [com.atguigu.mybatis.EmployeeMapper]: 0.753/4
Cache Hit Ratio [com.atguigu.mybatis.EmployeeMapper]: 0.84/5

缓存命中率=命中缓存的次数/查询的总次数

⑤ 查询结果存入二级缓存的时机

结论:SqlSession 关闭的时候,一级缓存中的内容会被存入二级缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1.开启两个SqlSession
SqlSession session01 = factory.openSession();
SqlSession session02 = factory.openSession();

// 2.获取两个EmployeeMapper
EmployeeMapper employeeMapper01 = session01.getMapper(EmployeeMapper.class);
EmployeeMapper employeeMapper02 = session02.getMapper(EmployeeMapper.class);

// 3.使用两个EmployeeMapper做两次查询,返回两个Employee对象
Employee employee01 = employeeMapper01.selectEmployeeById(2);
Employee employee02 = employeeMapper02.selectEmployeeById(2);

// 4.比较两个Employee对象
System.out.println("employee02.equals(employee01) = " + employee02.equals(employee01));

上面代码打印的结果是:

1
2
3
4
5
6
7
8
9
DEBUG 12-01 10:10:32,209 Cache Hit Ratio [com.atguigu.mybatis.EmployeeMapper]: 0.0  (LoggingCache.java:62)
DEBUG 12-01 10:10:32,570 ==> Preparing: select emp_id,emp_name,emp_salary,emp_gender,emp_age from t_emp where emp_id=? (BaseJdbcLogger.java:145)
DEBUG 12-01 10:10:32,624 ==> Parameters: 2(Integer) (BaseJdbcLogger.java:145)
DEBUG 12-01 10:10:32,643 <== Total: 1 (BaseJdbcLogger.java:145)
DEBUG 12-01 10:10:32,644 Cache Hit Ratio [com.atguigu.mybatis.EmployeeMapper]: 0.0 (LoggingCache.java:62)
DEBUG 12-01 10:10:32,661 ==> Preparing: select emp_id,emp_name,emp_salary,emp_gender,emp_age from t_emp where emp_id=? (BaseJdbcLogger.java:145)
DEBUG 12-01 10:10:32,662 ==> Parameters: 2(Integer) (BaseJdbcLogger.java:145)
DEBUG 12-01 10:10:32,665 <== Total: 1 (BaseJdbcLogger.java:145)
employee02.equals(employee01) = false

修改代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1.开启两个SqlSession
SqlSession session01 = factory.openSession();
SqlSession session02 = factory.openSession();

// 2.获取两个EmployeeMapper
EmployeeMapper employeeMapper01 = session01.getMapper(EmployeeMapper.class);
EmployeeMapper employeeMapper02 = session02.getMapper(EmployeeMapper.class);

// 3.使用两个EmployeeMapper做两次查询,返回两个Employee对象
Employee employee01 = employeeMapper01.selectEmployeeById(2);

// ※第一次查询完成后,把所在的SqlSession关闭,使一级缓存中的数据存入二级缓存
session01.close();
Employee employee02 = employeeMapper02.selectEmployeeById(2);

// 4.比较两个Employee对象
System.out.println("employee02.equals(employee01) = " + employee02.equals(employee01));

// 5.另外一个SqlSession用完正常关闭
session02.close();

打印结果:

1
2
3
4
5
6
DEBUG 12-01 10:14:06,804 Cache Hit Ratio [com.atguigu.mybatis.EmployeeMapper]: 0.0  (LoggingCache.java:62)
DEBUG 12-01 10:14:07,135 ==> Preparing: select emp_id,emp_name,emp_salary,emp_gender,emp_age from t_emp where emp_id=? (BaseJdbcLogger.java:145)
DEBUG 12-01 10:14:07,202 ==> Parameters: 2(Integer) (BaseJdbcLogger.java:145)
DEBUG 12-01 10:14:07,224 <== Total: 1 (BaseJdbcLogger.java:145)
DEBUG 12-01 10:14:07,308 Cache Hit Ratio [com.atguigu.mybatis.EmployeeMapper]: 0.5 (LoggingCache.java:62)
employee02.equals(employee01) = false

⑥ 二级缓存相关配置

在 Mapper 配置文件中添加的 cache 标签可以设置一些属性:

  • eviction 属性:缓存回收策略

    LRU(Least Recently Used) – 最近最少使用的:移除最长时间不被使用的对象。

    FIFO(First in First out) – 先进先出:按对象进入缓存的顺序来移除它们。

    SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。

    WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。

    默认的是 LRU。

  • flushInterval 属性:刷新间隔,单位毫秒

    默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新

  • size 属性:引用数目,正整数

    代表缓存最多可以存储多少个对象,太大容易导致内存溢出

  • readOnly 属性:只读,true/false

    true:只读缓存;会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。

    false:读写缓存;会返回缓存对象的拷贝(通过序列化)。这会慢一些,但是安全,因此默认是 false。

6、Mybatis 整合 EHCache

EHCache 作为第三方专门的缓存产品,相比 Mybatis 自带的缓存机制更加专业一些。

① 搭建 EHCache 使用环境

[1]在 Mybatis 环境基础上加入 jar 包

  • ehcache-core-2.6.8.jar:EHCache 核心包
  • mybatis-ehcache-1.0.3.jar:Mybatis 和 EHCache 的整合包
  • slf4j-api-1.6.1.jar:SFL4J 是一个日志标准
  • slf4j-log4j12-1.6.2.jar:SFL4J 标准下 log4j 的实现

[2]加入配置文件

文件名:ehcache.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
<!-- 磁盘保存路径 -->
<diskStore path="D:\atguigu\ehcache"/>

<defaultCache
maxElementsInMemory="1000"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>

引入第三方框架或工具时,配置文件的文件名可以自定义吗?

  • 可以自定义:文件名是由我告诉其他环境
  • 不能自定义:文件名是框架内置的、约定好的,就不能自定义,以避免框架无法加载这个文件

[3]指定缓存管理器的具体类型

在 Mapper 配置文件的 cache 标签内设置 type 属性

1
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>

②junit 测试

正常按照二级缓存的方式测试即可。因为整合 EHCache 后,其实就是使用 EHCache 代替了 Mybatis 自带的二级缓存。

③EHCache 配置文件说明

diskStore 标签:指定数据在磁盘中的存储位置。
defaultCache 标签:当借助 CacheManager.add(“demoCache”)创建 Cache 时,EhCache 便会采用指定的的管理策略

以下属性是必须的:
maxElementsInMemory - 在内存中缓存的 element 的最大数目
maxElementsOnDisk - 在磁盘上缓存的 element 的最大数目,若是 0 表示无穷大
eternal - 设定缓存的 elements 是否永远不过期。如果为 true,则缓存的数据始终有效,如果为 false 那么还要根据 timeToIdleSeconds,timeToLiveSeconds 判断
overflowToDisk - 设定当内存缓存溢出的时候是否将过期的 element 缓存到磁盘上

以下属性是可选的:
timeToIdleSeconds - 当缓存在 EhCache 中的数据前后两次访问的时间超过 timeToIdleSeconds 的属性取值时,这些数据便会删除,默认值是 0,也就是可闲置时间无穷大
timeToLiveSeconds - 缓存 element 的有效生命期,默认是 0.,也就是 element 存活时间无穷大
diskSpoolBufferSizeMB 这个参数设置 DiskStore(磁盘缓存)的缓存区大小.默认是 30MB.每个 Cache 都应该有自己的一个缓冲区.
diskPersistent - 在 VM 重启的时候是否启用磁盘保存 EhCache 中的数据,默认是 false。
diskExpiryThreadIntervalSeconds - 磁盘缓存的清理线程运行间隔,默认是 120 秒。每个 120s,相应的线程会进行一次 EhCache 中数据的清理工作
memoryStoreEvictionPolicy - 当内存缓存达到最大,有新的 element 加入的时候, 移除缓存中 element 的策略。默认是 LRU(最近最少使用),可选的有 LFU(最不常使用)和 FIFO(先进先出)

7、缓存的基本原理

集中体现在 org.apache.ibatis.cache.impl.PerpetualCache 类中,内部以 Map 的形式维护缓存数据的。