MyBatis 默认开启二级缓存,如果想要关闭,可以在 mybatis-config.xml 中配置:
<settings>
<setting name="cacheEnabled" value="false"/>
settings>
MyBatis 的二级缓存和命名空间绑定,即二级缓存需要配置在 XML 映射文件或者 Mapper 接口中。
示例:在 RoleMapper.xml 开启二级缓存
<mapper namespace="tk.mybatis.simple.mapper.RoleMapper">
<cache/>
mapper>
默认的二级缓存会有如下效果:
所有属性都可以通过 cache 标签的属性来修改:
<cache
eviction="FIFO"
flushInterval="60000"
size="1024"
readOnly="false"/>
创建了一个FIFO缓存,每隔60秒刷新一次,存储集合或对象的512个应用,而且返回的对象被认为是只读的,
只需要在接口名称前增加 @CacheNamespace(org.apache.ibatis.annotations.CacheNamespace)注解即可,该注解同样可以配置各项属性
@CacheNamespace(
eviction = FifoCache.class,
flushInterval = 60000,
size = 512,
readWrite = true
)
同时配置上述的二级缓存会抛出异常,因为 Mapper 接口和对应的 XML 文件是相同的命名空间。这时候可以使用参照缓存。在 Mapper 接口中,参照缓存配置如下:
@CacheNamespaceRef(RoleMapper.class)
public interface RoleMapper {
}
也可以在 XML 中配置参照缓存:
<cache-ref namespace="tk.mybatis.simple.mapper.RoleMapper"/>
这样可以解决同时配置二级缓存所导致的冲突,但参照缓存并不是为了解决这个问题而设计的,MyBatis 中很少同时使用两种配置。
MyBatis 使用 SerializedCache 序列化缓存来实现可读写缓存类,并通过序列化(将一个对象写入磁盘)和反序列化(从磁盘中恢复序列化的对象)来保证通过缓存获取数据时,得到的是一个新的实例。如果配置为只读缓存,MyBatis 会使用 Map 来存储缓存值,这种情况下,从缓存中获取的对象就是同一个实例。
因为 RoleMapper 使用可读写缓存,需要使用 SerializedCache 序列化缓存,这个缓存类要求所有被序列化的对象必须实现 Serializable(java.io.Serializable)接口。
public class SysRole implements Serializable{
private static final long serialVersionUID = 111111L;
//其它属性和getter,setter方法
}
测试代码:
@Test
public void testL2Cache() {
SqlSession sqlSession = getSqlSession();
SysRole role1 = null;
try {
RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
//查询id=1的角色
role1 = roleMapper.selectById(1L);
//对当前获取的对象重新赋值
role1.setRoleName("new name");
//再次查询相同id的角色
SysRole role2 = roleMapper.selectById(1L);
//虽然没有更新数据库,但是这个角色名和role1重新赋值的名字相同
Assert.assertEquals("new name", role2.getRoleName());
//role1和role2是同一个实例
Assert.assertEquals(role1, role2);
} finally {
// 关闭当前session
sqlSession.close();
}
System.out.println("开启新的session");
sqlSession = getSqlSession();
try {
RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
//查询id=1的角色
SysRole role2 = roleMapper.selectById(1L);
//第二个session获取的角色名仍是new name
Assert.assertEquals("new name", role2.getRoleName());
//这里role2和前一个session查询的结果是两个不同的实例
Assert.assertNotEquals(role1, role2);
//再次获取id=1的角色
SysRole role3 = roleMapper.selectById(1L);
//这里的role2和role3是两个不同的实例
Assert.assertNotEquals(role2, role3);
} finally {
// 关闭session
sqlSession.close();
}
}
测试结果:
DEBUG [main] - Cache Hit Ratio [tk.mybatis.simple.mapper.RoleMapper]: 0.0
DEBUG [main] - ==> Preparing: select id, role_name roleName, enabled, create_time 'createInfo.createTime', create_by 'createInfo.createBy' from sys_role where id = ?
DEBUG [main] - ==> Parameters: 1(Long)
TRACE [main] - <== Columns: id, roleName, enabled, createInfo.createTime, createInfo.createBy
TRACE [main] - <== Row: 1, 管理员, 1, 2019-01-06 10:36:27.0, 1
DEBUG [main] - <== Total: 1
DEBUG [main] - Cache Hit Ratio [tk.mybatis.simple.mapper.RoleMapper]: 0.0
开启新的session
DEBUG [main] - Cache Hit Ratio [tk.mybatis.simple.mapper.RoleMapper]: 0.3333333333333333
DEBUG [main] - Cache Hit Ratio [tk.mybatis.simple.mapper.RoleMapper]: 0.5
以 Cache Hit Ratio 开头的语句为当前执行方法的缓存命中率。
在第一部分测试中,第一次查询获取 role1 的时候由于没有缓存,所以执行了数据库查询。第二个查询获取 role2 的时候,role2 和 role1 是完全相同的实例,这里使用的是一级缓存,所以返回同一个实例。
调用 close 方法关闭 session 的时候,sqlSession 才会保存数据到二级缓存中,所以第一部分的两次查询命中率为 0。
在第二部分的测试中,再次获取 role2 和 role3 时是从缓存中取得的值,命中率分别为 0.333、0.5。因为是可读写缓存,role2 和 role3 都是反序列换得到的结果,所以它们是不同的实例。
MyBatis 的二级缓存是和命名空间绑定的,通常情况下每一个 Mapper 映射文件都有自己的二级缓存,不同的 Mapper 的二级缓存互不影响。在常见的数据库操作中,多表联合查询非常常见。在关联多表查询时,肯定会将该查询放在某个命名空间下的映射文件中,这样一个多表的查询就会缓存在该命名空间的二级缓存中。涉及这些表的增、删、改操作在不同的映射文件中,它们的命名空间不同,当有数据变化时,多表查询的缓存未必会被清空,这种情况下就产生了脏数据。
在 UserMapper 中有 selectUserAndRoleById 方法:
select
u.id,
u.user_name userName,
u.user_password userPassword,
u.user_info userInfo,
u.create_time createTime,
r.role_name as "role.roleName",
r.enabled as "role.enabled"
from sys_user u
inner join sys_user_role ur on ur.user_id = u.id
inner join sys_role r on ur.role_id = r.id
where u.id = #{id}
@Test
public void testDirtyDate() {
SqlSession sqlSession = getSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
SysUser user1 = userMapper.selectUserAndRoleById(1001L);
//id为1001的用户角色名是普通用户
Assert.assertEquals("普通用户", user1.getRole().getRoleName());
System.out.println("角色名:" + user1.getRole().getRoleName());
} finally {
// TODO: handle finally clause
sqlSession.close();
}
sqlSession = getSqlSession();
try {
RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
SysRole role = roleMapper.selectById(2L);
Assert.assertEquals("普通用户", role.getRoleName());
role.setRoleName("脏数据");
roleMapper.updateById(role);
//提交修改
sqlSession.commit();
} finally {
// TODO: handle finally clause
sqlSession.close();
}
System.out.println("开启新的Session");
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);
System.out.println("角色名:" + user.getRole().getRoleName());
Assert.assertEquals("普通用户", user.getRole().getRoleName());
Assert.assertEquals("脏数据", role.getRoleName());
//还原数据
role.setRoleName("普通用户");
roleMapper.updateById(role);
sqlSession.commit();
} finally {
// TODO: handle finally clause
sqlSession.close();
}
}
第一个 session 中获取了用户和关联的角色信息,此时角色名为“普通用户”;第二个 session 中查询角色并修改了角色信息,此时角色名更改为“脏数据”;第三个 session 中查询用户和关联的角色信息,这时从缓存中直接取出数据,取出的用户关联的角色名仍然为修改前的“普通用户”,出现了脏读现象。
使用参照缓存可以避免脏数据的出现。当某几个表可以作为一个业务整体时,通常让几个会关联的 ER 表同时使用一个二级缓存。在上面的例子中,由于是更新 Role 表导致 UserMapper 中出现脏数据,所以在 UserMapper.xml 中配置参照缓存:
<mapper namespace="tk.mybatis.simple.mapper.UserMapper">
<cache-ref namespace="tk.mybatis.simple.mapper.RoleMapper"/>
mapper>
在无法保证数据不出现脏读的情况下,建议在业务层使用可控制的缓存代替二级缓存。
EhCache 是一个 java 进程内的缓存框架。EhCache 的主要特性如下:
<dependency>
<groupId>org.mybatis.cachesgroupId>
<artifactId>mybatis-ehcacheartifactId>
<version>1.0.3version>
dependency>
在 src/main/resources 目录下新增 ehcache.xml 文件:
<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 的含义是,判断从缓存中读取数据时是返回对象的引用还是复制一个对象返回。默认是 false,返回数据引用。
copyOnWrite 的含义是,判断写入缓存时是直接缓存对象的引用还是复制一个对象然后缓存。默认是 false,直接缓存数据。
如果想使用可读写缓存,就需要将这两个属性配置为 true。
需要在 RoleMapper.xml 中应用:
<mapper namespace="tk.mybatis.simple.mapper.RoleMapper">
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
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"/>
Redis 是一个高性能的 key-value 数据库。
<dependency>
<groupId>org.mybatis.cachesgroupId>
<artifactId>mybatis-redisartifactId>
<version>1.0.0-beta2version>
dependency>
有关 Redis 安装启动可参考Redis官方文档。
Redis 服务启动后,在 src/main/resources 目录下新增 redis.properties 文件:
host=192.168.16.142
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>
RedisCache 在保存和获取缓存数据时,使用了 Java 的序列化和反序列化,因此被缓存的对象需要实现 serializable 接口。
Redis 并不会因为应用的关闭而失效,所有的查询都使用缓存,是可读写缓存。使用 Redis 的缓存服务可以将分布式应用连接到同一个缓存服务器,实现分布式应用间的缓存共享。