二级缓存

在 MyBatis 的全局配置中有一个参数 cacheEnabled这个参数是二级缓存的全局开关,默认值是 true,初始状态为启用状态。如果把这个参数设置为 false,即使有后面的二级缓存配置,也不会生效。

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