在从源码聊聊mybatis一次查询都经历了些什么一文中咱们梳理了mybatis执行查询SQL的具体流程,在Executor中简单提到了缓存。本文将从源码一步一步详细解析mybatis缓存的架构,以及自定义缓存等相关内容。因为一级缓存是写死在代码里面的,因此本文重点讨论的是二级缓存,下文中提到的缓存若是没有特别指定的话都是指二级缓存。java
实现自定义缓存很是简单,只须要实现org.apache.ibatis.cache.Cache
接口,而后为须要的Mapper配置实现就能够了。
下面的代码是一个简单的缓存实现sql
@Slf4j
public class MyCache implements Cache, InitializingObject {
private String id;
private String key;
private Map<Object, Object> table = new ConcurrentHashMap<>();
public MyCache(String id) {
this.id = id;
}
@Override
public void initialize() throws Exception {
log.info("id = {}", id);
log.info("key = {}", key);
}
// ......
}
复制代码
使用注解方式为Mapper配置缓存,使用XML配置也是相似的apache
@Mapper
@CacheNamespace(
// 指定实现类
implementation = MyCache.class,
// 指定淘汰策略(也实现了Cache接口),mybatis经过装饰者模式实现淘汰策略
// 只有当implementation是PerpetualCache时才会生效
eviction = LruCache.class,
// 配置缓存属性,mybatis会将对应的属性注入到缓存对象中
properties = {
@Property(name = "key", value = "hello mybatis")
}
)
public interface AddressMapper {
// ......
}
复制代码
缓存是什么时候建立的呢?咱们不妨想一下,缓存是配置在Mapper上的,那么应该会在解析Mapper的时候顺便把缓存配置也解析了吧。咱们不妨先看看Mapper配置解析的代码,Configuration类添加Mapper时会调用org.apache.ibatis.binding.MapperRegistry
的addMapper
方法,以下所示,很直观的,这里使用了一个叫作MapperAnnotationBuilder
的类来解析Mapper
注解。缓存
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
knownMappers.put(type, new MapperProxyFactory<T>(type));
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
}
}
复制代码
那么咱们关注一下这个类的parse
方法,很是棒,咱们一会儿就找到了解析缓存配置的地方。mybatis
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
loadXmlResource();
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
// 解析缓存
parseCache();
// 解析引用的缓存
parseCacheRef();
Method[] methods = type.getMethods();
for (Method method : methods) {
if (!method.isBridge()) {
// 解析生成MappedStatement
parseStatement(method);
}
}
}
parsePendingMethods();
}
复制代码
parseCache
方法也很是直观,简单粗暴,取出@CacheNamespace
注解中的配置,而后传递给MapperBuilderAssistant#useNewCache
方法建立缓存对象,MapperBuilderAssistant
是构建Mapper的一个辅助类。架构
private void parseCache() {
CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);
if (cacheDomain != null) {
Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size();
Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval();
// 把属性配置转成Properties对象
Properties props = convertToProperties(cacheDomain.properties());
assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval,
size, cacheDomain.readWrite(), cacheDomain.blocking(), props);
}
}
复制代码
先把缓存对象添加到配置对象的注册表中,这样的话其余的Mapper就能够经过配置@CacheNamespaceRef
来引用这个缓存对象了。而后设置缓存对象到辅助类的成员变量,在后面建立MappedStatement时候拿出使用。app
public Cache useNewCache(/* ... */) {
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
// 添加到配置对象的缓存注册表中
configuration.addCache(cache);
// 设置为当前Mapper的缓存,后面构建MappedStatement的时候会用到
currentCache = cache;
return cache;
}
复制代码
而后再看看CacheBuilder#build
方法都干了些啥吧,具体细节我注释在下面的代码里面。ide
public Cache build() {
// 首先,确保实现类和淘汰策略为空的时候,设置默认的实现PerpetualCache和LruCache
setDefaultImplementations();
// 这里要求实现的缓存类必须提供一个带id参数的构造器,否则就会报错
Cache cache = newBaseCacheInstance(implementation, id);
// 设置经过@Property配置的属性到缓存对象中,而后若是实现了InitializingObject接口还会调用initialize方法
setCacheProperties(cache);
// 从下面这段逻辑能够看出来,咱们配置的缓存淘汰策略只对默认缓存有效果
// 自定义缓存须要本身实现淘汰策略
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;
}
复制代码
这个建立好的缓存是如何配置到MappedStatement中去的呢?回到MapperAnnotationBuilder#parse
方法找到parseStatement(method)
,最终会调用到MapperBuilderAssistant#addMappedStatement()
方法,下面代码就会把刚才建立的缓存对象设置到每一个MappedStatement中去,因而可知mybatis二级缓存的做用域是整个Mapper的(若是被其余Mapper引用,还会扩张)。svg
public MappedStatement addMappedStatement(/* ... */) {
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
/* ... */
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);
/* ... */
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
}
复制代码
到这里终因而把咱们自定义的缓存设置到了配置中了,接下来就是缓存的使用了。ui
在从源码聊聊mybatis一次查询都经历了些什么这篇文章中简单提到过缓存的使用是在CachingExecutor
中。再把代码贴过来看一看:
public <E> List<E> query(/* ... */) throws SQLException {
// 这里就取到前面设置到ms(MappedStatement)中的缓存对象了
Cache cache = ms.getCache();
if (cache != null) {
// 经过上面的配置就能知道,默认状况下除了select都须要清空
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
// 又懵逼了?这个tcm是啥
List<E> list = (List<E>) tcm.getObject(cache, key);
// 缓存未命中,查库
if (list == null) {
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list);
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
复制代码
一切都瓜熟蒂落了,不过半路杀出个程咬金,这个tcm(TransactionalCacheManager)是什么东西呢?看看下面这张图,mybatis的每次会话(SqlSession)都会建立一个tcm,这个tcm里面其实维护着一个HashMap,map的key就是Mapper的cache对象,value是一个使用TransactionalCache
装饰的cache对象。 {% asset_img cache.svg mybatis缓存 %} 从名字就能够猜一猜,这个TransactionalCache应该是和事务有关系的,从下面的代码能够看出,putObject操做并无直接添加到缓存中,而是先put到一个本地Map,而后再批量提交。getObject缓存未命中时会把key添加到一个本地的Set中,在将来批量提交的时候会把这个Set中的key也put到缓存中,value设置为null,来防止缓存穿透。
public class TransactionalCache implements Cache {
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
public Object getObject(Object key) {
Object object = delegate.getObject(key);
if (object == null) {
// 未命中key添加到Set中
entriesMissedInCache.add(key);
}
/* ... */
}
public void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear();
}
public void commit() {
// clearOnCommit在TransactionalCache#clear方法被调用后设置为true
// 此时才会在提交的时候清空delegate
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
// 为未命中的key设置null
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
}
复制代码
至于何时commit会被调用呢,咱们再回看一下TransactionalCacheManager的commit,会提交当前SqlSession全部Mapper的缓存,而TransactionalCacheManager的commit是在CachingExecutor的commit中调用的,而Executor的commit又依赖与SqlSession的commit操做,也就是说,若是咱们不手动调用SqlSession的commit的话,就只能等到SqlSession关闭的时候才会提交这个查询缓存。
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
复制代码
从源码咱们不难发现CachingExecutor在每次调用update方法的时候,都会先清空TransactionalCache的本地的HashMap,而后在提交的时候再清空Mapper的缓存。所以,在更新操做比较频繁的场景下,二级缓存反而不会起到很好的做用。因此是否开启二级缓存,还要取决于业务场景。可能大部分的场景下,关闭二级缓存都是一个比较不错的方案。