Mybatis之缓存详解

Mybatis之缓存详解

  • 前言
  • 一级缓存
    • 注意点
    • 一级缓存失效的四种情况 (未 close SqlSession情况下)
  • 二级缓存
    • 全局开关
    • 分开关
    • entity 实现序列化接口
    • 示例
    • 注意点
  • 外部缓存
    • 集成EhCache缓存
      • 添加项目依赖
      • 配置 EhCache
    • 配置Redis缓存
      • 添加依赖
      • 配置Redis

Mybatis系列文章
Mybatis之配置详解
Mybatis之XML 映射器详解
Mybatis之动态 SQL详解
Mybatis之缓存详解
Mybatis之映射器注解详解
Mybatis之Mybatis Generator vs IDEA插件 自动生成代码

前言

Mybatis 使用到了两种缓存:一级缓存(即本地缓存 local cache)和二级缓存(second level cache)。

一级缓存

一级缓存(local cache), 即本地缓存, 作用域默认为sqlSession。当Session flushclose 后,该Session中的所有Cache将被清空。每当一个新 session 被创建,MyBatis 就会创建一个与之相关联的本地缓存。任何在 session 执行过的查询结果都会被保存在当前SqlSession的一个基于PerpetualCache 类Map中,key:hashCode+查询的SqlId+编写的sql查询语句+参数。所以,当再次执行参数相同的相同查询时,就不需要实际查询数据库了。本地缓存将会在做出修改、事务提交或回滚,以及关闭 session 时清空。

Mybatis之缓存详解_第1张图片

默认情况下,本地缓存的生命周期等同于整个 session 的周期。由于缓存会被用来解决循环引用问题和加快重复嵌套查询的速度,所以无法将其完全禁用。但你可以通过设置 localCacheScope=STATEMENT 来只在语句执行时使用缓存,达到让本地缓存失效的作用。如果你仅想让某个查询的本地缓存失效可以在select标签中添加flushCache="true",每次查询后不会将结果放入本地缓存。

<setting name="localCacheScope" value="STATEMENT"/>

以下为具体示范:

@Transactional
public GradeParam selectByNameAndLevel(String name, GradeLevel level) {
    // 测试一级缓存
    gradeParamMapper.selectByNameAndLevel(name, level);
    GradeParam gradeParam = gradeParamMapper.selectByNameAndLevel(name, level);
    return gradeParam;
}
<select id="selectByNameAndLevel" resultType="GradeParam">
  select
  <include refid="columns" />
  from grade_param
  where name = #{param1,jdbcType=VARCHAR} and level = #{param2,jdbcType=TINYINT}
</select>

结果:
Mybatis之缓存详解_第2张图片
本例子基于spring boot项目构建,sqlSession生命周期交给了Spring管理,在开启事务的情况下,一个事务中共用同一个sqlSession。由测试结果可见,查询1过后,查询2不会再去查库,本地缓存起到了作用。查询出的两个对象都是同一个对象。

注意点

  1. 如果localCacheScope 被设置为 SESSION,对于某个对象,MyBatis 将返回在本地缓存中唯一对象的引用。对返回的对象(例如 list)做出的任何修改将会影响本地缓存的内容,进而将会影响到在本次 session 中从缓存返回的值。因此,不要对 MyBatis 所返回的对象作出更改,以防后患。
  2. 由于本地缓存作用于sqlSession生命周期,这样在session1中执行查询1过后,未执行查询2之前,执行session2的更新,更新了session1查询1查出来的数据,再执行session1的查询2仍旧是查询出查询1的旧数据,这样导致本地缓存无法获取最新的记录。解决方案:使用二级缓存
  3. 综上两点,建议禁用一级缓存,如果需要缓存,可以考虑下文提到的二级缓存以及外部缓存。

