MyBatis 10-缓存配置
http://www.broadview.com.cn/book/3643
https://github.com/mybatis-book/book
一般提到 MyBatis 缓存时都是指二级缓存。一级缓存默认会启用,且不能控制。
一级缓存
@Test
public void testL1Cache(){
// 获取 sqlSession
SqlSession sqlSession = getSqlSession();
SysUser user1 = null;
try {
// 获取 UserMapper 接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 调用 selectById 方法,查询 id = 1 的用户
user1 = userMapper.selectById(1l);
// 对当前获取的对象重新赋值
user1.setUserName("New Name");
// 再次查询获取 id 相同的用户
SysUser user2 = userMapper.selectById(1l);
// 虽然我们没有更新数据库,但是这个用户名和我们 user1 重新赋值的名字相同了
Assert.assertEquals("New Name", user2.getUserName());
// 不仅如何,user2 和 user1 完全就是同一个实例
Assert.assertEquals(user1, user2);
} finally {
// 关闭当前的 sqlSession
sqlSession.close();
}
System.out.println("开启新的 sqlSession");
// 开始另一个新的 session
sqlSession = getSqlSession();
try {
// 获取 UserMapper 接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 调用 selectById 方法,查询 id = 1 的用户
SysUser user2 = userMapper.selectById(1l);
// 第二个 session 获取的用户名仍然是 admin
Assert.assertNotEquals("New Name", user2.getUserName());
// 这里的 user2 和 前一个 session 查询的结果是两个不同的实例
Assert.assertNotEquals(user1, user2);
// 执行删除操作
userMapper.deleteById(2L);
// 获取 user3
SysUser user3 = userMapper.selectById(1l);
// 这里的 user2 和 user3 是两个不同的实例
Assert.assertNotEquals(user2, user3);
} finally {
// 关闭 sqlSession
sqlSession.close();
}
}
MyBatis 的一级缓存存在于 SqlSession 的生命周期中,在同一个 SqlSession 中查询时,MyBatis会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个 Map 对象中。如果同一个 SqlSession 中执行的方法和参数完全一致,那么通过算法会生成相同的键值,当 Map 缓存对象中已经存在该键值时,则会返回缓存中的对象。
如果不想让 selectById 方法使用一级缓存,可以加上 flushCach="true"
,会在查询数据前清空当前的一级缓存。
<!-- useCache="false" 可以禁止使用缓存 -->
<select id="selectById" flushCache="true" resultMap="userMap">
select * from sys_user where id=#{id}
</select>
另外所有的 insert,update、delete 操作都会清空一级缓存。
二级缓存
一级缓存只存在于 SqlSession 的生命周期中,而二级缓存可以理解为存在于 SqlSessionFactory 的生命周期中。
配置二级缓存
mybatis-config.xml 文件中可以配置二级缓存的全局开关,默认是 true
<settings>
<setting name="cacheEnabled" value="true">
</settings>
二级缓存是和命名空间绑定的,所以需要配置在 Mapper.xml 映射文件中(命名空间是 xml 根节点 mapper 的 namespace 属性),或者配置在 Mapper.java 接口中(命令空间就是接口的全限定名称)。
Mapper.xml 中配置二级缓存
只需要在你的 SQL 映射文件中添加一行 <cache/>
默认效果:
- 映射语句文件中的所有 select 语句的结果将会被缓存。
- 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
- 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
- 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
- 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
- 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
这些属性可以通过 cache 元素的属性来修改。比如:
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。
eviction
(回收策略):- LRU – 最近最少使用:移除最长时间不被使用的对象。默认策略
- FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
- SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
- WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
flushInterval
(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。size
(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。readOnly
(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。
Mapper 接口中配置二级缓存
@CacheNamespace(
eviction=FifoCache.class,
flushInterval=60000,
size=512,
readWrite=true // true 为读写(默认),false 为只读
)
public interface RoleMapper {
}
当同时使用注解方式和 xml 映射文件时,如果同时配置了二级缓存,会抛出异常 Caches collection already contains value for tk.mybatis.simple.mapper.RoleMapper
因为这时是相同的命名空间,这时应该使用参照缓存
// 这样就会使用命名空间为 tk.mybatis.simple.mapper.RoleMapper 的缓存配置,即 RoleMapper.xml 中配置的缓存
@CacheNamespaceRef(RoleMapper.class)
public interface RoleMapper {
}
也可以在 xml 中配置参照缓存
<cache-ref namespace="tk.mybatis.simple.mapper.RoleMapper"/>
不会同时使用 Mapper 接口注解方式和 xml 映射文件,所以参照缓存不是为解决这个问题设计的,主要作用是解决脏读
使用二级缓存
由于配置的是可读写的缓存,而 MyBatis 使用 SerializedCache 序列化缓存来实现可读写缓存类,并通过序列化和反序列化来保证通过缓存获取数据时,得到的是一个新的实例。如果配置为只读缓存,MyBatis 会使用 Map 来存储缓存值。
使用 SerializedCache 序列化缓存,要求所有被序列化的对象必须实现 Serializable 接口。
public class SysRole implements Serializable {
private static final long serialVersionUID = 6320941908222932112L;
}
测试
@Test
public void testL2Cache(){
SqlSession sqlSession = getSqlSession();
SysRole role1 = null;
try {
// 获取 RoleMapper 接口
RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
// 调用 selectById 方法,查询 id = 1 的用户
role1 = roleMapper.selectById(1l);
// 对当前获取的对象重新赋值
role1.setRoleName("New Name");
// 再次查询获取 id 相同的用户
SysRole role2 = roleMapper.selectById(1l);
// 虽然我们没有更新数据库,但是这个用户名和我们 role1 重新赋值的名字相同了
Assert.assertEquals("New Name", role2.getRoleName());
// 不仅如何,role2 和 role1 完全就是同一个实例
Assert.assertEquals(role1, role2);
} finally {
sqlSession.close();
}
System.out.println("开启新的 sqlSession");
// 开始另一个新的 session
sqlSession = getSqlSession();
try {
// 获取 RoleMapper 接口
RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
// 调用 selectById 方法,查询 id = 1 的用户
SysRole role2 = roleMapper.selectById(1l);
// 第二个 session 获取的用户名仍然是 admin
Assert.assertEquals("New Name", role2.getRoleName());
// 这里的 role2 和 前一个 session 查询的结果是两个不同的实例
Assert.assertNotEquals(role1, role2);
// 获取 role3
SysRole role3 = roleMapper.selectById(1l);
// 这里的 role2 和 role3 是两个不同的实例
Assert.assertNotEquals(role2, role3);
} finally {
sqlSession.close();
}
}
// 第一次没有缓存,命中率 0
DEBUG [main] - Cache Hit Ratio [tk.mybatis.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
DEBUG [main] - <== Total: 1
// 使用的一级缓存,所以命中率还是 0
DEBUG [main] - Cache Hit Ratio [tk.mybatis.simple.mapper.RoleMapper]: 0.0
开启新的 sqlSession
// 第三次查询,命中率 1/3
DEBUG [main] - Cache Hit Ratio [tk.mybatis.simple.mapper.RoleMapper]: 0.3333333333333333
DEBUG [main] - Cache Hit Ratio [tk.mybatis.simple.mapper.RoleMapper]: 0.5
这个例子中没有真正的读写安全,role1.setRoleName("New Name");
,第二部分的代码中查询结果 roleName 都是 "New Name"。所以想要安全使用,需要避免无意义的修改。
集成 EhCache
EhCache 是一个纯粹的 Java 进程内的缓存框架,具有快速、精干等特点。具体来说,EhCache 主要的特性如下。
- 快速。
- 简单。
- 多种缓存策略。
- 缓存数据有内存和磁盘两级,无须担心容量问题。
- 存数据会在虚拟机重启的过程中写入磁盘。
- 可以通过RMI、可插入 AP I等方式进行分布式缓存。
- 具有缓存和缓存管理器的侦昕接口。
- 支持多缓存管理器实例以及一个实例的多个缓存区域。
添加项目依赖
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.0.3</version>
</dependency>
配置 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>
maxElementsInMemory:设置基于内存的缓存中可存放的对象最大数目
eternal:设置对象是否为永久的,true 表示永不过期,此时将忽略
timeToIdleSeconds:设置对象空闲最长时间,以秒为单位,超过这个时间对象过期。当对象过期时,ehCache 会把它从缓存中清除。如果此值为 0,表示对象可以无限期地处于空闲状态。
timeToLiveSeconds:设置对象生存最长时间,超过这个时间,对象过期。如果此值为 0,表示对象可以无限期地存在于缓存中,该属性值必须大于或等于 timeToIdleSeconds 属性值
overflowToDisk:设置基于内在的缓存中的对象数目达到上限后,是否把溢出的对象写到基于硬盘的缓存中
diskPersistent:当 jvm 结束时是否持久化对象 true false 默认是false
diskExpiryThreadIntervalSeconds:指定专门用于清除过期对象的监听线程的轮询时间
memoryStoreEvictionPolicy:当内存缓存达到最大,有新的 element 加入的时候,移除缓存中 element 的策略,默认是LRU(最近最少使用),可选的有LFU(最不常使用)和 FIFO(先进先出)
copyOnRead
:判断从缓存中读取数据时是返回对象的引用还是复制一个对象返回。默认情况下是 false,即返回数据的引用,这种情况下返回的都是相同的对象,和 MyBatis 默认缓存中的只读对象是相同的。如果设置为 true,那就是可读写缓存,每次读取缓存时都会复制一个新的实例。copyOnWrite
:判断写入缓存时是直接缓存对象的引用还是复制一个对象然后缓存,默认也是 false。如果想使用可读写缓存,就需要将这两个属性配置为 true,如果使用只读缓存,可以不配置这两个属性,使用默认值 false 即可。
修改 RoleMapper.xml 中的缓存配置
修改 RoleMapper.xml 中的配置如下。
<mapper namespace="tk.mybatis.simple.mapper.RoleMapper">
<!-- cache 其他属性不会起到任何作用 -->
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
</mapper>
ehcache.xml 只有一个默认的缓存配置,如果想针对某一个命名空间进行配置,需添加
<cache
name="tk.mybatis.simple.mapper.RoleMapper"
maxElementsInMemory="3000"
eternal="false"
copyOnRead="true"
copyOnWrite="true"
timeToIdleSeconds="3600"
timeToLiveSeconds="3600"
overflowToDisk="true"
diskPersistent="true"/>
集成 Redis 缓存
添加项目依赖
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
配置 Redis
在 src/main/resources 目录下新增 redis.properties 文件
host=127.0.0.1
port=6379
connectionTimeout=5000
soTimeout=5000
password=
database=0
clientName=
修改 RoleMapper.xml 中的缓存配置
<mapper namespace="tk.mybatis.simple.mapper.RoleMapper">
<cache type="org.mybatis.caches.redis.RedisCache"/>
</mapper>
脏数据的产生和避免
一个多表的查询就会缓存在某命名空间的二级缓存中。涉及这些表的增、删、改操作通常不在一个映射文件中,它们的命名空间不同,因此当有数据变化时,多表查询的缓存未必会被清空,这种情况下就会产生脏数据。
在 UserMapper.xml 添加二级缓存配置,增加 <cache/>
元素,SysUser 对象实现 Serializable 接口。
@Test
public void testDirtyData(){
SqlSession sqlSession = getSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
SysUser user = userMapper.selectUserAndRoleById(1001L);
Assert.assertEquals("普通用户", user.getRole().getRoleName());
System.out.println("角色名:" + user.getRole().getRoleName());
} finally {
sqlSession.close();
}
// 开始另一个新的 session
sqlSession = getSqlSession();
try {
RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
SysRole role = roleMapper.selectById(2L);
role.setRoleName("脏数据");
roleMapper.updateById(role);
sqlSession.commit();
} finally {
sqlSession.close();
}
System.out.println("开启新的 sqlSession");
sqlSession = getSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
SysUser user = userMapper.selectUserAndRoleById(1001L);
SysRole role = roleMapper.selectById(2L);
Assert.assertEquals("普通用户", user.getRole().getRoleName());
Assert.assertEquals("脏数据", role.getRoleName());
System.out.println("角色名:" + user.getRole().getRoleName());
// 还原数据
role.setRoleName("普通用户");
roleMapper.updateById(role);
sqlSession.commit();
} finally {
sqlSession.close();
}
}
第一个 SqlSession 中获取了用户和关联的角色信息,第二个 SqlSession 中查询角色并修改了角色的信息,第三个 SqlSession 查询用户和关联的角色信息。这时从缓存中直接取出数据,就出现了脏数据,因为角色名称己经修改,但是这里读取到的角色名称仍然是修改前的名字,因此出现了脏读。
如何避免脏数据的出现?就需要用到参照缓存了。可以让几个会关联的表同时使用同一个二级缓存。修改 UserMapper.xml 的缓存配置。
<cache-ref namespace="tk.mybatis.simple.mapper.RoleMapper">
二级缓存适用场景
- 查询为主的应用中,只有尽可能少的增、删、改操作。
- 绝大多数以单表操作存在时,由于很少存在互相关联的情况,因此不会出现脏数据。
- 可以按业务划分对表进行分组时,如关联的表比较少,可以通过参照缓存进行配置。
除了推荐使用的情况,如果脏读对系统没有影响,也可以考虑使用。在无法保证数据不出现脏读的情况下,建议在业务层使用可控制的缓存代替二级缓存。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 bin07280@qq.com
文章标题:MyBatis 10-缓存配置
文章字数:3.8k
本文作者:Bin
发布时间:2019-12-01, 15:53:26
最后更新:2019-12-03, 15:54:10
原始链接:http://coolview.github.io/2019/12/01/MyBatis/MyBatis%2010-%E7%BC%93%E5%AD%98%E9%85%8D%E7%BD%AE/版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。