MyBatis 10-缓存配置

http://www.broadview.com.cn/book/3643
https://github.com/mybatis-book/book

一般提到 MyBatis 缓存时都是指二级缓存。一级缓存默认会启用,且不能控制。

一级缓存

@Test
public void testL1Cache(){
    // 获取 sqlSession
    SqlSession sqlSession = getSqlSession();
    SysUser user1 = null;
    try {
        // 获取 UserMapper 接口
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        // 调用 selectById 方法,查询 id = 1 的用户
        user1 = userMapper.selectById(1l);
        // 对当前获取的对象重新赋值
        user1.setUserName("New Name");
        // 再次查询获取 id 相同的用户
        SysUser user2 = userMapper.selectById(1l);
        // 虽然我们没有更新数据库,但是这个用户名和我们 user1 重新赋值的名字相同了
        Assert.assertEquals("New Name", user2.getUserName());
        // 不仅如何,user2 和 user1 完全就是同一个实例
        Assert.assertEquals(user1, user2);
    } finally {
        // 关闭当前的 sqlSession
        sqlSession.close();
    }
    System.out.println("开启新的 sqlSession");
    // 开始另一个新的 session
    sqlSession = getSqlSession();
    try {
        // 获取 UserMapper 接口
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        // 调用 selectById 方法,查询 id = 1 的用户
        SysUser user2 = userMapper.selectById(1l);
        // 第二个 session 获取的用户名仍然是 admin
        Assert.assertNotEquals("New Name", user2.getUserName());
        // 这里的 user2 和 前一个 session 查询的结果是两个不同的实例
        Assert.assertNotEquals(user1, user2);
        // 执行删除操作
        userMapper.deleteById(2L);
        // 获取 user3
        SysUser user3 = userMapper.selectById(1l);
        // 这里的 user2 和 user3 是两个不同的实例
        Assert.assertNotEquals(user2, user3);
    } finally {
        // 关闭 sqlSession
        sqlSession.close();
    }
}

MyBatis 的一级缓存存在于 SqlSession 的生命周期中,在同一个 SqlSession 中查询时,MyBatis会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个 Map 对象中。如果同一个 SqlSession 中执行的方法和参数完全一致,那么通过算法会生成相同的键值,当 Map 缓存对象中已经存在该键值时,则会返回缓存中的对象。

如果不想让 selectById 方法使用一级缓存,可以加上 flushCach="true",会在查询数据前清空当前的一级缓存。

<!-- useCache="false" 可以禁止使用缓存 -->
<select id="selectById" flushCache="true" resultMap="userMap">
    select * from sys_user where id=#{id}
</select>

另外所有的 insert,update、delete 操作都会清空一级缓存。

二级缓存

一级缓存只存在于 SqlSession 的生命周期中,而二级缓存可以理解为存在于 SqlSessionFactory 的生命周期中。

配置二级缓存

mybatis-config.xml 文件中可以配置二级缓存的全局开关,默认是 true

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

二级缓存是和命名空间绑定的,所以需要配置在 Mapper.xml 映射文件中(命名空间是 xml 根节点 mapper 的 namespace 属性),或者配置在 Mapper.java 接口中(命令空间就是接口的全限定名称)。

Mapper.xml 中配置二级缓存

只需要在你的 SQL 映射文件中添加一行 <cache/>

默认效果:

  • 映射语句文件中的所有 select 语句的结果将会被缓存。
  • 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
  • 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
  • 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
  • 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
  • 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

这些属性可以通过 cache 元素的属性来修改。比如:

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

这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。

  1. eviction(回收策略):
    • LRU – 最近最少使用:移除最长时间不被使用的对象。默认策略
    • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
    • SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
    • WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
  2. flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。
  3. size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。
  4. readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。

Mapper 接口中配置二级缓存

@CacheNamespace(
    eviction=FifoCache.class,
    flushInterval=60000,
    size=512,
    readWrite=true  // true 为读写(默认),false 为只读
)
public interface RoleMapper {
}

当同时使用注解方式和 xml 映射文件时,如果同时配置了二级缓存,会抛出异常 Caches collection already contains value for tk.mybatis.simple.mapper.RoleMapper

因为这时是相同的命名空间,这时应该使用参照缓存

// 这样就会使用命名空间为 tk.mybatis.simple.mapper.RoleMapper 的缓存配置,即 RoleMapper.xml 中配置的缓存
@CacheNamespaceRef(RoleMapper.class)
public interface RoleMapper {
}

也可以在 xml 中配置参照缓存

<cache-ref namespace="tk.mybatis.simple.mapper.RoleMapper"/>

不会同时使用 Mapper 接口注解方式和 xml 映射文件,所以参照缓存不是为解决这个问题设计的,主要作用是解决脏读

使用二级缓存

由于配置的是可读写的缓存,而 MyBatis 使用 SerializedCache 序列化缓存来实现可读写缓存类,并通过序列化和反序列化来保证通过缓存获取数据时,得到的是一个新的实例。如果配置为只读缓存,MyBatis 会使用 Map 来存储缓存值。