一级缓存失效的四种情况 (未 close SqlSession情况下)

  1. 不同的SqlSession对应不同的一级缓存
  2. 同一个SqlSession但是查询条件不同
  3. 同一个SqlSession两次查询期间执行了任何一次增删改操作
  4. 同一个SqlSession两次查询期间手动清空了缓存

二级缓存

二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是namespace 级别的,可以被多个SqlSession 共享,二级缓存存在于 SqlSessionFactory生命周期中。如果你的MyBatis使用了二级缓存,并且你的Mapper中select语句也配置使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:二级缓存 —> 一级缓存 —> 数据库。

实际上MyBatis 用了一个装饰器的类来维护,就是CachingExecutor。如果启用了二级缓存,MyBatis 在创建Executor 对象的时候会对Executor 进行装饰。通过当前查询,会判断二级缓存是否有缓存结果,如果有就直接返回,如果没有委派交给真正的查询器Executor 实现类,比如SimpleExecutor 来执行查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户。
Mybatis之缓存详解_第3张图片
CachingExecutor 对于查询请求处理,CachingExecutor中query方法获取MappedStatement的cache缓存,封装到TransactionalCacheManager,通过TransactionalCacheManager、cacheKey未提交先将结果集存在集合中,commit时来操作存入cache。

CachingExecutor 中的核心源码

    private final Executor delegate;
    private final TransactionalCacheManager tcm = new TransactionalCacheManager();

    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        Cache cache = ms.getCache();
        if (cache != null) {
            this.flushCacheIfRequired(ms);
            if (ms.isUseCache() && resultHandler == null) {
                this.ensureNoOutParams(ms, boundSql);
                List<E> list = (List)this.tcm.getObject(cache, key);
                if (list == null) {
                    list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                    this.tcm.putObject(cache, key, list);
                }

                return list;
            }
        }

        return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }
    
    public void commit(boolean required) throws SQLException {
        this.delegate.commit(required);
        this.tcm.commit();
    }

全局开关

在 mybatis 中, 二级缓存有全局开关和分开关, 全局开关, 在 mybatis-config.xml 中如下配置:

<settings>
  <!--全局地开启或关闭配置文件中的所有映射器已经配置的任何缓存。 -->
  <setting name="cacheEnabled" value="true"/>
</settings>

分开关

配置完全局开关还需配置分开关才能使二级缓存起作用。在mapper.xml中配置分开关具体配置如下:

<cache type="org.apache.ibatis.cache.impl.PerpetualCache" size="1024" eviction="LRU" flushInterval="120000" readOnly="false"/>

cache标签可以配置的属性:

属性 描述
eviction 缓存的回收策略:LRU – 最近最少使用的:移除最长时间不被使用的对象。FIFO – 先进先出:按对象进入缓存的顺序来移除它们。SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。默认的是 LRU。
flushInterval 缓存刷新间隔,缓存多长时间清空一次,默认不清空,设置一个毫秒值
readOnly 是否只读:true:只读,mybatis认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。mybatis为了加快获取速度,直接就会将数据在缓存中的引用交给用户。不安全,速度快。false:非只读,mybatis觉得获取的数据可能会被修改。mybatis会利用序列化和反序列的技术克隆一份新的数据给你。安全,速度慢。默认是true。
size 对应为引用的数量,即最多的缓存对象数据, 默认为 1024。
type 指定自定义缓存的全类名,mybatis 默认缓存实现类 org.apache.ibatis.cache.impl.PerpetualCache。实现Cache接口即可
blocking blocking 为阻塞, 默认值为 false。 当指定为 true 时将采用 BlockingCache 进行封装。使用 BlockingCache 会在查询缓存时锁住对应的 Key,如果缓存命中了则会释放对应的锁,否则会在查询数据库以后再释放锁,这样可以阻止并发情况下多个线程同时查询数据。

entity 实现序列化接口

/**
 * 年级参数类
 *
 * @author : Nestor.Bian
 * @version : V 1.0
 * @date : 2020/3/20
 */
