二级缓存
MyBatis 的二级缓存非常强大,它不同于一级缓存只存在于 SqlSession 的生命周期中,而是可以理解为存在于 SqlSessionFactory 的生命周期中。虽然目前还没接触过同时存在多个 SqlSessionFactory 的情况,但可以知道,当存在多个 SqlSessionFactory 时,它们的缓存都是绑定在各自对象上的,缓存数据在一般情况下是不相通的。只有在使用如 Redis 这样的缓存数据库时,才可以共享缓存。
配置二级缓存
首先从 MyBatis 最简单的二级缓存配置开始。在 MyBatis 的全局配置 settings 中有一个参数 cacheEnabled,这个参数是二级缓存的全局开关,默认值是 true,初始状态为启用状态。如果把这个参数设置为 false,即使有后面的二级缓存配置,也不会生效。由于这个参数值默认为 true,所以不必配置,如果想要配置,可以在 mybatis-config.xml 中添加如下代码。
<settings>
<!--其他配置-->
<setting name="cacheEnabled" value="true" />
</settings>
MyBatis 的二级缓存是和命名空间绑定的,即二级缓存需要配置在 Mapper.xml 映射文件中,或者配置在 Mapper.java 接口中。在映射文件中,命名空间就是 XML 根节点 mapper 的 namespace 属性。在 Mapper 接口中,命名空间就是接口的全限定名称。
Mapper.xml中配置二级缓存
在保证二级缓存的全局配置开启的情况下,给 UserMapper.xml 开启二级缓存只需要在 UserMapper.xml 中添加 <cache/> 元素即可,添加后的 UserMapper.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="tk.mybatis.simple.mapper.UserMapper">
<cache/>
<!--其他配置-->
</mapper>
默认的二级缓存会有如下效果。
-
映射语句文件中的所有 SELECT 语句将会被缓存。
-
映射语句文件中的所有 INSERT、UPDATE、DELETE 语句会刷新缓存。
-
缓存会使用 Least Recently Used(LRU,最近最少使用的)算法来收回。
-
根据时间表(如 no Flush Interval,没有刷新间隔),缓存不会以任何时间顺序来刷新。
-
缓存会存储集合或对象(无论查询方法返回什么类型的值)的 1024 个引用。
-
缓存会被视为 read/write(可读/可写)的,意味着对象检索不是共享的,而且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
所有的这些属性都可以通过缓存元素的属性来修改,示例如下。
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true" />
这个更高级的配置创建了一个 FIFO 缓存,并每隔 60 秒刷新一次,存储集合或对象的 512 个引用,而且返回的对象被认为是只读的,因此在不同线程中的调用者之间修改它们会导致冲突。cache 可以配置的属性如下。
-
eviction(收回策略)
-
LRU(最近最少使用的):移除最长时间不被使用的对象,这是默认值。
-
FIFO(先进先出):按对象进入缓存的顺序来移除它们。
-
SOFT(软引用):移除基于垃圾回收器状态和软引用规则的对象。
-
WEAK(弱引用):更积极地移除基于垃圾收集器状态和弱引用规则的对象。
-
-
flushInterval(刷新间隔)。可以被设置为任意的正整数,而且它们代表一个合理的毫秒形式的时间段。默认情况不设置,即没有刷新间隔,缓存仅仅在调用语句时刷新。
-
size(引用数目)。可以被设置为任意正整数,要记住缓存的对象数目和运行环境的可用内存资源数目。默认值是 1024。
-
readOnly(只读)。属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例,因此这些对象不能被修改,这提供了很重要的性能优势。可读写的缓存会通过序列化返回缓存对象的拷贝,这种方式会慢一些,但是安全,因此默认是 false。
Mapper接口中配置二级缓存
在使用注解方式时,如果想对注解方法启用二级缓存,还需要在 Mapper 接口中进行配置,如果 Mapper 接口也存在对应的 XML 映射文件,两者同时开启缓存时,还需要特殊配置。
当只使用注解方式配置二级缓存时,如果在 RoleMapper 接口中,则需要增加如下配置。
@CacheNamesapce
public interface RoleMapper {
// 接口方法
}
只需要增加 @CacheNamespace(org.apache.ibatis.annotations.CacheNamespace) 注解即可,该注解同样可以配置各项属性,配置示例如下。
@CacheNamespace(
eviction = FifoCache.class,
flushInterval = 60000,
size = 512,
readWrite = true
)
这里的 readWrite 属性和 XML 中的 readOnly 属性一样,用于配置缓存是否为只读类型,在这里 true 为读写,false 为只读,默认为 true。
当同时使用注解方式和 XML 映射文件时,如果同时配置了上述的二级缓存,就会抛出如下异常。
这是因为 Mapper 接口和对应的 XML 文件是相同的命名空间,想使用二级缓存,两者必须同时配置(如果接口不存在使用注解方式的方法,可以只在 XML 中配置),因此按照上面的方式进行配置就会出错,这个时候应该使用参照缓存。在 Mapper 接口中,参照缓存配置如下。
@CacheNamespaceRef(RoleMapper.class)
public interface RoleMapper {
}
因为想让 RoleMapper 接口中的注解方法和 XML 中的方法使用相同的缓存,因此使用参照缓存配置 RoleMapper.class,这样就会使用命名空间为 tk.mybatis.simple.mapper.RoleMapper 的缓存配置,即 RoleMapper.xml 中配置的缓存。
Mapper 接口可以通过注解引用 XML 映射文件或者其他接口的缓存,在 XML 中也可以配置参照缓存,如可以在 RoleMapper.xml 中进行如下修改。
<cache-ref namespace="tk.mybatis.simple.mapper.RoleMapper" />
这样配置后,XML 就会引用 Mapper 接口中配置的二级缓存,同样可以避免同时配置二级缓存导致的冲突。
MyBatis 中很少会同时使用 Mapper 接口注解方式和 XML 映射文件,所以参照缓存并不是为了解决这个问题而设计的。参照缓存除了能够通过引用其他缓存减少配置外,主要的作用是解决脏读(后面章节详细介绍)。
为了保证后续测试一致,对 RoleMapper 接口和 XML 映射文件进行如下配置。
@CacheNamespaceRef(RoleMapper.class)
public interface RoleMapper {
// 其它接口
}
<mapper namespace="tk.mybatis.simple.mapper.RoleMapper">
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="false" />
<!--其他方法-->
</mapper>
使用二级缓存
上一节讲到,对 RoleMapper 配置二级缓存后,当调用 RoleMapper 所有的 select 查询方法时,二级缓存就已经开始起作用了。需要注意的是,由于配置的是可读写的缓存,而 MyBatis 使用 SerializedCache(org.apache.ibatis.cache.decorators.SerializedCache) 序列化缓存来实现可读写缓存类,并通过序列化和反序列化来保证通过缓存获取数据时,得到的是一个新的实例。因此,如果配置为只读缓存,MyBatis 就会使用 Map 来存储缓存值,这种情况下,从缓存中获取的对象就是同一个实例。
因为使用可读写缓存,可以使用 SerializedCache 序列化缓存。这个缓存类要求所有被序列化的对象必须实现 Serializable(java.io.Serializable) 接口,所以还需要修改 SysRole 对象,代码如下。
public class SysRole implements Serializable{
private static final long serialVersionUID = 335857052537245985L;
// 其它属性和 getter,setter 方法
}
做好所有准备后,编写一个测试来查看二级缓存的效果,测试代码如下。
@Test
public void testL2Cache() {
//获取 sqlSession
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.assertNotEquals(role1, role2);
} finally {
//关闭当前的 sqlSession
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
sqlSession.close();
}
}
这个测试仍然比较长,先执行测试输出日志,日志内容如下。
日志中存在好几条以 Cache Hit Ratio 开头的语句,这行日志后面输出的值为当前执行方法的缓存命中率。在测试第一部分中,第一次查询获取 role1 的时候由于没有缓存,所以执行了数据库查询。在第二个查询获取 role2 的时候,role2 和 role1 是完全相同的实例,这里使用的是一级缓存,所以返回同一个实例。
当调用 close 方法关闭 SqlSession 时,SqlSession 才会保存查询数据到二级缓存中。在这之后二级缓存才有了缓存数据。所以可以看到在第一部分的两次查询时,命中率都是 0。
在第二部分测试代码中,再次获取 role2 时,日志中并没有输出数据库查询,而是输出了命中率,这时的命中率是 0.3333333333333333。这是第 3 次查询,并且得到了缓存的值,因此该方法一共被请求了 3 次,有 1 次命中,所以命中率就是三分之一。后面再获取 role3 的时候,就是 4 次请求,2 次命中,命中率为 0.5。并且因为可读写缓存的缘故,role2 和 role3 都是反序列化得到的结果,所以它们不是相同的实例。在这一部分,这两个实例是读写安全的,其属性不会互相影响。
在这个例子中并没有真正的读写安全,为什么? 因为这个测试中加入了一段不该有的代码,即 role1.setRoleName("New Name");,这里修改 role1 的属性值后,按照常理应该更新数据,更新后会清空一、二级缓存,这样在第二部分的代码中就不会出现查询结果的 roleName 都是 "New Name" 的情况了。所以想要安全使用,需要避免毫无意义的修改。这样就可以避免人为产生的脏数据,避免缓存和数据库的数据不一致。 |
MyBatis 默认提供的缓存实现是基于 Map 实现的内存缓存,已经可以满足基本的应用。但是当需要缓存大量的数据时,不能仅仅通过提高内存来使用 MyBatis 的二级缓存,还可以选择一些类似 EhCache 的缓存框架或 Redis 缓存数据库等工具来保存 MyBatis 的二级缓存数据。接下来两节,我们会介绍两个常见的缓存框架。