使用 SerializedCache 序列化缓存,要求所有被序列化的对象必须实现 Serializable 接口。

public class SysRole implements Serializable {
    private static final long serialVersionUID = 6320941908222932112L;
}

测试

@Test
public void testL2Cache(){
    SqlSession sqlSession = getSqlSession();
    SysRole role1 = null;
    try {
        // 获取 RoleMapper 接口
        RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
        // 调用 selectById 方法,查询 id = 1 的用户
        role1 = roleMapper.selectById(1l);
        // 对当前获取的对象重新赋值
        role1.setRoleName("New Name");
        // 再次查询获取 id 相同的用户
        SysRole role2 = roleMapper.selectById(1l);
        // 虽然我们没有更新数据库,但是这个用户名和我们 role1 重新赋值的名字相同了
        Assert.assertEquals("New Name", role2.getRoleName());
        // 不仅如何,role2 和 role1 完全就是同一个实例
        Assert.assertEquals(role1, role2);
    } finally {
        sqlSession.close();
    }
    System.out.println("开启新的 sqlSession");
    // 开始另一个新的 session
    sqlSession = getSqlSession();
    try {
        // 获取 RoleMapper 接口
        RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
        // 调用 selectById 方法,查询 id = 1 的用户
        SysRole role2 = roleMapper.selectById(1l);
        // 第二个 session 获取的用户名仍然是 admin
        Assert.assertEquals("New Name", role2.getRoleName());
        // 这里的 role2 和 前一个 session 查询的结果是两个不同的实例
        Assert.assertNotEquals(role1, role2);
        // 获取 role3
        SysRole role3 = roleMapper.selectById(1l);
        // 这里的 role2 和 role3 是两个不同的实例
        Assert.assertNotEquals(role2, role3);
    } finally {
        sqlSession.close();
    }
}
// 第一次没有缓存,命中率 0
DEBUG [main] - Cache Hit Ratio [tk.mybatis.simple.mapper.RoleMapper]: 0.0
DEBUG [main] - ==>  Preparing: select id,role_name roleName, enabled, create_by createBy, create_time createTime from sys_role where id = ?
DEBUG [main] - ==> Parameters: 1(Long)
TRACE [main] - <==    Columns: id, roleName, enabled, createBy, createTime
TRACE [main] - <==        Row: 1, 管理员, 1, 1, 2016-04-01 17:02:14
DEBUG [main] - <==      Total: 1
// 使用的一级缓存,所以命中率还是 0
DEBUG [main] - Cache Hit Ratio [tk.mybatis.simple.mapper.RoleMapper]: 0.0
开启新的 sqlSession
// 第三次查询,命中率 1/3
DEBUG [main] - Cache Hit Ratio [tk.mybatis.simple.mapper.RoleMapper]: 0.3333333333333333
DEBUG [main] - Cache Hit Ratio [tk.mybatis.simple.mapper.RoleMapper]: 0.5

这个例子中没有真正的读写安全,role1.setRoleName("New Name");,第二部分的代码中查询结果 roleName 都是 "New Name"。所以想要安全使用,需要避免无意义的修改。

集成 EhCache

EhCache 是一个纯粹的 Java 进程内的缓存框架,具有快速、精干等特点。具体来说,EhCache 主要的特性如下。

  • 快速。
  • 简单。
  • 多种缓存策略。
  • 缓存数据有内存和磁盘两级,无须担心容量问题。
  • 存数据会在虚拟机重启的过程中写入磁盘。
  • 可以通过RMI、可插入 AP I等方式进行分布式缓存。
  • 具有缓存和缓存管理器的侦昕接口。
  • 支持多缓存管理器实例以及一个实例的多个缓存区域。

添加项目依赖

<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:/cache" />

    <defaultCache
        maxElementsInMemory="3000"
        eternal="false"
        copyOnRead="true"
        copyOnWrite="true"
        timeToIdleSeconds="3600"
        timeToLiveSeconds="3600"
        overflowToDisk="true"
        diskPersistent="true"/>