@Data
public class GradeParam implements Serializable {
    public static final long serialVersionUID = 1L;
    private Long id;
    private String name;
    private String managerName;
    @JsonDeserialize(using = BaseEnumJsonDeserializer.class)
    private GradeLevel level;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

必不可少的步骤:存取对象到二级缓存中需要序列化和反序列化

示例

GradeParamMapper.xml中添加分开关配置

<cache eviction="LRU" blocking="false" flushInterval="180000" size="2048" readOnly="fals/>
@Transactional
public GradeParam selectByNameAndLevel(String name, GradeLevel level) {
    GradeParam gradeParam = gradeParamMapper.selectByNameAndLevel(name, level);
    return gradeParam;
}

结果
Mybatis之缓存详解_第4张图片
第一次api接口调用:
SqlSession 查询出年级参数, 日志中打印了 「Cache Hit Ratio」, 代表二级缓存使用了, 但是没有命中。 sqlSession.close()后, 此时将数据序列化并保持到二级缓存中。
第二次api接口调用:
查询出年级参数对象,直接从二级缓存中拿了数据, 因此没有发送 SQL 语句,命中率 0.5
由于 readOnly=“false”, 由于都是反序列化得到的, 两次查询出的对象为不同的实例。

注意点

  1. 由于在更新时会刷新缓存, 因此需要注意使用场合:查询频率很高, 更新频率很低时使用, 即经常使用 select, 相对较少使用delete, insert, update。
  2. 缓存是以 namespace 为单位的,不同 namespace 下的操作互不影响。但刷新缓存是刷新整个 namespace 的缓存, 也就是你 update 了一个, 则整个namespace 缓存都被清空。可以使用,让多个mapper.xml使用同一个namespace,这样一个mapper.xml中的操作刷新了缓存,那么整个namespace下的缓存都会被清除。
  3. 最好在 「只有单表操作」 的表的 namespace 使用缓存, 而且对该表的操作都在这个 namespace 中。 否则可能会出现脏读的情况。
    引起脏读的操作通常发生在多表关联操作中,比如在两个不同的mapper中都涉及到同一个表的增删改查操作,当其中一个mapper对这张表进行查询操作,此时另一个mapper进行了更新操作刷新缓存,然后第一个mapper又查询了一次,那么这次查询出的数据是脏数据。出现脏读的原因是他们的操作的缓存并不是同一个。

外部缓存

集成EhCache缓存

缓存数据有内存和磁盘两级,无须担心容量问题。

  • 缓存数据会在虚拟机重启的过程中写入磁盘。可以通过RMI、可插入API等方式进行分布式缓存。
  • 具有缓存和缓存管理器的侦昕接口。
  • 支持多缓存管理器实例以及一个实例的多个缓存区域。

添加项目依赖

<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-ehcache</artifactId>
    <version>1.0.3</version>
</dependency>

配置 EhCache

src/main/resources 目录下新增 ehcache.xml 文件。

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="ehcache.xsd"
         updateCheck="false" monitoring="autodetect"
         dynamicConfig="true">

    <!--指定数据在磁盘中的存储位置-->
    <diskStore path="D:/ehcache"/>

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


    <cache  name="com.nestor.mybatisdemo.mapper.SchoolMapper"
            maxElementsInMemory="3000"
            eternal="false"
            copyOnRead="true"
            copyOnWrite="true"
            timeToIdleSeconds="3600"
            timeToLiveSeconds="3600"
            overflowToDisk="true"
            diskPersistent="true"/>
</ehcache>

diskStore:指定数据在磁盘中的存储位置。
defaultCache:当借助CacheManager.add(“demoCache”)创建Cache时,EhCache便会采用指定的的管理策略。
以下属性是必须的
maxElementsInMemory - 在内存中缓存的element的最大数目。不存放在内存中需要设置成1,Ehcache 2.0后设置成0表示不限制最大数目
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(先进先出)。
copyOnRead - 判断从缓存中读取数据时是返回对象的引用还是复制一个对象返回。默认情况下是false,即返回数据的引用,这种情况下返回的都是相同的对象,和MyBatis默认缓存中的只读对象是相同的。如果设置为true ,那就是可读写缓存,每次读取缓存时都会复制一个新的实例 。
copyOnWrite - 判断写入缓存时是直接缓存对象的引用还是复制一个对象然后缓存,默认也是false。如果想使用可读写缓存,就需要将这两个属性配置为true,如果使用只读缓存,可以不配置这两个属性,使用默认值false 即可 。

ehcache-cache 提供了如下 2 个可选的缓存实现。

