MyBatis(五) MyBatis的缓存
使用缓存,可以让前端请求更快地获取数据,且能避免频繁的数据库交互,通常听说的redis、memcached就是,那么,MyBatis同样也提供了查询缓存的特性给我们使用。
MyBatis有两个级别的缓存:
一级缓存
也叫本地缓存,默认会启用而且不能控制,下面测试一下:
package cn.linjk.mybatistest.mapper;
import cn.linjk.mybatistest.domain.User;
import org.apache.ibatis.session.SqlSession;
import org.junit.Assert;
import org.junit.Test;
public class CacheTest extends BaseMapperTest {
@Test
public void testLevelOneCache() {
SqlSession sqlSession = getSqlSession();
User user = null;
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
user = userMapper.selectById(1L);
user.setName("New Name");
// 注意:这里虽然创建了新的userTmp对象,但是,查询出的用户名仍然是`New Name`!,而并没有更新数据库操作!
User userTmp = userMapper.selectById(1L);
Assert.assertEquals("New Name", userTmp.getName());
Assert.assertEquals(user, userTmp);
}
finally {
sqlSession.close();
}
// 开启新session......
sqlSession = getSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User userNew = userMapper.selectById(1L);
Assert.assertEquals("admin", userNew.getName());
}
finally {
sqlSession.close();
}
}
}
从上面可以发现,MyBatis的一级缓存存在于SqlSession的生命周期中,在同一个SqlSession中查询时,MyBatis会把执行的方法和参数通过算法生成缓存的键值,如不需要使用一级缓存,可以在select加参数flushCache:
但是这样会增加数据库的查询次数,要尽量避免这样处理。
另外,数据库的INSERT、UPDATE和DELETE操作都会清空一级缓存。
二级缓存
二级缓存使用场景:
1. 以查询为主的应用,只有尽可能少的增删改操作
2. 大多数表以单表操作存在,即避免了脏数据
3. 可以按业务划分对表进行分组时,如关联的表较少,可以通过参照缓存降低脏数据
4. 出现脏数据对系统功能无影响
5. 任何情况下,可在业务层使用可控制的缓存代替二级缓存
MyBatis的二级缓存默认是打开的,也可以在mybatis-config.xml文件配置:
MyBatis的二级缓存是和命名空间绑定的,即需要配置在Mapper.xml映射文件(只需要加cache标签即可)或Mapper.java接口中,在Mapper.xml中命名空间就是XML根节点mapper的namespace属性,在Mapper.java接口中命名空间就是接口的全限定名称。例如:
- eviction: 收回策略,有LRU(最近最少使用的)、FIFO(先进先出)、SOFT(软引用)、WEAK(弱引用)
- flushInterval: 刷新间隔,默认情况不设置,即没有刷新间隔,缓存仅在调用语句时刷新
- size: 引用数目,默认为1024
- readOnly: 只读,默认为false
这里设置了一个FIFO缓存,它每隔60秒刷新一次,存储集合或对象的512个引用,而且返回的对象被认为是只读的(这样在不同线程中的调用者之间修改会导致冲突)。
上面在UserMapper.xml配置了缓存,当调用所有select的查询方法时,二级缓存就会开始工作,如果readOnly为true,MyBatis会使用Map来存储缓存值,为false则使用org.apache.ibatis.cache.decorators.SerializedCache序列化缓存来保证通过缓存获取数据,因此User来还需要实现java.io.Serializable接口,现在看看测试代码:
第一个session和一级缓存一样,当关闭第一个session时,查询数据会被保存到二级缓存,因此,在新的session会出现缓存命中率0.33333,因为,这是第三次执行这个查询,所以结果为1/3=0.33333,注意,这个sql如果配置了flushCache为true,则二级缓存也不会起作用。
如上的二级缓存是基于Map实现的内存缓存,当需要缓存大量数据时,可以使用EhCache缓存框架或Redis缓存数据库来保存二级缓存的数据。现在来看主流的Redis方案。
这里为了方便,直接安装win版的redis:
按默认设置安装测试成功:
1. 添加项目依赖
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
2. 配置Redis
src/main/resources目录下新增文件redis.properties:
3. 修改UserMapper.xml的配置
User类实现序列化接口:
---- 配置IDEA可以检测生成UUID
然后双击类名--alt+Enter键即可:
如下:
4. 测试
还是运行二级缓存的单元测试,成功,这时看看redis缓存:
注意,当再次运行这个单元测试时,会失败,注意到这句:
这是因为Redis作为缓存服务器,当应用再次运行查询时使用缓存的数据,因为是可读写缓存,所以不是相同的对象。
这样,可以实现分布式缓存,而MyBatis默认的二级缓存不行。
脏数据
二级缓存虽然能提高应用效率,但使用不当会产生脏数据,前面说到,MyBatis的二级缓存是与命名空间绑定的,使用多表查询时,设计的多个表的CRUD操作通常不在一个映射文件(即命名空间)中,这样,当有数据变化时,多表查询的缓存未必会被清空,就会产生脏数据了。
例如如下操作会产生脏数据:
1. 查询用户角色
2. 修改角色名
3. 查询用户角色
在第三步因为有缓存,所以,查询到的用户角色名还是原来的。
针对这种情况,对于简单不复杂的表关联,可以用参照缓存来实现,即让几个会关联的ER表同时使用同一个二级缓存,因为用户有角色,所以,在UserMapper.xml参照RoleMapper.xml的二级缓存:
这样,在上面的第3步的查询中,不会从Redis缓存而是直接查询数据库去获取最新的数据。