专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

Mybatis缓存

简介

什么是缓存

缓存在我们工作生活中经常被提及,比如“怎么清理浏览器的缓存”,“手机内存不够了,如何删除缓存”,“硬盘的缓存是不是越大越好”等等。

其实这些“缓存”可以分为三类:

  • 硬件缓存:指的是一块芯片,可以被集成到硬盘或 CPU 上。它用来充当硬盘(CPU)与外界接口(通常是内存)之间的暂存器。利用缓存可以减轻系统的负荷,同时提高数据的传输速率。
  • 客户端缓存:某些应用,比如浏览器、微信,为了实现快速响应用户的请求,会把用户之前浏览的东西(文字或图片等)存在本地。在下次访问时,如果本地的缓存里有请求的内容,那么就直接展示出来,不用再次向服务器发送请求,等待服务器响应。
  • 服务端缓存:它与客户端缓存目的相同,只不过是站在服务器的角度考虑。如果每次接到客户端请求都要连接一次数据库,当用户请求过多,将会导致负载过大。这时可以把一些经常被请求的数据存放在内存中,当有请求时直接返回,不用连接数据库,这样可以减轻数据库的负担。

关于缓存的定义,总结为一句话就是:缓存是临时存放数据(使用频繁的数据)的地方,介于外部请求和真实数据之间。

为什么要用缓存

1、 缓解服务器压力(不用每次去请求资源);
2、 提升性能(打开本地资源速度当然比请求回来再打开要快得多);
3、 减少带宽消耗;

缓存中的一些术语

命中(HIT):当客户端发起一个请求,如果被请求的资源在缓存中,这个资源就会被使用,我们就叫它缓存命中。

未命中(MISS):当客户端发起一个请求,如果没有在缓存追踪找到,我们称这种 情况为缓存未命中。这时需要查询数据库,并且将查询结果加入缓存中。

存储成本:当未命中时,我们会从数据库中取出数据,然后加入到缓存中。把这个数据放入缓存所需要的时间和空间,就是 存储成本。

失效:当缓存中的数据需要更新时,就意味着当前缓存中的这个数据失效了。缓存中的数据需要同步进行更新操作。还有一种情况就是该缓存过了失效时间。因为缓存会占用内存,缓存量过大会引发别的问题,我们一般都会设置失效时间来让缓存定时过期失效。

失效策略:如果缓存满了,而当前请求又没有命中缓存,那么就会按照某一种策略,把缓存中的某个旧资源剔除,而把新的资源加入缓存。这些决定应该剔除哪个旧资源的策略统称为失效策略(缓存算法)。

常见的一般策略有:

  • FIFO(first in first out) :先进先出策略,最先进入缓存的数据在缓存空间不够的情况下(超出最大元素限制)会被优先被清除掉,以腾出新的空间接受新的数据。策略算法主要比较缓存元素的创建时间。在数据实效性要求场景下可选择该类策略,优先保障最新数据可用。
  • LFU(less frequently used) :最少使用策略。无论是否过期,根据元素的被使用次数判断,清除使用次数较少的元素释放空间。策略算法主要比较元素的 hitCount(命中次数)。在保证高频数据有效性场景下,可选择这类策略。
  • LRU(least recently used) :最近最少使用策略。无论是否过期,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素释放空间。策略算法主要比较元素最近一次被get使用时间。在热点数据场景下较适用,优先保证热点数据的有效性。

除此之外,还有一些简单策略比如:

  • 根据过期时间判断,清理过期时间最长的元素;
  • 根据过期时间判断,清理最近要过期的元素;
  • 随机清理;
  • 根据关键字(或元素内容)长短清理等。

Mybatis缓存

Mybatis 缓存属于服务端缓存。

MyBatis 包含一个非常强大的查询缓存特性,它可以非常方便地定制和配置缓存。缓存可以极大的提升查询效率。

MyBatis 系统中默认定义了两级缓存:一级缓存二级缓存

  • 默认情况下,只有一级缓存开启。(SqlSession级别的缓存,也称为本地缓存)
  • 二级缓存需要手动开启和配置,它是基于 namespace 级别的缓存,缓存只作用于 cache 标签所在的映射文件中的语句。

一级缓存

一级缓存也叫本地缓存,它仅仅对一个会话中的数据进行缓存,在 Mybatis 中是指 SqlSession 对象开启到关闭的这段时间里称为一个会话。

