前言

  使用缓存可以使应用更快的获取数据,避免频繁的数据库交互,尤其是在查询越多、缓存命中率越高的情况下,使用缓存的作用就越明显。MyBatis作为持久化框架,提供了非常强大的查询缓存特性,可以非常方便地配置和定制使用。一般提到MyBatis缓存的时候,都是指二级缓存,一级缓存默认会启用,并且不能控制,因此很少会提到。不过,知道一级缓存的存在可以避免产生一些难以发现的错误。

一级缓存
SqlSession sqlSession = sessionFactory.openSession();
try{
TUserMapper tUserMapper = sqlSession.getMapper(TUserMapper.class);
TUser result1 = tUserMapper.selectByPrimaryKey(11L);
System.out.println("第一次查询:" + result1.getuName());
result1.setuName("cache");
TUser result2 = tUserMapper.selectByPrimaryKey(new Long(11));
System.out.println("第二次查询:" + result2.getuName());
tUserMapper.deleteByPrimaryKey(8L);
TUser result3 = tUserMapper.selectByPrimaryKey(new Long(11));
System.out.println("第三次查询:" + result3.getuName());
}finally {
sqlSession.close();
}

查询结果:

第一次查询:mh
第二次查询:cache
第三次查询:mh

说明:
  第一次执行selectByPrimaryKey方法查询时,真正执行了数据库查询,得到了result1的结果。第二次查询获取result2时,使用了MyBatis的一级缓存,没有查询数据库,直接从缓存中获取了结果。所以result1和result2是一个对象。
  MyBatis的一级缓存存在于SqlSession的生命周期中,在同一个SqlSession中查询时,MyBatis会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个Map对象中。如果同一个SqlSession中执行的方法和参数完全一致,那么通过算法会生成相同的键值,当Map缓存对象中已经存在该键值时,则会返回缓存中的对象。如果我们不想让selectByPrimaryKey使用一级缓存,可以对方法做如下修改:

<select id="selectByPrimaryKey" flushCache="true" resultMap="BaseResultMap">
select id, u_name, u_password
from t_user
where
id = #{id,jdbcType=BIGINT}
</select>

  设置了flushCache属性为true。这种方式表示在查询前清空一级缓存,会影响其他的查询,所以避免这么使用。
  另外,第三次查询时又真正执行了数据库查询,是因为前面执行了delete操作。任何的Insert、update、delete操作都会清空一级缓存。

二级缓存
  MyBatis的二级缓存非常强大,它不同于一级缓存只存在于SqlSession的生命周期中,而是可以理解为存在于SqlSessionFactory的生命周期中。

配置二级缓存
  MyBatis的二级缓存是默认开启的。也可以在mybatis-config.xml中显式声明,配置如下:

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

  如果将cacheEnabled设为false,即使有后面的二级缓存配置,也不会生效。
  MyBatis的二级缓存是和命名空间绑定的,即二级缓存需要配置在Mapper.xml映射文件中,或者配置在Mapper.java接口中。在映射文件中,命名空间就是XML根节点mapper的namespace属性。在接口中,命名空间就是接口的全限定名称。

Mapper.xml中配置二级缓存
  在保证二级缓存的全局配置开启的情况下,给TUserMapper.xml开启二级缓存只需要在其中添加<cache/>元素即可,添加后的TUserMapper.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="cn.jujianfei.demo.dao.TUserMapper">
<cache/>
..........
</mapper>

在Mapper接口中配置二级缓存
  当使用注解配合接口开发时,开启二级缓存就需要在接口上配置了。具体如下:

@CacheNamespace
public interface TUserMapper {
//接口方法
}

使用二级缓存
//TUserMapper.xml
<cache readOnly="false"></cache>

  cache的属性readOnly设置为flase表示此二级缓存为可读写缓存,可以使用SerializedCache序列化缓存。这个缓存类要求所有被序列化的对象必须实现Serializable(java.io.Serializable)接口,所以还需要修改TUser对象,代码如下:

public class TUser implements Serializable {
......
}

测试类
SqlSession sqlSession = sessionFactory.openSession();
try{
TUserMapper tUserMapper = sqlSession.getMapper(TUserMapper.class);
TUser result1 = tUserMapper.selectByPrimaryKey(11L);
System.out.println("第一次查询:" + result1.getuName());
result1.setuName("cache");
TUser result2 = tUserMapper.selectByPrimaryKey(new Long(11));
System.out.println("第二次查询:" + result2.getuName());
Assert.assertEquals(result1,result2);
}finally {
sqlSession.close();
}