</ehcache>
  • maxElementsInMemory:设置基于内存的缓存中可存放的对象最大数目

  • eternal:设置对象是否为永久的,true 表示永不过期,此时将忽略

  • timeToIdleSeconds:设置对象空闲最长时间,以秒为单位,超过这个时间对象过期。当对象过期时,ehCache 会把它从缓存中清除。如果此值为 0,表示对象可以无限期地处于空闲状态。

  • timeToLiveSeconds:设置对象生存最长时间,超过这个时间,对象过期。如果此值为 0,表示对象可以无限期地存在于缓存中,该属性值必须大于或等于 timeToIdleSeconds 属性值

  • overflowToDisk:设置基于内在的缓存中的对象数目达到上限后,是否把溢出的对象写到基于硬盘的缓存中

  • diskPersistent:当 jvm 结束时是否持久化对象 true false 默认是false

  • diskExpiryThreadIntervalSeconds:指定专门用于清除过期对象的监听线程的轮询时间

  • memoryStoreEvictionPolicy:当内存缓存达到最大,有新的 element 加入的时候,移除缓存中 element 的策略,默认是LRU(最近最少使用),可选的有LFU(最不常使用)和 FIFO(先进先出)

  • copyOnRead :判断从缓存中读取数据时是返回对象的引用还是复制一个对象返回。默认情况下是 false,即返回数据的引用,这种情况下返回的都是相同的对象,和 MyBatis 默认缓存中的只读对象是相同的。如果设置为 true,那就是可读写缓存,每次读取缓存时都会复制一个新的实例。

  • copyOnWrite :判断写入缓存时是直接缓存对象的引用还是复制一个对象然后缓存,默认也是 false。如果想使用可读写缓存,就需要将这两个属性配置为 true,如果使用只读缓存,可以不配置这两个属性,使用默认值 false 即可。

修改 RoleMapper.xml 中的缓存配置

修改 RoleMapper.xml 中的配置如下。

<mapper namespace="tk.mybatis.simple.mapper.RoleMapper">
    <!-- cache 其他属性不会起到任何作用 -->
    <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
</mapper>

ehcache.xml 只有一个默认的缓存配置,如果想针对某一个命名空间进行配置,需添加

<cache
    name="tk.mybatis.simple.mapper.RoleMapper"
    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

在 src/main/resources 目录下新增 redis.properties 文件

host=127.0.0.1
port=6379
connectionTimeout=5000
soTimeout=5000
password=
database=0
clientName=

修改 RoleMapper.xml 中的缓存配置

<mapper namespace="tk.mybatis.simple.mapper.RoleMapper">
    <cache type="org.mybatis.caches.redis.RedisCache"/>
</mapper>

脏数据的产生和避免

一个多表的查询就会缓存在某命名空间的二级缓存中。涉及这些表的增、删、改操作通常不在一个映射文件中,它们的命名空间不同,因此当有数据变化时,多表查询的缓存未必会被清空,这种情况下就会产生脏数据。

在 UserMapper.xml 添加二级缓存配置,增加 <cache/> 元素,SysUser 对象实现 Serializable 接口。

@Test
public void testDirtyData(){
    SqlSession sqlSession = getSqlSession();
    try {
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        SysUser user = userMapper.selectUserAndRoleById(1001L);
        Assert.assertEquals("普通用户", user.getRole().getRoleName());
        System.out.println("角色名:" + user.getRole().getRoleName());
    } finally {
        sqlSession.close();
    }

    // 开始另一个新的 session
    sqlSession = getSqlSession();
    try {
        RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
        SysRole role = roleMapper.selectById(2L);
        role.setRoleName("脏数据");
        roleMapper.updateById(role);
        sqlSession.commit();
    } finally {
        sqlSession.close();
    }

    System.out.println("开启新的 sqlSession");
    sqlSession = getSqlSession();
    try {
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
        SysUser user = userMapper.selectUserAndRoleById(1001L);
        SysRole role = roleMapper.selectById(2L);
        Assert.assertEquals("普通用户", user.getRole().getRoleName());
        Assert.assertEquals("脏数据", role.getRoleName());
        System.out.println("角色名:" + user.getRole().getRoleName());
        // 还原数据
        role.setRoleName("普通用户");
        roleMapper.updateById(role);
        sqlSession.commit();
    } finally {
        sqlSession.close();
    }
}

第一个 SqlSession 中获取了用户和关联的角色信息,第二个 SqlSession 中查询角色并修改了角色的信息,第三个 SqlSession 查询用户和关联的角色信息。这时从缓存中直接取出数据,就出现了脏数据,因为角色名称己经修改,但是这里读取到的角色名称仍然是修改前的名字,因此出现了脏读。

如何避免脏数据的出现?就需要用到参照缓存了。可以让几个会关联的表同时使用同一个二级缓存。修改 UserMapper.xml 的缓存配置。

<cache-ref namespace="tk.mybatis.simple.mapper.RoleMapper">

二级缓存适用场景

  • 查询为主的应用中,只有尽可能少的增、删、改操作。
  • 绝大多数以单表操作存在时,由于很少存在互相关联的情况,因此不会出现脏数据。
  • 可以按业务划分对表进行分组时,如关联的表比较少,可以通过参照缓存进行配置。

除了推荐使用的情况,如果脏读对系统没有影响,也可以考虑使用。在无法保证数据不出现脏读的情况下,建议在业务层使用可控制的缓存代替二级缓存。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 bin07280@qq.com

文章标题:MyBatis 10-缓存配置

文章字数:3.8k

本文作者:Bin

发布时间:2019-12-01, 15:53:26

最后更新:2019-12-03, 15:54:10

原始链接:http://coolview.github.io/2019/12/01/MyBatis/MyBatis%2010-%E7%BC%93%E5%AD%98%E9%85%8D%E7%BD%AE/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录