在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis 提供了一级缓存的方案优化这部分场景,如果是相同的 SQL 语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。具体执行过程如下图所示。

32_1.png

每个 SqlSession 中持有了 Executor,每个 Executor 中有一个 LocalCache。当用户发起查询时,MyBatis 根据当前执行的语句生成 MappedStatement,在 Local Cache 进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入Local Cache,最后返回结果给用户。

实例分析

1、在测试项目中加入日志记录,方便查看效果。

2、User 实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
    private int id;
    private String name;
    private String password;
}

3、编写接口

User getUser(@Param("id") int id);

4、接口配置文件

<resultMap id="userMap" type="User">
    <result property="password" column="pwd" />
</resultMap>

<select id="getUser" resultMap="userMap">
    select * from mybatis.user where id=#{id}
</select>

5、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" />

    <settings>
        <setting name="logImpl" value="LOG4J"/>
    </settings>

    <typeAliases>
        <package name="com.msdn.bean"/>
    </typeAliases>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${driver}"/>
                <!--jdbc.url=jdbc:mysql://localhost:3306/oto?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC-->
                <property name="url" value="${url}"/>
                <property name="username" value="${username}"/>
                <property name="password" value="${password}"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="com/msdn/mapper/UserMapper.xml"/>
    </mappers>
</configuration>

6、测试

    @Test
    public void getUser(){
        SqlSession sqlSession = MybatisUtil.getSqlSession();

        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        User user = userMapper.getUser(1);
        System.out.println(user);

        User user2 = userMapper.getUser(1);
        System.out.println(user);
        System.out.println(user == user2);
        sqlSession.close();
    }

7、结果分析

2020-03-21 20:39:58,472 DEBUG [com.msdn.mapper.UserMapper] - Cache Hit Ratio [com.msdn.mapper.UserMapper]: 0.0
2020-03-21 20:39:58,704 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==>  Preparing: select * from mybatis.user where id=? 
2020-03-21 20:39:58,731 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Parameters: 1(Integer)
2020-03-21 20:39:58,747 DEBUG [com.msdn.mapper.UserMapper.getUser] - <==      Total: 1
User(id=1, name=hresh, password=123456)
2020-03-21 20:39:58,748 DEBUG [com.msdn.mapper.UserMapper] - Cache Hit Ratio [com.msdn.mapper.UserMapper]: 0.0
User(id=1, name=hresh, password=123456)
true

通过日志记录可以看出,SQL 语句只执行了一次,第二次获取 User 对象并未查询数据库,最后两个对象比较结果为 true 也说明是同一个对象。

一级缓存失效

一级缓存是默认开启且无法关闭的, 基于 SqlSession 级别。我们说的一级缓存失效,指的是在 SqlSession 对象存活期间,不止一次向数据库发送数据请求。

1、SqlSession 对象不同

    @Test
    public void getUser2(){
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        SqlSession sqlSession2 = MybatisUtil.getSqlSession();

        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        User user = userMapper.getUser(1);
        System.out.println(user);
        sqlSession.close();

        UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
        User user2 = userMapper2.getUser(1);
        System.out.println(user);
        System.out.println(user == user2);
        sqlSession2.close();
    }

执行结果为:

2020-03-21 21:45:48,085 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==>  Preparing: select * from mybatis.user where id=? 
2020-03-21 21:45:48,118 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Parameters: 1(Integer)
2020-03-21 21:45:48,138 DEBUG [com.msdn.mapper.UserMapper.getUser] - <==      Total: 1
User(id=1, name=hresh, password=123456)
2020-03-21 21:45:48,139 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==>  Preparing: select * from mybatis.user where id=? 
2020-03-21 21:45:48,140 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Parameters: 1(Integer)
2020-03-21 21:45:48,141 DEBUG [com.msdn.mapper.UserMapper.getUser] - <==      Total: 1
User(id=1, name=hresh, password=123456)
false

从结果中可以看出执行过程中有两条 SQL 语句,可以得出结论 :每个 sqlSession 中的缓存是独立的。

2、SqlSession 对象相同,查询请求不同

    @Test
    public void getUser(){
        SqlSession sqlSession = MybatisUtil.getSqlSession();

        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        User user = userMapper.getUser(1);
        System.out.println(user);

        User user2 = userMapper.getUser(2);
        System.out.println(user2);
        System.out.println(user == user2);
        sqlSession.close();
    }

执行结果为:

