1、一级缓存
@Test
public void testL1Cache() {
//获取SqlSession
SqlSession sqlSession = getSqlSession();
SysUser user1 = null;
try {
//获取UserMapper接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//查询id=1的用户
user1 = userMapper.selectById(1l);
//重新赋值
user1.setUserName("New Name");
//再次查询id相同的用户
SysUser user2 = userMapper.selectById(1l);
//user1和user2是同一个对象
Assert.assertEquals("New Name", user2.getUserName());
Assert.assertEquals(user1, user2);
} finally {
sqlSession.close();
}
//开启新的sqlSession
sqlSession = getSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//查询id=1的用户
SysUser user2 = userMapper.selectById(1l);
//user2的name为admin
System.out.println(user2.getUserName());//admin
//user2和上面的user1是两个不同的实例
System.out.println(user1 == user2); //false
//执行删除操作
userMapper.deleteById(2l);
//获取user3
SysUser user3 = userMapper.selectById(1l);
//user2和user3是两个不同的实例
System.out.println(user2);
System.out.println(user3);
System.out.println(user2 == user3);//false
} finally {
sqlSession.close();
}
}
在第一次执行selectById方法获取SysUser数据时,真正执行了数据库查询,得到了user1的结果。第二次执行获取user2的时候,从日志可以看到,在“开启新的sqlSession”
这行日志上面,只有一次查询,也就是说第二次查询并没有执行数据库操作。
从测试代码来看,获取user1后重新设置了userName的值,之后没有进行任何更新数据库的操作。在获取user2对象后,发现user2对象的userName值竟然和user1重新设
置后的值一样。再往下可以发现,原来user1和user2竟然是同一个对象,之所以这样就是因为MyBatis的一级缓存。
MyBatis的一级缓存存在于SqlSession的生命周期中,在同一个SqlSession中查询时,MyBatis会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个Map对象中。如果同一个SqlSession中执行的方法和参数完全一致,那么通过算法会生成相同的键值,当Map缓存对象中已经存在该键值时,则会返回缓存中的对象。
缓存中的对象和我们得到的结果是同一个对象,反复使用相同参数执行同一个方法时,总是返回同一个对象,因此就会出现上面测试代码中的情况。在使用MyBatis的过程中,要避免在使用如上代码中的user2时出现的错误。我们可能以为获取的user2应该是数据库中的数据,却不知道user1的一个重新赋值会影响到user2。如果不想让selectById方法使用一级缓存,可以对该方法做如下修改。
<select id="selectById" resultMap="userMap" flushCache="true">
select * from sys_user where id = #{id}
</select>
该修改在原来方法的基础上增加了f1 ushCache:=“true”,这个属性配置为true后,会在查询数据前清空当前的一级缓存,因此该方法每次都会重新从数据库中查询数据,此时的user2和user1就会成为两个不同的实例,可以避免上面的问题。但是由于这个方法清空了一级缓存,会影响当前SqlSession中所有缓存的查询,因此在需要反复查询获取只读数据的情况下,会增加数据库的查询次数,所以要避免这么使用。
在关闭第一个SqlSession后,又重新获取了一个SqlSession,因此又重新查询了user2,这时在日志中输出了数据库查询SQL,user2是一个新的实例,和user1没有任何
关系。这是因为一级缓存是和SqlSession绑定的,只存在于SqlSession的生命周期中。
接下来执行了一个deleteById操作,然后使用相同的方法和参数获取了user3实例,从日志和结果来看,user3和user2也是完全不同的两个对象。这是因为任何的INSERT、
UPDATE、DELETE操作都会清空一级缓存,所以查询user3的时候由于缓存不存在,就会再次执行数据库查询获取数据。
2、二级缓存
MyBatis的二级缓存非常强大,它不同于一级缓存只存在于SqlSession的生命周期中,而是可以理解为存在于SqlSessionFactory的生命周期中。虽然目前还没接触过同时存在多个SqlSessionFactory的情况,但可以知道,当存在多个SqlSessionFactory时,它们的缓存都是绑定在各自对象上的,缓存数据在一般情况下是不相通的。只有在使用如Rdis这样的缓存数据库时,才可以共享缓存。
2.1 配置二级缓存
首先从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接口中,命名空间就是接口的全限定名称。
xml中配置二级缓存:
在保证二级缓存的全局配置开启的情况下,给RoleMapper…xml开启二级缓存只需要在
UserMapper.xml中添加<cache/>元素即可,添加后的UserMapper…xml如下。
<mapper namespace="pers.zhang.simple.mapper.UserMapper">
<cache/>
<!-- 其它配置 -->
</mapper>
默认的二级缓存会有如下效果。
- 映射语句文件中的所有SELECT语句将会被缓存。
- 映射语句文件中的所有NSERT、UPDATE、DELETE语句会刷新缓存。
- 缓存会使用Least Recently Used(LRU,最近最少使用的)算法来收回。
- 根据时间表(如no Flush Interval,没有刷新间隔),缓存不会以任何时间顺序来刷新。
- 缓存会存储集合或对象(无论查询方法返回什么类型的值)的1024个引用。
- 缓存会被视为read/write(可读/可写)的,意味着对象检索不是共享的,而且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
所有的这些属性都可以通过缓存元素的属性来修改,示例如下。
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
这个更高级的配置创建了一个FIF0缓存,并每隔60秒刷新一次,存储集合或对象的512个引用,而且返回的对象被认为是只读的,因此在不同线程中的调用者之间修改它们会导致冲突。
cache可以配置的属性如下。
- eviction(收回策略)
- LRU(最近最少使用的):移除最长时间不被使用的对象,这是默认值。
- FIF0(先进先出):按对象进入缓存的顺序来移除它们。
- S0FT(软引用):移除基于垃圾回收器状态和软引用规则的对象。
- WEAK(弱引用):更积极地移除基于垃圾收集器状态和弱引用规则的对象。
- flushInterval(刷新间隔)。可以被设置为任意的正整数,而且它们代表一个合理的毫秒形式的时间段。默认情况不设置,即没有刷新间隔,缓存仅仅在调用语句时刷新。
- size(引用数目)。可以被设置为任意正整数,要记住缓存的对象数目和运行环境的可用内存资源数目。默认值是1024。
- readOnly(只读)。属性可以被设置为true或false。只读的缓存会给所有调用者返回缓存对象的相同实例,因此这些对象不能被修改,这提供了很重要的性能优势。可读写的缓存会通过序列化返回缓存对象的拷贝,这种方式会慢一些,但是安全,因此默认是false。
Mapper接口中配置二级缓存:
在使用注解方式时,如果想对注解方法启用二级缓存,还需要在Mapper接口中进行配置,如果Mapper接口也存在对应的XML映射文件,两者同时开启缓存时,还需要特殊配置。
当只使用注解方式配置二级缓存时,如果在RoleMapper接口中,则需要增加如下配置。
@CacheNamespace(
eviction = FifoCache.class,
flushInterval = 60000,
size = 512,
readWrite = true
)
public interface RoleMapper {
}
当同时使用注解方式和XML映射文件时,如果同时配置了上述的二级缓存,就会抛出异常。
这是因为Mapper接口和对应的XML文件是相同的命名空间,想使用二级缓存,两者必须同时配置(如果接口不存在使用注解方式的方法,可以只在XML中配置),因此按照上面的方式进行配置就会出错,这个时候应该使用参照缓存。
@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="pers.zhang.simple.mapper.RoleMapper">
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="false" />
</mapper>
2.2 使用二级缓存
由于配置的是可读可写的缓存,而MyBatis使用SerializedCache (org.apache.ibatis.cache.decorators.SerializedCache)
序列化缓存来实现可读写缓存类,并通过序列化和反序列化来保证通过缓存获取数据时,得到的是一个新的实例。因此,如果配置为只读缓存,MyBatis就会使用Map来存储缓存值,这种情况下,从缓存中获取的对象就是同一个实例。
因为使用可读写缓存,可以使用SerializedCache序列化缓存。这个缓存类要求有被序列化的对象必须实现Serializable(java.io.Serializable)接口,所以还需要
修改SysRole对象,代码如下。
public class SysRole implements Serializable {
}
测试类:
日志:
DEBUG [main] - Cache Hit Ratio [pers.zhang.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.0
DEBUG [main] - <== Total: 1
DEBUG [main] - Cache Hit Ratio [pers.zhang.simple.mapper.RoleMapper]: 0.0
true
开启新的sqlSession
DEBUG [main] - Cache Hit Ratio [pers.zhang.simple.mapper.RoleMapper]: 0.3333333333333333
New Name
false
DEBUG [main] - Cache Hit Ratio [pers.zhang.simple.mapper.RoleMapper]: 0.5
false
日志中存在好几条以Cache Hit Ratio开头的语句,这行日志后面输出的值为当前执行方法的缓存命中率。在测试第一部分中,第一次查询获取ro1e1的时候由于没有缓存,所以执行了数据库查询。在第二个查询获取ro1e2的时候,ro1e2和role1是完全相同的实例,这里使用的是一级缓存,所以返回同一个实例。
当调用close方法关闭SqlSession时,SqlSession才会保存查询数据到二级缓存中。在这之后二级缓存才有了缓存数据。所以可以看到在第一部分的两次查询时,命中率都是0。
在第二部分测试代码中,再次获取ro1e2时,日志中并没有输出数据库查询,而是输出了命中率,这时的命中率是0.3333333333333333。这是第3次查询,并且得到了缓存的值,因此该方法一共被请求了3次,有1次命中,所以命中率就是三分之一。后面再获取ro1e3的时候,就是4次请求,2次命中,命中率为0.5。并且因为可读写缓存的缘故,ro1e2和ro1e3都是反序列化得到的结果,所以它们不是相同的实例。在这一部分,这两个实例是读写安全的,其属性不会互相影响。
MyBatis默认提供的缓存实现是基于Map实现的内存缓存,已经可以满足基本的应用。但是当需要缓存大量的数据时,不能仅仅通过提高内存来使用MyBatis的二级缓存,还可以选择一些类似EhCache的缓存框架或Redis缓存数据库等工具来保存MyBatis的二级缓存数据。
3、集成EhCache缓存
EhCache是一个纯粹的Java进程内的缓存框架,具有快速、精干等特点。具体来说,EhCache主要的特性如下。
- 快速。
- 简单。
- 多种缓存策略。
- 缓存数据有内存和磁盘两级,无须担心容量问题。
- 缓存数据会在虚拟机重启的过程中写入磁盘。
- 可以通过RMI、可插入API等方式进行分布式缓存。
- 具有缓存和缓存管理器的侦听接口。
- 支持多缓存管理器实例以及一个实例的多个缓存区域。
3.1 添加依赖
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-echcache</artifactId>
<version>1.0.3</version>
</dependency>
3.2 配置EhCache
在src/main/resources目录下新增echcache.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>
上面的配置中重点要看两个属性,copyOnRead和copyOnWrite属性。这两个属性的配置会对后面使用二级缓存产生很大影响。
copyOnRead的含义是,判断从缓存中读取数据时是返回对象的引用还是复制一个对象返回。默认情况下是false,即返回数据的引用,这种情况下返回的都是相同的对象,和MyBatis默认缓存中的只读对象是相同的。如果设置为true,
那就是可读写缓存,每次读取缓存时都会复制一个新的实例。
copyOnWrite的含义是,判断写入缓存时是直接缓存对象的引用还是复制一个对象然后缓存,默认也是false。如果想使用可读写缓存,就需要将这两个属性配置为true,如果使
用只读缓存,可以不配置这两个属性,使用默认值fa1se即可。
3.3 修改RoleMapper.xml中的缓存配置
ehcache-cache提供了如下2个可选的缓存实现。
- org.mybatis.caches.ehcache.EhcacheCache
- org.mybatis.caches.ehcache.LoggingEhcache
在这两个缓存中,第二个是带日志的缓存,由于MyBatis初始化缓存时,如果Cache不是
LoggingEhcache(org.mybatis.caches.ehcache.LoggingEhcache),MyBatis便会使用Logging Ehcache装饰代理缓存,所以上面两个缓存使用时并没有区别,都会输出缓存命中率的日志。
修改RoleMapper.xml中的配置如下。
<mapper namespace="tk.mybatis.simple.mapper.RoleMapper">
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
<!--其他配置-->
</mapper>
只通过设置type属性就可以使用EhCache缓存了,这时cache的其他属性都不会起到任何作用,针对缓存的配置都在ehcache.xml中进行。在ehcache.xml配置文件中,只有一个默认的缓存配置,所以配置使用EhCache缓存的Mapper映射文件都会有一个以映射文件命名空间
命名的缓存。如果想针对某一个命名空间进行配置,需要在ehcache.xml中添加一个和映射文件命名空间一致的缓存配置,例如针对RoleMapper,可以进行如下配置。
<cache
name="tk.mybatis.simple.mapper.RoleMapper"
maxElementsInMemory="3000"
eternal="false"
copyonRead="true"
copyonWrite="true"
timeToIdleSeconds="3600"
timeToLiveSeconds="3600"
overflowToDisk="true"
diskPersistent="true"/>
4、集成Redis缓存
Redis是一个高性能的key-value数据库。
MyBatis项目开发者提供了Redis的MyBatis二级缓存实现,该项目名为redis-cache,目前只有beta版本,项目地址是https:/∥github.com/mybatis/redis-cache。
4.1 添加项目依赖
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
4.2 配置Redis
Redis服务启动后,在src/main/resources目录下新增redis…properties文件。
host=localhost
p0rt=6379
connectionTimeout=5000
soTimeout=5000
password=
database=0
上面这几项是redis-cache项目提供的可以配置的参数,这里配置了服务器地址、端口和超时时间。
4.3 修改RoleMapper.xml中的缓存配置
redis-cache提供了1个Mybatis的缓存实现,org.mytis.caches.redis.RedisCache。
修改RoleMapper.xml中的配置如下。
<mapper namespace="tk.mybatis.simple.mapper.RoleMapper">
<cache type="org.mybatis.caches.redis.RedisCache"/>
<!--其他配置-->
</mapper>
配置依然很简单,RedisCache在保存缓存数据和获取缓存数据时,使用了Java的序列化和反序列化,因此还需要保证被缓存的对象必须实现Serializable接口。改为RedisCache缓存配置后,testL2Cache测试第一次执行时会全部成功,但是如果再次执行,就会出错。
这是因为Redis作为缓存服务器,它缓存的数据和程序(或测试)的启动无关,Redis的缓存并不会因为应用的关闭而失效。所以再次执行时没有进行一次数据库查询,所有查询都使用缓存,测试的第一部分代码中的ro1e1和role2都是直接从二级缓存中获取数据,因为是可读写缓存,所以不是相同的对象。
当需要分布式部署应用时,如果使用MyBatis自带缓存或基础的EhCahca缓存,分布式应用会各自拥有自己的缓存,它们之间不会共享缓存,这种方式会消耗更多的服务器资源。如果使用类似Redis的缓存服务,就可以将分布式应用连接到同一个缓存服务器,实现分布式应用间的缓存共享。
5、二级缓存适用场景
二级缓存虽然好处很多,但并不是什么时候都可以使用。在以下场景中,推荐使用二级缓存。
- 以查询为主的应用中,只有尽可能少的增、删、改操作。
- 绝大多数以单表操作存在时,由于很少存在互相关联的情况,因此不会出现脏数据。
- 可以按业务划分对表进行分组时,如关联的表比较少,可以通过参照缓存进行配置。
除了推荐使用的情况,如果脏读对系统没有影响,也可以考虑使用。在无法保证数据不出
现脏读的情况下,建议在业务层使用可控制的缓存代替二级缓存。