前言
缓存机制是MyBatis的一大特性,它分为一级和二级缓存,其中一级缓存是SqlSession
级别的缓存,其实现较为简单,而二级缓存是mapper级别的缓存,并且多个SqlSession
之间可以共享。
一级缓存
每当我们使用MyBatis开启一次和数据库的会话,MyBatis会创建出一个SqlSession
对象表示一次数据库会话,对于这种会话级别的数据缓存,我们就称之为一级缓存。通过一级缓存,每次查询时都将结果缓存起来,当下次查询的时候,如果判断先前有个完全一样的查询,会从缓存中直接将结果取出,返回给用户,不需要再进行一次数据库查询了。
我们知道,SqlSession
只是一个MyBatis对外的接口,SqlSession
将它的工作交给了Executor
执行器这个角色来完成,负责完成对数据库的各种操作。当创建了一个SqlSession
对象时,MyBatis会为这个SqlSession
对象创建一个新的Executor
执行器,而缓存信息就被维护在这个Executor
执行器中,MyBatis将缓存和对缓存相关的操作封装到了Cache
接口中。
PerpetualCache
Executor
接口的实现类BaseExecutor
中拥有一个Cache
接口的实现类PerpetualCache
,则对于BaseExecutor
对象而言,它将使用PerpetualCache
对象维护缓存,下面看一下它的源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53public class PerpetualCache implements Cache {
private String id;
private Map<Object, Object> cache = new HashMap<Object, Object>();
public PerpetualCache(String id) {
this.id = id;
}
public String getId() {
return id;
}
public int getSize() {
return cache.size();
}
public void putObject(Object key, Object value) {
cache.put(key, value);
}
public Object getObject(Object key) {
return cache.get(key);
}
public Object removeObject(Object key) {
return cache.remove(key);
}
public void clear() {
cache.clear();
}
public ReadWriteLock getReadWriteLock() {
return null;
}
public boolean equals(Object o) {
if (getId() == null) throw new CacheException("Cache instances require an ID.");
if (this == o) return true;
if (!(o instanceof Cache)) return false;
Cache otherCache = (Cache) o;
return getId().equals(otherCache.getId());
}
public int hashCode() {
if (getId() == null) throw new CacheException("Cache instances require an ID.");
return getId().hashCode();
}
}
可以看出,PerpetualCache
实现原理其实很简单,其内部就是通过一个简单的HashMap
来实现的,没有其他的任何限制。
生命周期
关于一级缓存的生命周期,有以下几条规则:
- MyBatis在开启一个数据库会话时,会创建一个新的
SqlSession
对象,SqlSession
对象中会有一个新的Executor
对象,Executor
对象中持有一个新的PerpetualCache
对象;当会话结束时,SqlSession
对象及其内部的Executor
对象还有PerpetualCache
对象也一并释放掉。 - 如果
SqlSession
调用了close()
方法,会释放掉一级缓存PerpetualCache
对象,一级缓存将不可用; - 如果
SqlSession
调用了clearCache()
,会清空PerpetualCache
对象中的数据,但是该对象仍可使用; SqlSession
中执行了任何一个update操作(update()
、delete()
、insert()
) ,都会清空PerpetualCache
对象的数据,但是该对象可以继续使用;
工作流程
- 对于某个查询,会构建一个key值,根据这个key值去缓存
Cache
中取出对应的缓存结果 - 判断从
Cache
中根据特定的key值取的数据是否为空,即是否命中 - 如果命中,则直接将缓存结果返回
- 如果没命中:
4.1 去数据库中查询数据,得到查询结果
4.2 将key和查询到的结果分别作为key-value对存储到Cache
中
4.3. 将查询结果返回 - 结束
CacheKey
在上面的工作流程中已经看到,Cache
中的Map
是根据一个key来查询缓存的,这个key的决定因素具体如下:
- 传入的
statementId
(比如为com.xxx.mapper.selectUserName
) - 查询时要求的结果集中的结果范围 (结果的范围通过
rowBounds.offset
和rowBounds.limit
表示) - 这次查询所产生的最终要传递给JDBC
Preparedstatement
的SQL语句(boundSql.getSql()
) - 要设置的参数值(只用这个SQL语句所需要的参数)
因此,CacheKey其实就是由statementId + rowBounds + 传递给JDBC的SQL + 传递给JDBC的参数值
这四个条件并生成哈希构建而成的。MyBatis认为,对于两次查询,只要构建出的CacheKey一样,就认为它们是完全相同的查询,也就可以根据这个CacheKey去缓存中查找已有的缓存结果。
注意事项
MyBatis的一级缓存就是简单的使用了HashMap
,MyBatis只负责将查询数据库的结果存储到缓存中去,不会去判断缓存存放的时间是否过长、是否过期,并且也没有对缓存的大小进行限制,因此对于准确性要求比较高的数据来说,要控制好SqlSession
的生存时间,其生存时间越长,它缓存的数据有可能就越旧,从而造成与真实数据库的误差较大。
二级缓存
当开一个会话时,一个SqlSession
对象会使用一个Executor
对象来完成会话操作,MyBatis的二级缓存机制的关键就是对这个Executor
对象做文章。如果用户配置了cacheEnabled=true
,那么MyBatis在为SqlSession
对象创建Executor
对象时,会对Executor
对象加上一个装饰者:CachingExecutor
,这时SqlSession
使用CachingExecutor
对象来完成操作请求。CachingExecutor
对于查询请求,会先判断该查询请求在二级缓存中是否有缓存结果,如果有缓存结果,则直接返回该结果;如果缓存中没有,再交给真正的Executor
对象来完成查询操作,之后CachingExecutor
会将真正Executor
返回的查询结果放置到缓存中,然后在返回给用户。
缓存粒度
MyBatis并不是简单地对整个Application就只有一个Cache
缓存对象,它将缓存划分的更细,即是Mapper级别的,每一个Mapper都可以拥有一个Cache
对象,具体如下:
- 为每一个Mapper分配一个
Cache
缓存对象(使用<cache>
节点配置) - 多个Mapper共用一个
Cache
缓存对象(使用<cache-ref>
节点配置)
使用条件
虽然在Mapper中配置了<cache>
,并且为此Mapper分配了Cache
对象,这并不表示我们使用Mapper中定义的查询语句查到的结果都会放置到Cache
对象之中,我们必须指定Mapper中的某条选择语句是否支持缓存,即在<select>
节点中配置useCache="true"
,如 <select id="selectByMinSalary" resultMap="BaseResultMap" parameterType="java.util.Map" useCache="true">
,Mapper才会对此Select的查询支持缓存特性。
总结来说,要想使某条select
查询支持二级缓存,需要保证:
- MyBatis支持二级缓存的总开关:全局配置变量参数
cacheEnabled=true
- 该
select
语句所在的Mapper,配置了<cache>
或<cached-ref>
节点,并且有效 - 该
select
语句的参数useCache=true
二级缓存实现的选择
MyBatis对二级缓存的设计非常灵活,它自己内部实现了一系列的Cache
缓存实现类,有大量的Cache
的装饰器来增强Cache
缓存的功能。另外,MyBatis还允许用户自定义Cache
接口实现,用户只需要实现org.apache.ibatis.cache.Cache
接口,然后将Cache
实现类配置在<cache type="">
节点的type
属性上即可。除此之外,MyBatis还支持跟第三方内存缓存库如Memecached的集成。总之,使用MyBatis的二级缓存有三个选择:
- MyBatis自身提供的缓存实现
- 用户自定义的
Cache
接口实现 - 跟第三方内存缓存库的集成