2020-03-21 21:52:34,817 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==>  Preparing: select * from mybatis.user where id=? 
2020-03-21 21:52:34,854 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Parameters: 1(Integer)
2020-03-21 21:52:34,871 DEBUG [com.msdn.mapper.UserMapper.getUser] - <==      Total: 1
User(id=1, name=hresh, password=123456)
2020-03-21 21:52:34,872 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==>  Preparing: select * from mybatis.user where id=? 
2020-03-21 21:52:34,872 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Parameters: 2(Integer)
2020-03-21 21:52:34,874 DEBUG [com.msdn.mapper.UserMapper.getUser] - <==      Total: 1
User(id=2, name=hresh2, password=123456)
false

同样发现有两条 SQL 语句,说明同一 SqlSession 下的缓存中添加新的数据需要请求数据库。

3、SqlSession 相同,两次查询操作之间执行了增删改操作

    @Test
    public void getUser(){
        SqlSession sqlSession = MybatisUtil.getSqlSession();

        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        User user = userMapper.getUser(1);
        System.out.println(user);

        //增删改操作,可能会修改原来的数据,所以必定会刷新缓存
        User user1 = new User(4,"acorn22","12344");
        userMapper.updateUser(user1);

        User user2 = userMapper.getUser(1);
        System.out.println(user2);
        System.out.println(user == user2);
        sqlSession.close();
    }

执行结果:

2020-03-21 21:58:51,685 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==>  Preparing: select * from mybatis.user where id=? 
2020-03-21 21:58:51,721 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Parameters: 1(Integer)
2020-03-21 21:58:51,738 DEBUG [com.msdn.mapper.UserMapper.getUser] - <==      Total: 1
User(id=1, name=hresh, password=123456)
2020-03-21 21:58:51,740 DEBUG [com.msdn.mapper.UserMapper.updateUser] - ==>  Preparing: update mybatis.user set name=? where id=? 
2020-03-21 21:58:51,741 DEBUG [com.msdn.mapper.UserMapper.updateUser] - ==> Parameters: acorn22(String), 4(Integer)
2020-03-21 21:58:51,748 DEBUG [com.msdn.mapper.UserMapper.updateUser] - <==    Updates: 1
2020-03-21 21:58:51,749 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==>  Preparing: select * from mybatis.user where id=? 
2020-03-21 21:58:51,749 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Parameters: 1(Integer)
2020-03-21 21:58:51,751 DEBUG [com.msdn.mapper.UserMapper.getUser] - <==      Total: 1
User(id=1, name=hresh, password=123456)
false

两次查询操作之间执行了修改操作,修改操作后再做任何操作,都会重新请求数据库。说明增删改操作可能会对数据库中的数据产生影响。

4、SqlSession 相同,手动清除一级缓存

    @Test
    public void getUser(){
        SqlSession sqlSession = MybatisUtil.getSqlSession();

        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        User user = userMapper.getUser(1);
        System.out.println(user);

        //手动清理缓存
        sqlSession.clearCache();

        User user2 = userMapper.getUser(1);
        System.out.println(user2);
        System.out.println(user == user2);
        sqlSession.close();
    }

执行结果:

2020-03-21 22:03:34,909 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==>  Preparing: select * from mybatis.user where id=? 
2020-03-21 22:03:34,947 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Parameters: 1(Integer)
2020-03-21 22:03:34,965 DEBUG [com.msdn.mapper.UserMapper.getUser] - <==      Total: 1
User(id=1, name=hresh, password=123456)
2020-03-21 22:03:34,965 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==>  Preparing: select * from mybatis.user where id=? 
2020-03-21 22:03:34,966 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Parameters: 1(Integer)
2020-03-21 22:03:34,967 DEBUG [com.msdn.mapper.UserMapper.getUser] - <==      Total: 1
User(id=1, name=hresh, password=123456)
false

可以认为一级缓存是个 map 集合,做了 clear 操作。

二级缓存

在上文中提到的一级缓存中,其最大的共享范围就是一个 SqlSession 内部,如果多个 SqlSession 之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用CachingExecutor 装饰 Executor,进入一级缓存的查询流程前,先在CachingExecutor 进行二级缓存的查询,具体的工作流程如下所示。

32_2.png

二级缓存开启后,同一个 namespace 下的所有操作语句,都影响着同一个 Cache,即二级缓存被多个 SqlSession 共享,是一个全局的变量。

当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。

实例分析

1、在 mybatis-config.xml 中开启全局缓存

<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>

2、在对应的 Mapper 配置文件中配置二级缓存

