二级缓存
MyBatis 的二级缓存是和命名空间绑定的,即二级缓存需要配置在 Mapper.xml 映射文件中,或者配置在 Mapper.java 接口中。
在映射文件中,命名空间就是 XML 根节点 mapper 的 namespace 属性。
在 Mapper 接口中,命名空间就是接口的全限定名称。
在 Mapper.xml 中配置二级缓存
在保证二级缓存的全局配置开启的情况下,给 CountryMapper.xml 开启二级缓存只需要在CountryMapper.xml 中添加 <cache/> 元素即可:
<mapper namespace="com.study.mybatis.mapper.CountryMapper">
<cache/>
<!-- 其他配置 -->
</mapper>
cache 可以配置的属性如下:
eviction(回收策略)
LRU(最近最少使用):移除最长时间不被使用的对象,这是默认值。
FIFO(先进先出):按对象进入缓存的顺序来移除它们。
SOFT(软引用):移除基于垃圾回收器状态和软引用规则的对象。
WEAK(弱引用):更积极地移除基于垃圾收集器状态和弱引用规则的对象。
flushInterval(刷新间隔):可以被设置为任意的正整数,代表一个合理的毫秒形式的时间段。默认情况不设置,即没有刷新间隔,缓存仅仅在调用语句时刷新。
size(缓存大小):可以被设置为任意正整数。默认值是 1024,表示缓存会存储 1024 个缓存对象。
readOnly(只读):可以被设置为 true 或 false。
只读的缓存会给所有调用者返回缓存对象的相同实例,因此这些对象不能被修改,这提供了很重要的性能优势。
可读写的缓存会通过序列化返回缓存对象的拷贝,这种方式会慢一些,但是安全,因此默认是 false。
因为可读写的缓存会通过 Java 的序列化机制返回缓存对象的拷贝,因此,缓存对象必须实现 Serilizable 接口。
在 Mapper 接口中配置二级缓存
当只使用注解方式配置二级缓存时,如果在 CountryMapper 接口中,则需要增加如下配置:
@CacheNamespace
public interface CountryMapper {
...
}
只需要增加 @CacheNamespace 注解即可,该注解同样可以配置各项属性:
eviction(回收策略)
flushInterval(刷新间隔)
size(缓存大小)
readWrite(读写)
@CacheNamespace 注解中的各属性与标签中的属性功能类似,除了 readWrite 与 readOnly 这对反义词外,其他属性的用法完全一致。
同时在 Mapper.xml 和 Mapper 接口配置二级缓存
当同时使用注解方式和 XML 映射文件时,如果同时配置了上述的二级缓存,就会抛出如下异常:
java.lang.IllegalArgumentException: Caches collection already contains value for com.study.mybatis.mapper.CountryMapper
这个时候应该使用参照缓存。
在 Mapper 接口中,参照缓存配置如下:
@CacheNamespaceRef(CountryMapper.class) public interface CountryMapper { ... }
或者,在 Mapper.xml 中使用配置参照缓存。
MyBatis 中很少会同时使用 Mapper 接口注解方式和 XML 映射文件,所以参照缓存并不是为了解决这个问题而设计的。参照缓存除了能够通过引用其他缓存减少配置外,主要的作用是解决脏读。
示例
@Autowired
private SqlSessionFactory sqlSessionFactory;
@Test
public void testL2Cache2() {
Country country1;
// 获取 sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
CountryMapper countryMapper = sqlSession.getMapper(CountryMapper.class);
// 获取id为1的国家
// 在此处放入缓存,缓存中存放的是一个拷贝
country1 = countryMapper.selectByPrimaryKey(1);
// 修改获取到的对象的name实例字段
country1.setName("NEW NAME");
} finally {
// 关闭sqlSession时,将对象存入缓存
sqlSession.close();
}
sqlSession = sqlSessionFactory.openSession();
try {
CountryMapper countryMapper = sqlSession.getMapper(CountryMapper.class);
Country country2 = countryMapper.selectByPrimaryKey(1);
// 因为 缓存 返回的是 对象的拷贝,因此对象引用并不相等
Assert.assertNotEquals(country1, country2);
// 命中 二级缓存
// 脏读!!! 缓存出现了不一致
Assert.assertEquals("NEW NAME", country2.getName());
} finally {
sqlSession.close();
}
}
@Autowired
private CountryMapper countryMapper;
@Test
public void testL2Cache() {
// 获取id为1的国家
// 在此处放入缓存,缓存中存放的是一个拷贝
Country country1 = countryMapper.selectByPrimaryKey(1);
// 修改获取到的对象的name实例字段
country1.setName("NEW NAME");
// 再次获取id为1的国家, 命中缓存
Country country2 = countryMapper.selectByPrimaryKey(1);
// 因为 缓存 返回的是 对象的拷贝,因此对象引用并不相等
Assert.assertNotEquals(country1, country2);
// 正常
Assert.assertNotEquals("NEW NAME", country2.getName());
}
上面给出了两个示例,程序的逻辑完全一样,但是对于 country2 的 name 值却完全不同。
造成这种区别的原因在于:当调用 close 方法关闭 SqlSession 时,SqlSession 才会保存查询数据到二级缓存中。因此,第一个测试程序会在执行完
country1.setName("NEW NAME");
之后才将 country1 保存到二级缓存中,而第二个测试程序则在countryMapper.selectByPrimaryKey(1)
执行完后立刻便将 country1 保存到了二级缓存中。通过上面两个示例程序,可以发现,进入二级缓存的是原始对象的一个拷贝,并不会造成指针泄露。
MyBatis 默认提供的缓存实现是基于 Map 实现的内存缓存,已经可以满足基本的应用。但是当需要缓存大量的数据时,不仅仅能通过提高内存来使用 MyBatis 的二级缓存,还可以选择一些类似 EhCache 的缓存框架或 Redis 缓存数据库等工具来保存 MyBatis 的二级缓存数据。
Last updated