Mybatis的缓存机制
1 Mybatis的缓存机制介绍
- Mybatis包含了一个非常强大的查询缓存特性,它可以非常方便的配置和定制。缓存可以极大的提升查询效率。
- Mybatis系统中默认定义了两级缓存:一级缓存和二级缓存。
- 默认情况下,只有一级缓存(SqlSession级别的缓存,也称为本地缓存)开启。
- 二级缓存需要手动开启和配置,它是基于namespace级别的缓存。
- 为了提高扩展性。Mybatis定义了缓存接口Cache。我们可以通过实现Cache接口来自定义二级缓存。
2 准备工作
- 导入相关jar包的Maven坐标:
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
- sql脚本
DROP TABLE IF EXISTS `employee`;
CREATE TABLE `employee` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`last_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`gender` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
- db.properties
jdbc.url=jdbc:mysql://192.168.134.100:3306/test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true&allowMultiQueries=true
jdbc.driverClass=com.mysql.cj.jdbc.Driver
jdbc.username=root
jdbc.password=123456
- log4j.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
<appender name="STDOUT" class="org.apache.log4j.ConsoleAppender">
<param name="Encoding" value="UTF-8" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%-5p %d{MM-dd HH:mm:ss,SSS} %m (%F:%L) \n" />
</layout>
</appender>
<logger name="java.sql">
<level value="debug" />
</logger>
<logger name="org.apache.ibatis">
<level value="info" />
</logger>
<root>
<level value="debug" />
<appender-ref ref="STDOUT" />
</root>
</log4j:configuration>
- Employee.java
package com.sunxiaping.domain;
import org.apache.ibatis.type.Alias;
import java.io.Serializable;
@Alias("emp")
public class Employee implements Serializable {
private Integer id;
private String lastName;
private String email;
private String gender;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
@Override
public String toString() {
return "Employee{" +
"id=" + id +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +
", gender='" + gender + '\'' +
'}';
}
}
- EmployeeMapper.java
package com.sunxiaping.mapper;
public interface EmployeeMapper {
}
- EmployeeMapper.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.sunxiaping.mapper.EmployeeMapper">
</mapper>
- mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="db.properties"></properties>
<settings>
<!-- 开启自动驼峰命名规则映射 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 开启对jdbcType的NULL的支持 -->
<setting name="jdbcTypeForNull" value="NULL"/>
<!-- 开启延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 开启按需加载 -->
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driverClass}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<package name="com.sunxiaping.mapper"/>
</mappers>
</configuration
3 一级缓存
3.1 一级缓存体验
-
和数据库同一次会话期间查询到的数据会放在本地缓存中,如果以后需要获取相同的数据,直接从缓存中取,而不需要再去查询数据库。
-
示例:
-
EmployeeMapper.java
package com.sunxiaping.mapper;
import com.sunxiaping.domain.Employee;
public interface EmployeeMapper {
Employee findById(Integer id);
}
- EmployeeMapper.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.sunxiaping.mapper.EmployeeMapper">
<!--
抽取可重用的SQL片段,方便后面引用
-->
<sql id="emp_sql" >
id as id,last_name as lastName,email as email ,gender as gender
</sql>
<select id="findById" resultType="com.sunxiaping.domain.Employee">
SELECT <include refid="emp_sql"/>
FROM employee
WHERE id = #{id,jdbcType=INTEGER}
</select>
</mapper>
- 测试:
package com.sunxiaping;
import com.sunxiaping.domain.Employee;
import com.sunxiaping.mapper.EmployeeMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
public class EmployeeTest {
SqlSessionFactory sqlSessionFactory = null;
SqlSession sqlSession = null;
EmployeeMapper employeeMapper = null;
@Before
public void before() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
sqlSession = sqlSessionFactory.openSession(true);
employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
}
@After
public void after() {
if (null != sqlSession) {
sqlSession.close();
}
}
@Test
public void testFindById() {
Employee employee1 = employeeMapper.findById(1);
Employee employee2 = employeeMapper.findById(1);
System.out.println(employee1 == employee2);
}
}
- 日志:
3.2 一级缓存失效的四种情况
- ①SqlSession不同,一级缓存不同。
- ②SqlSession相同,但是查询条件不同(当前一级缓存中还没有这个数据),一级缓存不同。
- ③SqlSession相同,两次查询期间执行了增删改操作(这次增删改可能对当前数据有影响),一级缓存不同。
- ④SqlSession相同,手动清空了缓存,一级缓存不同。
4 二级缓存
4.1 二级缓存的介绍
- 基于namespace级别的缓存,一个namespace对应一个二级缓存。
- 工作机制:
- ①一个会话,查询一条数据,这个数据就会被放在当前会话的一级缓存中;
- ②如果会话关闭,一级缓存中的数据会被保存到二级缓存中。
- ③新的会话查询信息,就可以参照二级缓存。
- ④对于一个SqlSession来说,如果通过EmployeeMapper查询到Employee对象,和通过DepartmentMapper查询到Department对象,放到二级缓存中的位置是不同的。
4.2 二级缓存的使用
-
步骤:
- ①全局配置文件中开启二级缓存。
<settings> <!-- 开启自动驼峰命名规则映射 --> <setting name="mapUnderscoreToCamelCase" value="true"/> <!-- 开启对jdbcType的NULL的支持 --> <setting name="jdbcTypeForNull" value="NULL"/> <!-- 开启延迟加载 --> <setting name="lazyLoadingEnabled" value="true"/> <!-- 开启按需加载 --> <setting name="aggressiveLazyLoading" value="false"/> <!-- 开启二级缓存 --> <setting name="cacheEnabled" value="true"/> </settings>
- ②需要在使用二级缓存的映射文件处使用cache配置缓存。
<!-- cache标签:配置缓存的相关属性 - evication:缓存回收策略 - LRU:最近最少使用的,移除最长时间不被使用的对象,默认值。 - FIFO:先进先出,按对象进入缓存的顺序来移出它们 - SOFT:软引用,移除基于垃圾回收器状态和软引用规则的对象 - WEAK:弱引用,更积极的移除基于垃圾收集器状态和弱引用规则的对象 - flushInterval:刷新时间,单位是毫秒。 - 缓存多长时间清空一次,默认不清空。 - size:缓存存放多少元素,正整数。 - readOnly:是否只读 - true:只读缓存。 - Mybatis认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。 - Mybatis为了加快获取速度,直接将数据在缓存中的引用交给用户。 - 不安全,速度快。 - false:读写缓存。 - Mybatis认为获取的数据可能会被修改。 - Mybatis会利用序列化&&反序列化的技术克隆一份新的数据给你。 - 安全,速度慢。 - type:指定自定义缓存的全类名。 - 实现Cache接口即可 --> <cache eviction="LRU" flushInterval="60000" readOnly="false" size="1024" />
- ③注意POJO需要实现java.io.Serializable接口。
-
示例:
-
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="db.properties"></properties>
<settings>
<!-- 开启自动驼峰命名规则映射 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 开启对jdbcType的NULL的支持 -->
<setting name="jdbcTypeForNull" value="NULL"/>
<!-- 开启延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 开启按需加载 -->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 开启二级缓存 -->
<setting name="cacheEnabled" value="true"/>
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driverClass}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<databaseIdProvider type="DB_VENDOR">
<!--
为不同的数据库厂商起别名
-->
<property name="MySQL" value="mysql"/>
<property name="Oracle" value="oracle"/>
<property name="SQL Server" value="sqlserver"/>
</databaseIdProvider>
<mappers>
<package name="com.sunxiaping.mapper"/>
</mappers>
</configuration>
- EmployeeMapper.java
package com.sunxiaping.mapper;
import com.sunxiaping.domain.Employee;
public interface EmployeeMapper {
Employee findById(Integer id);
}
- EmployeeMapper.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.sunxiaping.mapper.EmployeeMapper">
<!--
cache标签:配置缓存的相关属性
- evication:缓存回收策略
- LRU:最近最少使用的,移除最长时间不被使用的对象,默认值。
- FIFO:先进先出,按对象进入缓存的顺序来移出它们
- SOFT:软引用,移除基于垃圾回收器状态和软引用规则的对象
- WEAK:弱引用,更积极的移除基于垃圾收集器状态和弱引用规则的对象
- flushInterval:刷新时间,单位是毫秒。
- 缓存多长时间清空一次,默认不清空。
- size:缓存存放多少元素,正整数。
- readOnly:是否只读
- true:只读缓存。
- Mybatis认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。
- Mybatis为了加快获取速度,直接将数据在缓存中的引用交给用户。
- 不安全,速度快。
- false:读写缓存。
- Mybatis认为获取的数据可能会被修改。
- Mybatis会利用序列化&&反序列化的技术克隆一份新的数据给你。
- 安全,速度慢。
- type:指定自定义缓存的全类名。
- 实现Cache接口即可
-->
<cache eviction="LRU" flushInterval="60000" readOnly="false" size="1024"/>
<sql id="emp_sql">
id as id,last_name as lastName,email as email ,gender as gender
</sql>
<select id="findById" resultType="com.sunxiaping.domain.Employee">
SELECT
<include refid="emp_sql"/>
FROM employee
WHERE id = #{id,jdbcType=INTEGER}
</select>
</mapper>
- 测试:
package com.sunxiaping;
import com.sunxiaping.domain.Employee;
import com.sunxiaping.mapper.EmployeeMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
public class EmployeeTest {
SqlSessionFactory sqlSessionFactory = null;
@Before
public void before() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}
@Test
public void testSecondLevelCache() {
SqlSession sqlSession = sqlSessionFactory.openSession(true);
SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
EmployeeMapper employeeMapper2 = sqlSession2.getMapper(EmployeeMapper.class);
Employee employee1 = employeeMapper.findById(1);
System.out.println("employee1 = " + employee1);
sqlSession.close();
//第二次查询是从二级缓存中拿到的数据,并没有发送新的SQL
Employee employee2 = employeeMapper2.findById(1);
System.out.println("employee2 = " + employee2);
sqlSession2.close();
}
}
4.3 缓存有关的设置和属性
- 全局配置文件中的cacheEache如果设置为false,将会关闭二级缓存;但是一级缓存一直打开。
- Mapper映射文件的每个select标签的都有useCache属性,如果设置为false,将会不使用二级缓存;但是一级缓存一直打开。
- Mapper映射文件中的insert、delete、update标签的flushCache属性,默认情况下是true,意味着执行之后将会清空一级缓存和二级缓存;而select标签的flushcache属性,默认情况下是false,不会清空一级缓存和二级缓存。
- 调用sqlSession的clearCache()方法只会清空一级缓存。
- 全局配置文件中的localCacheScope默认值是SESSION,当前会话的所有数据保存在会话中。如果设置为STATEMENT,将禁用一级缓存。
- 当在某一个作用域(一级缓存或二级缓存)进行了增删改操作后,默认情况下该作用域的所有select中的缓存将被清空。
4.4 缓存原理图
4.5 第三方缓存整合Mybatis
4.5.1 Ehcache概述
- Ehcache是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认的CacheProvider。
- Mybatis定义了Cache接口方便我们进行自定义扩展,而mybatis-ehcache就对Ehcache进行了整合。
4.5.2 Ehcache整合Mybatis步骤
- 导入ehcache包、mybatis-ehcache的整合包以及日志包的Maven坐标:
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.0.3</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.6.1</version>
<scope>test</scope>
</dependency>
- 编写ehcache.xml
<?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:\ehcache" />
<defaultCache
maxElementsInMemory="10000"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>
<!--
属性说明:
l diskStore:指定数据在磁盘中的存储位置。
l defaultCache:当借助CacheManager.add("demoCache")创建Cache时,EhCache便会采用<defalutCache/>指定的的管理策略
以下属性是必须的:
l maxElementsInMemory - 在内存中缓存的element的最大数目
l maxElementsOnDisk - 在磁盘上缓存的element的最大数目,若是0表示无穷大
l eternal - 设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断
l overflowToDisk - 设定当内存缓存溢出的时候是否将过期的element缓存到磁盘上
以下属性是可选的:
l timeToIdleSeconds - 当缓存在EhCache中的数据前后两次访问的时间超过timeToIdleSeconds的属性取值时,这些数据便会删除,默认值是0,也就是可闲置时间无穷大
l timeToLiveSeconds - 缓存element的有效生命期,默认是0.,也就是element存活时间无穷大
diskSpoolBufferSizeMB 这个参数设置DiskStore(磁盘缓存)的缓存区大小.默认是30MB.每个Cache都应该有自己的一个缓冲区.
l diskPersistent - 在VM重启的时候是否启用磁盘保存EhCache中的数据,默认是false。
l diskExpiryThreadIntervalSeconds - 磁盘缓存的清理线程运行间隔,默认是120秒。每个120s,相应的线程会进行一次EhCache中数据的清理工作
l memoryStoreEvictionPolicy - 当内存缓存达到最大,有新的element加入的时候, 移除缓存中element的策略。默认是LRU(最近最少使用),可选的有LFU(最不常使用)和FIFO(先进先出)
-->
- 在Mapper的映射文件中配置cache标签
<?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.sunxiaping.mapper.EmployeeMapper">
<!--
配置Ehcache缓存
-->
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
<sql id="emp_sql">
id as id,last_name as lastName,email as email ,gender as gender
</sql>
<select id="findById" resultType="com.sunxiaping.domain.Employee">
SELECT
<include refid="emp_sql"/>
FROM employee
WHERE id = #{id,jdbcType=INTEGER}
</select>
</mapper>
4.5.3 参照缓存
-
如果想在命名空间中共享相同的缓存配置和示例,可以使用cache-ref标签来引用另外一个缓存。
-
示例:
-
DepartmentMapper.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.sunxiaping.mapper.DepartmentMapper">
<!-- 参照缓存 -->
<cache-ref namespace="com.sunxiaping.mapper.EmployeeMapper"/>
</mapper>