myBatis缓存综述
为了避免相同的查询重复去数据库中取值,myBatis形成了两级缓存机制,先从第二级缓存中获取,获取不到时从第一级缓存中获取,仍然取不到时才会实际的去库中查询。其中二级缓存是SqlSessionFactory级别的,所有由同一个工厂构建出来的SqlSession共享缓存,而一级缓存是SqlSession级别的,每个SqlSession独享缓存。在实现上,myBatis以Cache接口为核心、以PerpetualCache为基本实现,构建了多个单一职能的缓存组件,这些组件,有的实现了缓存结构满时,旧缓存的替换,比如最近最少使用算法的LruCache、先进先出的FifoCache等;有的实现了在存取缓存时打出日志,比如LoggingCache;还有的能周期性的对缓存进行清理,如ScheduledCache等。最终以装饰器模式进行组合复用,形成了多层次的缓存体系。
缓存名称 | 特性 | 依赖的数据结构 |
---|---|---|
PerpetualCache | 底层cache | HashMap |
FifoCache | 先进先出替换 | Deque |
LruCache | 最近最少使用的被替换 | LinkedHashMap |
ScheduledCache | 周期性的清除缓存 | |
BlockingCache | ConcurrentHashMap<Object, ReentrantLock> | |
LoggingCache | 打日志 | Log |
SerializedCache | 将缓存的值序列化存储 | |
SynchronizedCache | 所有的方法加入了synchronized | |
SoftCache | ReferenceQueue、SoftReference | |
WeakCache | ReferenceQueue、WeakReference | |
TransactionalCache |
一级缓存的实现
一级缓存是默认开启的,无需任务配置。在实现方式上,是直接在构造Executor对象时生成名为localCache的PerpetualCache缓存,没有任何形式的装饰和组合,其底层维护一个HashMap存储cache。
一级缓存的存值时机,是在BaseExecutor的queryFromDatabase方法中完成的:
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
取值时机是在BaseExecutor的query方法中。
二级缓存的配置与实现
二级缓存是默认不开启的,需要在mapper.xml中或者@CacheNamespace注解显式的进行设置,同一个SqlSessionFactory构建的SqlSession共享缓存,和SQL语句的解析一样,在初始化阶段就将缓存的配置一起解析,最终将缓存对象放置到Configuration对象中。
配置的例子:
<mapper namespace="com.mybatis.read.CountryDao">
<cache eviction="LRU" readOnly="true" flushInterval="100000" size="1024"></cache>
<select id="queryAllCountry" resultType="com.mybatis.read.Country">
SELECT * FROM oaxt.country
</select>
</mapper>
Configuration中使用caches来管理每个namespace对应的Cache对象:
protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
XMLMapperBuilder解析mapper的cache结点:
private void cacheElement(XNode context) {
if (context != null) {
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
Properties props = context.getChildrenAsProperties();
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
二级缓存的读取和放入是在CachingExecutor的TransactionalCacheManager对象完成的:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 在mapper.xml中配置了cache结点后,这里将取到不为空的cache对象
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 这里使用的是二级缓存
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 将在BaseExecutor中使用二级缓存
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
以图片的形式描述上述过程:
需要注意的是,在使用二级缓存过程中,只有执行了commit操作才会真正的将需要缓存的对象存入缓存结构中,TransactionCache的putObject方法只是简单的执行了entriesToAddOnCommit.put(key, object)操作,只有commit时才会调用实际缓存对象的putObject方法:
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
缓存构建中的装饰器模式
以Cache接口为基础,以PerpetualCache为基本实现,定义了ScheduledCache、LoggingCache等实现了不同功能的cache组,使用装饰器模式进行不同形式的组合实现功能的拼装。在CacheBuilder中可以看到构造多重功能复合的cache组件的过程:
private Cache setStandardDecorators(Cache cache) {
try {
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
if (clearInterval != null) {
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
if (readWrite) {
cache = new SerializedCache(cache);
}
cache = new LoggingCache(cache);
cache = new SynchronizedCache(cache);
if (blocking) {
cache = new BlockingCache(cache);
}
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
}
自定义Cache实现
myBatis提供的缓存的基础实现是在堆内存上HashMap缓存数据,当数据量大时会占据大量堆内存,降低应用的整体性能。可以利用其提供的Cache接口,在二级缓存中进行自定义实现Cache,将数据缓存到Redis等外部存储中,更好的发挥缓存的作用。
CacheBuilder的builde方法是能够进行自定义Cache实现的基础:
public Cache build() {
setDefaultImplementations();
// 这里的implementatin解析的是mapper.xml配置中的cache结点的type属性
// 当不传时,默认是PerpetualCache
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
// issue #352, do not apply decorators to custom caches
if (PerpetualCache.class.equals(cache.getClass())) {
for (Class<? extends Cache> decorator : decorators) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
cache = new LoggingCache(cache);
}
return cache;
}
当前主流的第三方缓存对myBatis都提供了集成:
缓存插件 | cache类名 | github地址 | document地址 |
---|---|---|---|
Redis | RedisCache | https://github.com/mybatis/redis-cache | mybatis.org/redis-cache/ |
EhCache | EhcacheCache | https://github.com/mybatis/ehcache-cache | mybatis.org/ehcache-cache/ |
memcached | MemcachedCache | https://github.com/mybatis/memcached-cache | mybatis.org/memcached-cache/ |