- 一级缓存
- 二级缓存
- Cache
- 缓存装饰器
一级缓存
一级缓存,又叫本地缓存,是PerpetualCache类型的永久缓存,保存在执行器中(BaseExecutor),而执行器又在SqlSession(DefaultSqlSession)中,所以一级缓存的生命周期与SqlSession是相同的。
MyBatis的一级缓存指的是在一个Session域内,Session为关闭的时候执行的查询会根据SQL为key被缓存,单独使用MyBatis而不继承Spring,使用原生的MyBatis的SqlSessionFactory来构造sqlSession查询,是可以使用以及缓存的。
当参数不变的时候只进行一次查询,参数变更以后,则需要重新进行查询,而清空缓存以后,参数相同的查询过的SQL也需要重新查询,当执行SQL时两次查询中间发生增删改操作,则SqlSession的缓存清空。
如果集成Spring是没有使用一级缓存。原因是一个sqlSession,但是实际上因为我们的dao继承SqlSessionDaoSupport,而SqlSessionDaoSupport内部sqlSession的实现是使用用动态代理实现的,这个动态代理sqlSessionProxy使用一个模板方法封装select()等操作,每一次select()查询都会自动先执行openSession(),执行完后调用close()方法,相当于生成一个新的session实例,所以我们无需手动的去关闭这个session(),当然也无法使用MyBatis的一级缓存,也就是说MyBatis的一级缓存在Spring中是没有作用的。
官方文档
MyBatis SqlSession provides you with specific methods to handle transactions programmatically. But when using MyBatis-Spring your beans will be injected with a Spring managed SqlSession or a Spring managed mapper. That means that Spring will always handle your transactions.
You cannot call SqlSession.commit(), SqlSession.rollback() or SqlSession.close() over a Spring managed SqlSession. If you try to do so, a UnsupportedOperationException exception will be thrown. Note these methods are not exposed in injected mapper classes.
二级缓存
二级缓存,又叫自定义缓存,实现Cache接口的类都可以作为二级缓存,所以可配置如encache等的第三方缓存。二级缓存以namespace名称空间为其唯一标识,被保存在Configuration核心配置对象中。1
2
3
4
5public class Configuration {
// ...
protected final Map<String, Cache> caches = new StrictMap<Cache>("Caches collection");
// ...
}
每次构建SqlSessionFactory对象时都会创建新的Configuration对象,因此,二级缓存的生命周期与SqlSessionFactory是相同的。在创建每个MapperedStatement对象时,都会根据其所属的namespace名称空间,给其分配Cache缓存对象。
二级缓存同样执行增删查改操作,会清空缓存。
二级缓存就是global caching,它超出session范围之外,可以被所有sqlSession共享,它的实现机制和MySQL的缓存一样,开启它只需要在MyBatis的配置文件开启settings里。1
<setting name="cacheEnabled" value="true"/>
在相应的Mapper文件里:1
2
3
4
5
6
7
8
9<mapper namespace="dao.userdao">
... select statement ...
<!-- Cache 配置 -->
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true" />
</mapper>
需要注意的是global caching的作用域是针对Mapper#Namespace而言的,也就是说只在有在这个Namespace内的查询才能共享这个cache。下面是官方文档的介绍:
It’s important to remember that a cache configuration and the cache instance are bound to the namespace of the SQL Map file. Thus, all statements in the same namespace as the cache are bound by it.
注意
- 映射语句文件中的所有
select语句将会被缓存。 - 映射语句文件中的所有
insert,update和delete语句会刷新缓存。 - 缓存会使用
Least Recently Used(LRU,最近最少使用的)算法来收回。 - 缓存会根据指定的时间间隔来刷新。
- 缓存会存储1024个对象。
如果二级缓存想要命中实现,则必须要将上一次sqlSession commit之后才能生效,不然将不会命中。原因:两个不同的session必须提交前面一个session才能缓存生效的,原因是因为MyBatis的缓存会被一个Transactioncache类包装住,所有的cache#putObject全部都会被暂时存到一个map里,等事务提交以后,这个map里的缓存对象才会被真正的cache类执行putObject操作。这么设计的原因是防止事务执行过程中出异常导致回滚,如果get到object后直接put进缓存,万一发生回滚,就很容易导致MyBatis缓存被脏读。
Cache
在MyBatis中,缓存的功能由根接口Cache(org.apache.ibatis.cache.Cache)定义。整个体系采用装饰器设计模式,数据存储和缓存的基本功能由PerpetualCache(org.apache.ibatis.cache.impl.PerpetualCache)永久缓存实现,然后通过一系列的装饰器来对PerpetualCache永久缓存进行缓存策略等方便的控制。如下图。
用于装饰PerpetualCache的标准装饰器共有8个(全部在org.apache.ibatis.cache.decorators包中):
FifoCache
先进先出算法,缓存回收策略
LoggingCache
输出缓存命中的日志信息
LruCache
最近最少使用算法,缓存回收策略
ScheduledCache
调度缓存,负责定时清空缓存
SerializedCache
缓存序列化和反序列化存储
SoftCache
基于软引用实现的缓存管理策略
SynchronizedCache
同步的缓存装饰器,用于防止多线程并发访问
WeakCache
基于弱引用实现的缓存管理策略
另外,还有一个特殊的装饰器TransactionalCache(事务性的缓存)
所有的缓存对象的操作与维护都是由Executor器执行来完成的,一级缓存由BaseExecutor(包含SimpleExecutor、ReuseExecutor、BatchExecutor三个子类)负责维护,二级缓存由CachingExecutor负责维护。因此需要注意的是,配置二级缓存不代表MyBatis就会使用二级缓存,还需要确保在创建SqlSession的过程中,MyBatis创建是CachingExecutor类型的执行器。
Cache
1 | package org.apache.ibatis.cache; |
PerpetualCache
1 | package org.apache.ibatis.cache.impl; |
在MyBatis中,PerpetualCache是唯一的Cache接口的基础实现。它内部维护一个HashMap,所有的缓存操作,其实都是对这个HashMap的操作。
缓存装饰器
LruCache最近最少使用的回收策略
1 | package org.apache.ibatis.cache.decorators; |
LruCache内部维护一个Map1
private Map<Object, Object> keyMap;
它的实际类型是LinkedHashMap<Object,Object>的匿名子类,子类重写removeEldesEntry(),用于获取在达到容量限制时被删除的key。
LruCache#setSize
1 | public void setSize(final int size) { |
在每次调用setSize(),都会创建一个新的该类型的对象,同时指定其容量大小。第三个参数为true代表Map中的键值对列表要按照访问顺序排序,每次被方位的键值对都会被移动到列表尾部(值为false时按照插入顺序排序)。
LruCache#putObject
1 | public void putObject(Object key, Object value) { |
在每次给代理缓存对象添加完键值对后,都会调用cycleKeyList()进行一次检查。
LruCache#cycleKeyList
1 | /** |
LruCache把新添加的键值对的键添加到keyMap中,如果发现keyMap内部删除一个key,则同样把代理缓存对象中相同的key删除。LruCache就是以这种方式实现最近最少访问回收算法的。
ScheduledCache调度缓存装饰器
它的内部维护2个字段,clearInterval和lastClear1
2
3
4// 调用clear()清空缓存的时间间隔,单位毫秒,默认1小时
protected long clearInterval;
// 最后一次清空缓存的时间,单位毫秒
protected long lastClear;
在对代理的缓存对象进行任何操作之前,都会首先调用clearWhenStale()方法检查当前时间点是否需要清理一次缓存,如果需要则进行清理并返回true,否则返回false。
ScheduledCache#clearWhenStale()
1 | /** |
SerializedCache序列化缓存装饰器
它负责在调用putObject(key,value)方法保存key/value时,把value序列化为字节数组;在调用getObject(key)获取value时,把获取到字节数组再反序列化回来。
使用此装饰器的前提是,所有要缓存的value必须实现Serializable接口,否则会抛出CacheException异常。
SerializedCache#putObject()
1 | @Override |
SerializedCache#getObject()
1 | @Override |
在反序列化时,SerializedCache使用内部定义的类CustomObjectInputStream,此类继承自ObjectInputStream,重写父类的resolveClass(),区别在于解析加载Class对象时使用的ClassLoader类加载器不同。
SerializedCache#CustomObjectInputStream
1 | public static class CustomObjectInputStream extends ObjectInputStream { |
LoggingCache日志缓存装饰器
它内部维护2个字段,requests查询次数计数器和hits查询命中次数计数器1
2
3
4
5
6
7
8 // 日志记录器
private Log log;
// 代理的缓存对象
private Cache delegate;
// 每次调用getObject(key)查询时,此值+1
protected int requests = 0;
// 每次调用getObject(key)获取到的value不为null时,此值+1
protected int hits = 0;
在每次调用getObject()从缓存对象中查询值时,都会迭代这两个计数器,并且计算实时命中率,打印到日志中。
LoggingCache#getObject()
1 | @Override |
LoggingCache#getHitRatio()
1 | /** |
SynchronizedCache同步的缓存装饰器
这个装饰器比较简单,只是把所有操作缓存对象的方法上都加synchronized,用来避免多线程并发访问。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 @Override
public synchronized int getSize() {
return delegate.getSize();
}
@Override
public synchronized void putObject(Object key, Object object) {
delegate.putObject(key, object);
}
@Override
public synchronized Object getObject(Object key) {
return delegate.getObject(key);
}
@Override
public synchronized Object removeObject(Object key) {
return delegate.removeObject(key);
}
@Override
public synchronized void clear() {
delegate.clear();
}
FifoCache先进先出缓存回收策略装饰器
它内部维护一个不限容量的LinkedList,名称为keyList,在构造方法中被创建。1
private LinkedList<Object> keyList;
FifoCache#FifoCache()
1 | public FifoCache(Cache delegate) { |
在调用putObject()添加缓存时,会在向代理的缓存对象中添加数据之前,调用cycleKeyList()进行一次验证,如果keyList超过限制长度,则进行回收。
FifoCache#putObject()
1 | @Override |
FifoCache#cycleKeyList()
1 | private void cycleKeyList(Object key) { |
SoftCache软引用缓存装饰器
它利用JDK的SoftReference软引用,借助垃圾回收器进行缓存对象的回收。
通常使用的引用方式都是强引用,如:Object obj = new Object();只要引用变量obj!=null,那么Object对象永远不会被垃圾回收器回收。而软引用的引用方式是这样的。1
SoftReference ref = new SoftReference(new Object());
引用变量ref引用SoftReference对象(这属于强引用),再由SoftReference对象内部引用new Object(这属于软引用)。
SoftCache数据结构
1 | // 强引用集合,最近一此查询命中的对象,其引用会被加入此集合的头部 |
SoftCache在写缓存之前,会先调用removeGarbageCollectedItems()删除已经被垃圾回收器回收的key/value,之后想缓存对象中写入SoftEntry类型的对象(定义在SoftCache的内部,是SoftReference类的子类)。
SoftCache#putObject()
1 | @Override |
SoftCache#removeGarbageCollectedItems()
1 | /** |
SoftCache在读缓存时,是直接读取的。这样存在一个问题,缓存的value对象已经被垃圾回收器回收,但是该对象的软引用对象还存在,这种情况下要删除缓存对象中,软引用对象对应的key。另外,每次调用getObject()查询到缓存对象中的value还未被回收时,都会把此对象的引用临时加入强引用集合,这样确保该对象不会被回收。这种机制保证访问频次越搞的value对象,被回收的几率越小。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@Override
public Object getObject(Object key) {
Object result = null;
@SuppressWarnings("unchecked") // assumed delegate cache is totally
// managed by this cache
SoftReference<Object> softReference = (SoftReference<Object>) delegate.getObject(key);
// 引用变量为null,说明不存在key指定的键值对
if (softReference != null) {
// 键值对存在的情况下,获取软引用变量引用的对象
result = softReference.get();
// 如果引用的value已经被回收,则删除缓存对象中的key
if (result == null) {
delegate.removeObject(key);
} else {
// See #586 (and #335) modifications need more than a read lock
// 缓存的value对象没有被回收,且这次访问到了,则把此对象引用加入强引用集合
// 使其不会被回收
synchronized (hardLinksToAvoidGarbageCollection) {
hardLinksToAvoidGarbageCollection.addFirst(result);
if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {
hardLinksToAvoidGarbageCollection.removeLast();
}
}
}
}
return result;
}
WeakCache弱引用缓存装饰器
WeakCache在实现上与SoftCache几乎相同,只是把引用对象由SoftReference软引用换成WeakReference弱引用。
TransactionalCache事务性缓存
TransactionalCache比其他Cache对象多出2个方法:commit()和rollback()。TransactionalCache对象内部存在暂存区,所有对缓存对象的写操作都不会直接作用于缓存对象,而是被保存在暂存区,只有调用TransactionalCache#commit(),所有的更新操作才会真正同步到缓存对象中。(二级缓存先执行commit()才能使用的原因)
这样的话,就会存在一个问题,如果事务被设置为自动提交(autoCommit=true)的话,写操作会更新RDBMS(关系型数据库管理系统),但不会清空缓存对象(因为自动提交不会调用commit()),这样会产生数据库与缓存中数据不一致的情况。如果缓存没有过期失效的机制,那么问题会很严重。
TransactionalCache数据结构
1 | // 原始的被代理的Cache缓存对象 |
之所以把putObject操作分为删除和添加两步,可能是因为有的缓存的添加逻辑是:如果key已存在,则不允许添加,抛出异常。