<cache
       eviction="FIFO"
       flushInterval="60000"
       size="512"
       readOnly="true"/>

3、测试

首先需要将 JavaBean 类实现序列化接口。

   /**
     * 首先需要开启二级缓存,只在同一个Mapper下有效;
     * 所有的数据都会先放在一级缓存中;
     * 只有当会话提交,或关闭时,才会提交到二级缓存中。
     *
     * */
    @Test
    public void getUser2(){
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        SqlSession sqlSession2 = MybatisUtil.getSqlSession();

        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        User user = userMapper.getUser(1);
        System.out.println(user);
        sqlSession.close();

        UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
        User user2 = userMapper2.getUser(1);
        System.out.println(user);
        System.out.println(user == user2);
        sqlSession2.close();
    }

执行结果:

2020-03-21 22:22:04,323 DEBUG [com.msdn.mapper.UserMapper] - Cache Hit Ratio [com.msdn.mapper.UserMapper]: 0.0
2020-03-21 22:22:04,668 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==>  Preparing: select * from mybatis.user where id=? 
2020-03-21 22:22:04,707 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Parameters: 1(Integer)
2020-03-21 22:22:04,729 DEBUG [com.msdn.mapper.UserMapper.getUser] - <==      Total: 1
User(id=1, name=hresh, password=123456)
2020-03-21 22:22:04,731 DEBUG [com.msdn.mapper.UserMapper] - Cache Hit Ratio [com.msdn.mapper.UserMapper]: 0.5
User(id=1, name=hresh, password=123456)
true

结论:

只要开启了二级缓存,在同一个 Mapper 中做的查询,数据都会存放在二级缓存中。数据首先会放在一级缓存中,当 sqlSession 对象提交或关闭后,一级缓存中的数据才会转到二级缓存中。

缓存原理

32_3.png

EhCache

ehcache 是一个用 Java 实现的使用简单,高速,实现线程安全的缓存管理类库,ehcache 提供了用内存,磁盘文件存储,以及分布式存储方式等多种灵活的 cache 管理方案。

使用步骤

1、导入相关依赖

<!-- https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-ehcache -->
<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-ehcache</artifactId>
    <version>1.1.0</version>
</dependency>

2、修改 Mapper 配置文件

<mapper namespace = “org.acme.FooMapper” > 
    <cache type = “org.mybatis.caches.ehcache.EhcacheCache” /> 
</mapper>

3、编写 ehcache.xml 文件,如果在加载时未找到/ehcache.xml资源或出现问题,则将使用默认配置。

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
         updateCheck="false">
    <!--
       diskStore:为缓存路径,ehcache分为内存和磁盘两级,此属性定义磁盘的缓存位置。参数解释如下:
       user.home – 用户主目录
       user.dir  – 用户当前工作目录
       java.io.tmpdir – 默认临时文件路径
     -->
    <diskStore path="java.io.tmpdir/Tmp_EhCache"/>
    <!--
       defaultCache:默认缓存策略,当ehcache找不到定义的缓存时,则使用这个缓存策略。只能定义一个。
     -->
    <!--
      name:缓存名称。
      maxElementsInMemory:缓存最大数目
      maxElementsOnDisk:硬盘最大缓存个数。
      eternal:对象是否永久有效,一但设置了,timeout将不起作用。
      overflowToDisk:是否保存到磁盘,当系统当机时
      timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
      timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
      diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
      diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
      diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
      memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
      clearOnFlush:内存数量最大时是否清除。
      memoryStoreEvictionPolicy:可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。
      FIFO,first in first out,这个是大家最熟的,先进先出。
      LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。
      LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
   -->
    <defaultCache
            eternal="false"
            maxElementsInMemory="10000"
            overflowToDisk="false"
            diskPersistent="false"
            timeToIdleSeconds="1800"
            timeToLiveSeconds="259200"
            memoryStoreEvictionPolicy="LRU"/>

    <cache
            name="cloud_user"
            eternal="false"
            maxElementsInMemory="5000"
            overflowToDisk="false"
            diskPersistent="false"
            timeToIdleSeconds="1800"
            timeToLiveSeconds="1800"
            memoryStoreEvictionPolicy="LRU"/>

</ehcache>

参考文献

聊聊MyBatis缓存机制

MyBatis:缓存

文章永久链接:https://tech.souyunku.com/18739

未经允许不得转载:搜云库技术团队 » Mybatis缓存

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们