Loading

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>
posted @ 2020-09-13 09:03  许大仙  阅读(272)  评论(0编辑  收藏  举报