- 一级缓存
- 二级缓存
- 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
内部维护一个Map
1
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
和lastClear
1
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
已存在,则不允许添加,抛出异常。