  • org.mybatis.caches.ehcache.EhcacheCache
  • org.mybatis.caches.ehcache.LoggingEhcache 这个是带日志的缓存。

在Mapper.xml中添加如下配置即可

<mapper namespace="com.nestor.mybatisdemo.mapper.GradeParamMapper">
	<cache type="org.mybatis.caches.ehcache.EhcacheCache" />
</mapper>

只通过设置 type属性就可以使用 EhCache 缓存了,这时cache的其他属性都不会起到任何作用,针对缓存的配置都在ehcache.xml中进行。在ehcache.xml配置文件中,只有一个默认的缓存配置,命名空间没有特殊配置默认使用默认的缓存配置。如果想针对某一个命名空间进行配置,需要在 ehcache.xml 中添加一个和映射文件命名空间一致的缓存配置,例如针对SchoolMapper可以进行如下配置。

<cache  name="com.nestor.mybatisdemo.mapper.SchoolMapper"
        maxElementsInMemory="3000"
        eternal="false"
        copyOnRead="true"
        copyOnWrite="true"
        timeToIdleSeconds="3600"
        timeToLiveSeconds="3600"
        overflowToDisk="true"
        diskPersistent="true"/>

储存位置如下:
在这里插入图片描述

配置Redis缓存

添加依赖

<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-redis</artifactId>
    <version>1.0.0-beta2</version>
</dependency>

配置Redis

使用 Redis 前,必须有一个 Redis 服务,有关Redis安装启动的相关内容,可参考如下地址中的官方文档:https://redis.io/topics/quickstart。Redis服务启动后,在src/main/resources目录下新增 redis.properties文件 。

host=localhost
port=6379
connectionTimeout=5000
soTimeout=5000
password=123456
database=0
clientName=

在mapper.xml中添加如下配置即可。

<mapper namespace="com.nestor.mybatisdemo.mapper.GradeParamMapper">
	<cache type="org.mybatis.caches.redis.RedisCache" />
</mapper>

配置依然很简单, RedisCache在保存缓存数据和获取缓存数据时,使用了Java的序列化和反序列化,因此还需要保证被缓存的对象必须实现Serializable接口。因为Redis作为缓存服务器,它缓存的数据和程序(或测试)的启动无关,Redis 的缓存并不会因为应用的关闭而失效。当需要分布式部署应用时,如果使用MyBatis自带缓存或基础的EhCache缓存,分布式应用会各自拥有自己的缓存,它们之间不会共享缓存 ,这种方式会消耗更多的服务器资源。如果使用类似 Redis 的缓存服务,就可以将分布式应用连接到同一个缓存服务器,实现分布式应用间的缓存共享

储存位置如下:
Mybatis之缓存详解_第5张图片
github工程地址:https://github.com/nestorbian/spring-boot-examples/tree/master/mybatis-demo

Mybatis系列文章
Mybatis之配置详解
Mybatis之XML 映射器详解
Mybatis之动态 SQL详解
Mybatis之缓存详解
Mybatis之映射器注解详解
Mybatis之Mybatis Generator vs IDEA插件 自动生成代码

参考:
mybatis缓存
MyBatis缓存介绍
mybatis 缓存的使用, 看这篇就够了
mybatis缓存机制

你可能感兴趣的:(Spring,Boot,Mybatis,笔记,java,mybatis,spring,boot)