概述
一般提到MyBatis缓存的时候,都是指二级缓存。一级缓存( 也叫本地缓存〉默认会启用,并且不能控制,因此很少会提到。本文简单介绍MyBatis一级缓存,了解MyBatis一级缓存可以避免产生一些难以发现的错误。后面介绍MyBatis二级缓存,包括二级缓存的基本配置用法,还有一些常用缓存框架和缓存数据库的结合。除此之外还会介绍二级缓存的适用场景,以及如何避免产生脏数据。
一、Mybatis一级缓存
先来一个示例,看看Mybatis一级缓存如何起作用:
public void testLlCache() {
// 获取SqlSession
SqlSession sqlSession = getSqlSession();
SysUser userl = null;
try {
//获取UserMapper 接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 调用selectByid 方法,查询id = 1 的用户
userl = userMapper.selectByid(lL);
//对当前获取的对象重新赋值
userl.setUserName(" New Name ");
//再次查询获取id 相同的用户
SysUser user2 = userMapper.selectByid(lL);
//虽然没有更新数据库,但是这个用户名和userl 重新赋值的名字相同
Assert.assertEquals(" New Name ", user2.getUserName());
// 无论如何, user 2 和userl 完全就是同一个实例
Assert.assertEquals(userl, user2);
} finally {
//关闭当前的sqlSessio 口
sqlSession.close();
System.out.println("开启新的sqlSession ");
//开始另一个新的session
sqlSession = getSqlSession();
try {
//获取UserMapper 接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//调用selectByid 方法,查询id = 1 的用户
SysUser user2 = userMapper.selectByid(lL);
// ;第二个session 获取的用户名仍然是admin
Assert.assertNotEquals(" New Name ", user2.getUserName());
//这里的us er2 和前一个session 查询的结采是两个不同的实例
Assert.assertNotEquals(userl, user2);
//执行7i11J 除操作
userMapper.deleteByid(2 L);
//获取user3
SysUser user3 = userMapper.selectByid(lL);
//这里的user2 和user3 是两个不同的实例
Assert.assertNotEquals(user2, user3);
} finally {
//关闭sqlSession
sqlSession.close();
}
}
}
查看日志可以知道:开启新sqlSession前的两次查询,user1查询了数据库,而第二句查询出来的user2,根本就没有查询,并且user2的值是user1设置后的值,所以user2只是use1r的引用。user1和user2是一个对象,原因是Mybatis的一级缓存。
Mybatis的一级缓存是存在于SqlSession的生命周期中,在同一个SqlSession中查询时,如果查询的方法名称和参数完全一致,先去缓存对象(Map对象)中查看,如果有查询缓存则会使用查询缓存中的对象返回。
如何清除一级缓存?
- 可以在查询mapper语句中添加属性<select id="selectById" flushCache="true" resultMap="userMap">sql</select>,这样再查询是总是清除缓存,每次都执行查询操作。
- 任何的insert、update、delete操作,都会清空一级缓存,所以上面的user3查询是新查询,由于一级缓存是默默的工作,因此要避免再使用过程中由于不了解而发生觉察不到的错误。
二、Mybatis二级缓存(重点理解)
MyBatis的二级不同于一级缓存只存在于SqlSession 的生命周期中,而是可以理解为存在于SqlSessionFactory的生命周期中。缓存数据在一般情况下是不相通的。只有在使用如Redis这样的缓存数据库时,才可以共享缓存。
2.1 配置二级缓存
在Mybatis的全局配置settings中一个参数cacheEnabled属性,是二级缓存的全局开关(总开关),默认是true,也就是默认开启的。如果将其设置为false,即使后面再怎么配置二级缓存也不生效。不过因为是默认开启,所以可以不用设置。如果设置请在mybatis-config.xml中添加如下代码:
<settings>
<!-- 其他配置 -->
<setting name="cacheEnabled" value="true"/>
</settings
二级缓存的配置需要在Mapper.xml映射文件中,或者配置在Mapper.java接口中,和命名空间是绑定的。
2.1.1 Mapper.xml中配置二级缓存
保证二级缓存的全局配置开启,仅仅在xml文件中添加一个<cache/>元素即可,是不是很简单,配置参考如下:
<mapper name="xxxx.xx.xx.xxMapper">
<cache/>
<!-- 其他配置 -->
</mapper>
默认的二级缓存会有如下效果:
- 映射语句中的所有select语句将会被缓存;
- 映射语句中的所有insert、updata、delete语句会刷新缓存;
- 缓存会使用Least Recently Used(LRU,最近最少使用的)算法来收回(缓存收回策略);
- 根据时间表(如no Flush Interval,没有刷新间隔),缓存不会以任何时间顺序来刷新;
- 缓存会存储集合或对象共1024个引用(无论查询结果是什么类型的);
- 缓存会被视为read/write(可读、可写),即不会被其他调用者或线程所干扰(只当前用户可见)。
<cache>元素的属性可以修改上面的默认配置,写法如下:(下面4个属性分别对应上面的3/4/5/6条效果)
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
属性解释:
eviction(收回策略)
- LRU(最近最少使用的):一处最长时间不会使用的对象,默认值。
- FIFO(先进先出):俺对象进入缓存的税讯来移除它们。
- SOFT(软引用):移除基于垃圾回收期状态和软引用规则的对象。
- WEAK(弱引用):更积极地移除基于垃圾收集器状态和软引用规则的对象。
flushInterval(刷新间隔):单位毫秒,默认情况不设置,即没有刷新间隔,缓存尽在调用语句是刷新。
size(引用条目):记住缓存的对象数目和运行环境的可用内存资源数目。默认1024。
readOnly(只读):可以设置为true和false。如果设置成true则会让每一个调用者返回的缓存对象都是同一个实例,因此不能修改(修改对象的值,影响别人),可是带来的好处是性能的提升。如果设置成false(可读可写)会通过序列化返回缓存对象的拷贝,相对会慢一些,但是安全,因此默认是false的。
2.1.2 Mapper接口中配置二级缓存
当只通过注解方式配置二级缓存时,需要添加如下配置:
@CacheNamespace(eviction=FifoCache.class, flushInterval=60000, size=512, readWrite=true)
public interface RoleMapper {
// 接口方法
}
注意:同时使用注解方式和XML配置文件时,如果同时配置了上述的二级缓存,就会出现异常,因为两者的命名空间相同。
解决:使用参照缓存。
- 如果XML中配置了二级缓存,则Mapper接口中使用@CacheNamespaceRef(RoleMapper.class)替换@CacheNamespace注解即可。
- 如果Mapper接口中配置了二级缓存,则XML中使用<cache-ref namespace="xx.xxx.xx.RoleMapper"/>替换<cache/>即可。
其实Mybatis中很少同时使用Mapper接口配置和XML映射文件,参照缓存除了能通用其他缓存减少配置外,主要作用是解决脏读。
2.2 使用二级缓存
配置了二级缓存后,当调用所有的select查询方法时,二级缓存就开始起作用了,如果配置的是可读可写的缓存,Mybatis是通过使用SerializedCache序列化来实现可读可写缓存类,所以查询的对象需要实现Serializable接口,如果配置的是仅可读,则Mybatis通过Map来存储缓存对象,因此读出来的都是相同实例。
MyBatis默认提供的缓存实现是基于Map实现的内存缓存,己经可以满足基本的应用。但是当需要缓存大量的数据时,不能仅仅通过提高内存来使用MyBatis的二级缓存,还可以选择一些类似EhCache的缓存框架或Redis缓存数据库等工具来保存MyBatis 的二级缓存数据。接下来两节,我们会介绍两个常见的缓存框架。
2.3 集成EhCache缓存
EhCache是一个纯粹的Java进程内的缓存框架,具有快速、精干等特点。具体来说,EhCache主要的特性如下。
- 快速。
- 简单。
- 多种缓存策略。
- 缓存数据有内存和磁盘两级,无须担心容量问题。
- 缓存数据会在虚拟机重启的过程中写入磁盘。
- 可以通过RMI、可插入API等方式进行分布式缓存。
- 具有缓存和缓存管理器的侦昕接口。
- 支持多缓存管理器实例以及一个实例的多个缓存区域。
2.3.1 添加项目依赖
<dependency>
<groupid>org.mybatis.caches</groupid>
<artifactid>mybatis-ehcache</artifactid>
<version>l.0.3</version>
</dependency>
2.3.2 配置EhCache
在src/main/resources目录下添加ehcache.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>
有关EhCache的详细配置,参考地址:http://www.ehcache.org/ehcache.xml中的内容。
- copyOnRead的含义是,判断从缓存中读取数据时是返回对象的引用还是复制一个对象返回。默认情况下是false,即返回数据的引用,这种情况下返回的都是相同的对象,和MyBatis默认缓存中的只读对象是相同的。如果设置为true,那就是可读写缓存,每次读取缓存时都会复制一个新的实例。
- copyOnWrite 的含义是,判断写入缓存时是直接缓存对象的引用还是复制一个对象然后缓存,默认也是false。 如果想使用可读写缓存,就需要将这两个属性配置为true,如果使用只读缓存,可以不配置这两个属性,使用默认值false即可。
2.3.3 修改XML中的缓存配置
<mapper name="xxxx.xx.xx.xxMapper">
<cache type="org.mybatis.caches.ehcache.EhcacheCache">
<!-- 其他配置 -->
</mapper>
这样配置了cache元素的type属性为ehcache的缓存类型,再添加其他属性都不起作用了,针对缓存的配置都已经在ehcache.xml中进行。因为ehcache.xml配置文件中只能配置一个默认缓存配置(<defaultCache>),如果想对单独的命名空间的Mapper做缓存配置可以使用<cache>元素,做细颗粒配置,如下:
<cache
name="tk.mybatis.simple.mapper.RoleMapper"
maxElementsinMemory="3000"
eternal="false"
copyOnRead="true"
copyOnWrite="true"
timeToidleSeconds="3600"
timeToLiveSeconds="3600"
overflowToDisk="true"
diskPersistent="true"/>
2.4 集成Redis缓存
实际生产项目使用Redis做缓存的项目不常见,所以省略,想配置可参考其他资料。
2.5 脏数据的产生和避免
二级缓存虽然能提高应用效率,减轻数据库服务器的压力,但是如果使用不当,很容易产生脏数据。 这些脏数据会在不知不觉中影响业务逻辑, 影响应用的实效,所以我们需要了解在MyBatis缓存中脏数据是如何产生的,也要掌握避免脏数据的技巧。
MyBatis的二级缓存是和命名空间绑定的,所以通常情况下每一个Mapper映射文件都拥有自己的二级缓存,不同Mapper的二级缓存互不影响。在常见的数据库操作中,多表联合查询非常常见,由于关系型数据库的设计, 使得很多时候需要关联多个表才能获得想要的数据。在关联多表查询时肯定会将该查询放到某个命名空间下的映射文件中,这样一个多表的查询就会缓存在该命名空间的二级缓存中。涉及这些表的增、删、改操作通常不在一个映射文件中,它们的命名空间不同, 因此当有数据变化时,多表查询的缓存未必会被清空,这种情况下就会产生脏数据。
该如何避免脏数据的出现呢?这时就需要用到参照缓存了。 当某几个表可以作为一个业务整体时,通常是让几个会关联的ER表同时使用同一个二级缓存,这样就能解决脏数据问题。在上面这个例子中,将UserMapper.xrnl 中的缓存配置修改如下。
<mapper namespace="tk.mybatis.simple.mapper.UserMapper">
<cache-ref namespace="tk.mybatis.simple.mapper.RoleMapper"/>
<!-- 其他配置 -->
</mapper>
修改为参照缓存后,虽然这样可以解决脏数据的问题,但是并不是所有的关联查询都可以这么解决,如果有几十个表甚至所有表都以不同的关联关系存在于各自的映射文件中时,使用参照缓存显然没有意义。
2.6 二级缓存适用场景
二级缓存虽然好处很多,但并不是什么时候都可以使用。 在以下场景中,推荐使用二级缓存。
- 以查询为主的应用中,只有尽可能少的增、删、改操作。
- 绝大多数以单表操作存在时,由于很少存在互相关联的情况,因此不会出现脏数据。
- 可以按业务划分对表进行分组时, 如关联的表比较少,可以通过参照缓存进行配置。
除了推荐使用的情况,如果脏读对系统没有影响,也可以考虑使用。 在无法保证数据不出现脏读的情况下, 建议在业务层使用可控制的缓存代替二级缓存。
三、总结
通过本文的学习,我们知道了一级缓存和二级缓存的区别,学会了如何配置二级缓存,除了MyBatis 默认提供的缓存外,还学会了如何集成EhCache。另外,我们认识到了二级缓存可能带来的脏读问题,也学会了特定情况下解决脏读的办法。
MyBatis 的二级缓存需要在特定的场景下才会适用,在选择使用二级缓存前一定要认真考虑脏读对系统的影响。在任何情况下,都可以考虑在业务层使用可控制的缓存来代替二级缓存。
@本文内容参考《MyBatis从入门到精通》作者:刘增辉 电子工业出版社