sqlSession = sessionFactory.openSession();
try{
TUserMapper tUserMapper = sqlSession.getMapper(TUserMapper.class);
TUser result3 = tUserMapper.selectByPrimaryKey(new Long(11));
System.out.println("第三次查询:" + result3.getuName());
TUser result4 = tUserMapper.selectByPrimaryKey(new Long(11));
System.out.println("第四次查询:" + result4.getuName());
Assert.assertNotEquals(result4,result3);
}finally {
sqlSession.close();
}

打印结果:

第一次查询:mh
第二次查询:cache
第三次查询:cache
第四次查询:cache

  由于配置的是可读写的缓存,而MyBatis使用SerializedCache序列化缓存来实现可读写缓存类,并通过序列化和反序列化来保证通过缓存获取数据时,得到的是一个新的实例。如果配置为只读缓存,MyBatis就会使用Map来存储缓存值,这种情况下,从缓存中获取的对象就是同一个实例。
  第一次查询,是从数据库查询;第二次查询是从一级缓存查询,所以第一次和第二次查询是同一个实例。当调用close方法关闭SqlSession时,SqlSession才会保存查询数据到二级缓存中。在这之后二级缓存才有了数据。第三次查询就是从二级缓存中获取数据了。第四次查询也是从二级缓存中获取数据。result3和result4都是反序列化得到的结果,所以它们不是相同的实例。在这一部分,这两个实例是读写安全的,其属性不会互相影响。
  实际上,示例代码并不安全。result1.setuName("cache");这里修改result1的属性值后,按照常理应该更新数据,更新后会清空一、二级缓存,但是没有。导致后续查询结果都是cache。所有要想安全使用,需要避免毫无意义的修改。这样就可以避免人为产生的脏数据,避免缓存和数据库的数据不一致。
  MyBatis默认提供的缓存实现时基于Map实现的内存缓存,已经可以满足基本的应用。但是当需要缓存大量的数据时,不能仅仅通过提高内存来使用MyBatis的二级缓存,还可以选择一些类似EhCache的缓存框架或Redis缓存数据库等工具来保存MyBatis的二级缓存数据。

脏数据的产生和避免
  二级缓存虽然能提高应用效率,减轻数据库服务器的压力,但是如果使用不当,很容易产生脏数据,影响使用效果。

产生

  MyBatis的二级缓存是和命名空间绑定的,所以通常情况下每一个Mapper映射文件都拥有自己的二级缓存,不同Mapper的二级缓存互不影响。在常见的数据库操作中,多表联查非常常见,而多表联查肯定会将该查询放到某个命名空间下的映射文件中,这样一个多表的查询就会缓存在该命名空间的二级缓存中。涉及到这些表的增、删、改操作通常不在一个映射文件中,它们的命名空间不同,因此当有数据变化时,多表查询的缓存未必会被清空,这种情况下就会产生脏数据。

避免

  该如何避免脏数据的出现呢?这时就需要用到参照缓存了。当某几个表可以作为一个业务整体时,通常是让几个会关联的ER表同时使用同一个二级缓存,这样就能解决脏数据问题。配置如下:


<mapper namespace="cn.jujianfei.demo.dao.TUserMapper">
    <cache readOnly="false"></cache>
    ......
</mapper>

<mapper namespace="cn.jujianfei.demo.dao.UserMapper">
    <cache-ref namespace="cn.jujianfei.demo.dao.TUserMapper"/>
    ......
</mapper>

  虽然这样可以解决脏数据的问题,但是并不是所有的关联查询都可以这么解决,如果有几十个表甚至所有表都以不同的关联关系存在于各自的映射文件中时,使用参照缓存显然没有意义。

二级缓存适用的场景
以查询为主的应用中,只有尽可能少的增、删、改操作。
绝大多数以单表操作存在时,由于很少存在互相关联的情况,因此不会出现脏数据。
可以按业务划分对表进行分组时,如关联的表比较少,可以通过参照缓存进行配置。
除了推荐使用的情况,如果脏读对系统没有影响,也可以考虑使用。在无法保证数据不出现脏读的情况下,建议在业务层使用可控制的缓存代替二级缓存。

总结语

  更新操作会清空一、二级缓存。