前言
缓存是为了协调吞吐速度相差很大的设备之间的数据传送。
引入缓存的主要原因,可归结为以下几点:
-
改善CPU与I/O设备间速度不匹配的矛盾
-
可以减少对 CPU的中断频率,放宽对中断响应时间的限制
-
提高 CPU和度 I/O设备之间的并行性
Mybatis也设置了缓存
一级缓存
一级缓存是SqlSession,一次与数据库会话的缓存
我们知道,一次sql操作数据库,需要一个SqlSession对象
mybatis默认是一级缓存,即SqlSession的缓存
我们可以通过日志测试一下:getUserById方法
@Test
public void selectUserById() {
SqlSession sqlSession = MybatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectUserById(1);
System.out.println(user);
System.out.println("=================");
User user1 = mapper.selectUserById(1);
System.out.println(user1);
System.out.println(user == user1);
sqlSession.close();
}
第一次查询User需要到数据库,第二次查询就会先访问一级缓存,查看是否存在id=1的user
深挖一级缓存
在前面流程分析时,Mybatis - 执行流程分析(源码分析)
一个SqlSession会持有一个Executor执行器,复杂查询缓存的维护
DefaultSqlSession持有BaseExecutor,BaseExecutor聚合了PerpetualCache
执行器是Mybatis的核心类,研究一级缓存可以查看BaseExecutor、PerpetualCache源码
PerpetualCache本质是一个HashMap,putObject、setObject,存取缓存
分析Executor的查询过程:
如果只开启一级缓存,调用query方法
BoundSql存入sql语句,将MappedStatement的Id、sql的offset、Sql的limit、Sql本身以及Sql中的参数传入了CacheKey这个类,CacheKey用来表示缓存项(通过多个属性唯一标识一个缓存项)
Statement Id + Offset + Limmit + Sql + Params相同,即可认为是相同sql
上面的query方法是创建CacheKey,然后进入真正处理的query方法
先查看一级缓存是否存储这个对象(map的key值为CacheKey),存在的话就直接取出,不存在就查数据库
会将数据库查到的值存入一级缓存
query方法的最后,会判断一级缓存级别是否为STATEMENT,是的话会清空缓存
(所以如果不需要缓存可以在mybatis的配置文件设置localCacheScope=statement)
官网:settings标签
关于增删改的问题
增删改都会进入executor的update方法
而update方法必定会清空缓存
一级缓存生命周期
从上面的分析中,可以看出一级缓存是生命周期:
- 一级缓存是会话的缓存,在一个SqlSession中,SqlSession关闭即缓存清空
- sqlSession可以手动清空缓存,调用clearCache方法
- 当update、delete、insert方法发生时,会清空缓存
二级缓存
一级缓存是一个SqlSession内的缓存,仅作用与一次会话
那如果想在一个Mapper中都缓存,就需要二级缓存
开启缓存方式:
在Mapper.xml中使用Cache标签
<cache/>
或者在mybatis-config.xml中设置
<setting name="cacheEnabled" value="true"/>
当然,这是默认值,Mybatis提供了一些参数
- type:引入自定义的缓存类(需要写全类名)
- eviction:清除策略,例如先进先出FIFO,最近最少使用LRU等
- flushInterval:刷新间隔,以毫秒为单位
- size:引用大小,最多可存储多少个结果集的引用(可以为任意正数)
- readOnly:设置为只读,只读的缓存会给所有调用者返回缓存对象的相同实例,可读写的缓存会(通过序列化)返回缓存对象的拷贝,默认为false,速度慢但是更安全
- blocking:是否使用阻塞缓存,默认为false,当指定为true时将采用BlockingCache进行封装,使用BlockingCache会在查询缓存时锁住对应的Key,如果缓存命中了则会释放对应的锁,否则会在查询数据库以后再释放锁这样可以阻止并发情况下多个线程同时查询数据
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
当开启二级缓存,就会缓存当前mapper.xml中的结果集
二级缓存是Application级的缓存,一级缓存缓存的是SQL语句,而二级缓存缓存的是结果对象,二级缓存会存储一级缓存的内容
二级缓存细节
一个简单的测试类:创建两个SqlSession,都查询id=1的User
@Test
public void cache() {
SqlSession sqlSession = MybatisUtils.getSqlSession();
SqlSession sqlSession2 = MybatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectUserById(1);
System.out.println(user);
System.out.println("=================");
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
User user1 = mapper2.selectUserById(1);
System.out.println(user1);
System.out.println(user == user1);
sqlSession.close();
sqlSession2.close();
}
可以看到,创建了两次连接,第二次连接查询了一次缓存
需要先将数据存入一级缓存中,而后二级缓存会获得一级缓存的数据
将sqlSession.close();
提前,放到第二次查询之前
然后会报错,NotSerializableException
还记得二级缓存的原理:通过序列化拷贝缓存对象
所以实体类需要实现序列化接口
最终结果:
比较两个对象,是false,因为是通过序列化拷贝的对象,并不是原对象
如果将cache设置为仅读就不用序列化
设置readOnly取出的缓存数据,对象是相同的,因为返回的都是缓存里的这个实例
二级缓存也是使用PerpetualCache对象,在获取Cache时先判断是否有二级缓存
配置文件
mybatis-config.xml
在mybatis-config.xml中有两个属性:
cacheEnabled,开启所有Mapper.xml的二级缓存
当然,如果只想要开启某些mapper的二级缓存还是要在对应的mapper设置
localCacheScope,设置一级缓存,STATEMENT设置是仅作用与当前执行语句
mapper.xml
cache标签:开启二级缓存
cache-ref标签:引入其他mapper.xml的缓存配置
(别同时配置这两个标签,后设置的会覆盖先设置的)
对应某个增删改查标签:
flushCache="true"表示该语句的执行结果,会清空本地缓存以及2级缓存
useCache="true"表示该语句的执行结果,会被缓存到到2级缓存
总结
调用缓存的过程:
缓存是一个很重要的提升系统性能的功能,看需要选择一级缓存、二级缓存
但是Mybatis的缓存性能并不强,所以还是推荐其他组件
学海无涯苦作舟
都看到这了,点个赞呗(^_−)☆