Mybatis 缓存
用过Mybatis的人或者看过关于Mybatis的面试题的人。肯定了解过Mybatis的二级缓存,关于一级缓存和二级缓存起什么作用?怎么开启。这些东西在网上一找一大篇。随便找一篇看看就好了。这里着重从源码角度来看缓存的具体实现?并且思考伴随着缓存就一直存在的问题。比如
- 缓存是否会失效?缓存啥时候失效?啥时候将数据放进缓存?
- 缓存有没有有效期?缓存使用的算法?
- 缓存中的数据怎么保持和数据库数据的一致性?
所以,从下面开始,就分析分析Mybatis 缓存的问题
1. 简介
简单的介绍一下Mybatis的一二级缓存,介绍怎么用不是本文的重点,可以自行在网上找一个篇博客看看。这里旨在从源码分析,简单的介绍一下,Mybatis有两级缓存,一级缓存是开启,并且无法关闭,作用于一个SqlSession,二级缓存的出现是为了解决跨Session问题的。因为一级缓存的存在可能出现脏读
比如:
@Test public void testShouldSelectClassUsingMapperClass(){ try( SqlSession session1 = sqlMapper.openSession(); SqlSession session2 = sqlMapper.openSession(); ){ ClassMapper mapper1 = session1.getMapper(ClassMapper.class); ClassMapper mapper2 = session2.getMapper(ClassMapper.class); long start = System.currentTimeMillis(); System.out.println("session1开始查询"); List<ClassDo> classDos = mapper1.listClassOwnerStudentByClassId(1, new String[]{"狗蛋"}); System.out.println(classDos); System.out.println("session2开始更新"); System.out.println(mapper2.updateClassNameById(1, "计科一班1")); System.out.println("session1开始查询"); List<ClassDo> classDos1 = mapper1.listClassOwnerStudentByClassId(1, new String[]{"狗蛋"}); System.out.println(classDos1); System.out.println("start:" + (System.currentTimeMillis()-start)); session1.commit(); session2.commit(); } } 复制代码
结果:
上面的代码,开了两个Session,没有利用二级缓存,只是一级缓存,可以看到,起初是session1查了数据库,名字为
计科一班
,之后session2更新数据库,名字为计科一班1,之后session1再次查询,获取到的还是session计科一班
,出现了数据一致性的问题,多个session之前的缓存不能共享,所以,二级缓存就出现了
二级缓存的范围是namespace
。
好了,简介就先到这里把。看代码吧
2. 一级缓存
如果看过我之前的文章的话Mybatis中获取获取代理对象的之后的执行操作,对整个的流程有一个大体的了解,那么肯定看过这样的代码
// BaseExecutor的query方法,
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
//如果Mapper中配置了flushCacheRequired为true的话,就会清空本地缓存。每次查询都会清空
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// lcnote 从一级缓存中查找,如果没有就会从数据库里面查找,记住这个localCache
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 缓存中没有查找到,就要去数据库查找了。
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {//lcnote 这个deferredLoads是干嘛的,
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
// 下面的这个会判断localScope的作用域,如果为STATEMENT,就会清楚缓存。
//有一个小问题?清楚缓存,这肯定是线程不安全的。有问题,如果两个线程同时操作,
//不需要清楚缓存 一个需要清楚缓存,那不就得么的了。出现问题了?
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
//
clearLocalCache();
}
}
return list;
}
// BaseExecutor的queryFromDatabase方法,
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
// 先上来放一个占位符号。key是上面构建的cacheKey,关于CacheKey的构建在上一篇中已经说了。
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
//做查询,
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
//这里的remove我不是很理解,与其说是占位,name在这里直接put覆盖就好了,这里这样做怕是为了防止doQuery发生异常,导致这个缓存是错误的,所以, 这里必须有finally,这都是细节细节。
localCache.removeObject(key);
}
localCache.putObject(key, list);//放在一级缓存中。
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
复制代码
这里的逻辑很简单,和大多的缓存使用一样,先从缓存中查找,缓存中有,就不去数据库查询,如果没有就从数据库查询,之后填充。
问题?
防止占位符有什么意义(EXECUTION_PLACEHOLDER)?
说实话,这里的占位符我觉得没有什么意义,在之后的查看里面也没有这样的判断,会不会存在这样的一种情况?
同样的方法,同一个SqlSession,如果查询查询在两个线程里面,先后有一点点的差距,就正好后面的线程拿到了这个占位符,在后序的代码里面好像也没有看到相关的处理。所以,后面的这个线程就拿到占位符了。
为什么要在finally里面要移除之前缓存的key?
因为前面已经放置了占位符了,如果在try里面报错了,那么就可能导致后面的代码不会走下去,必须用finally来保证肯定能移除一个。
会不会有线程安全问题?
会。
先从数据结构开始。
PerpetualCache
是缓存的具体实现,实际的数据结构是HashMap。HashMap本就不是线程安全的。在从缓存的操作开始,缓存的清楚和更新都没有任何的同步机制。
不过话说回来,在使用SqlSession的时候,人家已经告知这不是一个线程安全的。!
一级缓存的具体实现类
PerpetualCache
实现了Cache接口。这接口就是Mybatis中缓存的具体表现。具体的实现就是HashMap。
public class PerpetualCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new HashMap<>();
public PerpetualCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public int getSize() {
return cache.size();
}
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return cache.get(key);
}
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}
@Override
public void clear() {
cache.clear();
}
@Override
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());
}
@Override
public int hashCode() {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
return getId().hashCode();
}
}
复制代码
一级缓存什么时候失效?
只要引起数据不一致的场景都会让缓存失效,那就是 insert,delete,update
这三种操作。问题是,清楚缓存的是全部清楚还是针对性的清除?
带着疑问看看源码。
之前文章中介绍过MapperMethod
,这个方法会通过Type来判断这次查询是什么类型?我在这里就不具体分析了,对于INSERT,UPDATE,DELETE
方法最总会走到Update方法里面去。
public Object execute(SqlSession sqlSession, Object[] args) {
Object result; //美其名曰,策略模式
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
复制代码
最终就会走到下面的这个方法
// CachingExecutor类的 update方法,delegate是 BaseExecutor
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
//是否要刷新二级缓存 MapperStatement是有Cache,并且flushCacheRequired为true
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
// BaseExecutor的update方法
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 清楚一级缓存。
clearLocalCache();
return doUpdate(ms, parameter);
}
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
复制代码
问题
缓存什么时候清楚?
在insert,delete,update 和SqlSession#Commit的时候会清楚掉。包括一级缓存和二级缓存。
清楚二级缓存的要求是 MapperStatement有Cache,并且flushCacheRequired为true。就会清楚二级缓存。一级缓存每次都清楚。
缓存是全部清楚还是针对性的清楚?
全部清楚。要注意,这个会将一级缓存全部清楚掉
这样的缓存有问题吗?
有,我觉得有问题。清楚会将整个LocalCache全部清楚,那在极端情况下,缓存就会失效。如果一个查询,一个更新,一个查询,一个删除,一个查询,一个更新,这种密集的情况下。缓存就没有了。
这就意味着,只要sqlSession没有关闭并且没有commit,也没有更新,删除,插入的话,只有查找。那一级缓存里面的数据就不会清楚掉。
3. 二级缓存
二级缓存是在NameSpace级别的。二级缓存开启有两个条件
开启二级缓存的两个条件
-
setting
标签配置,这个就算不配,默认也是也是true<setting name="cacheEnabled" value="true"/> // 在解析Xml标签的时候,如果不配置,默认就是true configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true)); 复制代码
-
在Mapper的Xml中写
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/> 复制代码
只有在NameSpace下面配置了缓存的,并且全局的开启了,才能启动二级缓存
cacheEnabled为true会有什么操作
如果设置为true,在openSession的时候,会将创建好的Executor
用CachingExecutor
包装。
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
//如果开启了缓存就用CachingExecutor来包装之前搞好的Executor,
// CachingExecutor里面有一个TransactionalCacheManager,这是mybatis实现二级缓存的重点。
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
//在interceptorChain应用Executor。
//waring 这里也很重要,这是是实现mybatis拦截器的主要实现。主要看这个方法,Interceptor类里面的wrap方法
// 其实这里就是生成了代理对象,调用 Plugin.wrap(target, this);来生成代理对象,在这里会解析interceptor的上面的注解,组成map,key是接口的class。
// value是set,里面存放的需要拦截的方法。如果需要生成代理对象就生成代理对象,InvocationHandler是Plugin。 具体的还得看看方法里面是怎么写的。
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
复制代码
默认的的执行器的类型是ExecutorType.SIMPLE
,上面的代码已经很明确了,先是根据执行器的类型来创建执行器。如果cacheEnabled
为true,就会用CachingExecutor
来包装。这用到了代理的设计模式。这是静态代理还是动态代理?
问题?
用到了什么设计模式
代理的设计模式
静态代理还是动态代理
静态代理。
CachingExecutor长什么样子
public class CachingExecutor implements Executor {
private final Executor delegate;
// 二级缓存,一级缓存在 BaseExecutor中的localCache
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
public CachingExecutor(Executor delegate) {
this.delegate = delegate;
delegate.setExecutorWrapper(this);
}
@Override
public void close(boolean forceRollback) {
try {
// issues #499, #524 and #573
if (forceRollback) {
tcm.rollback();
} else {
tcm.commit();
}
} finally {
delegate.close(forceRollback);
}
}
@Override
public boolean isClosed() {
return delegate.isClosed();
}
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
@Override
public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
flushCacheIfRequired(ms);
return delegate.queryCursor(ms, parameter, rowBounds);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);//waring 这里很重要,会解析sql,首先会调用所有的sqlNode节点,然后会替换掉,把之前的参数变为?。然后生成映射关系。对于Collection会生成一个特殊的参数。
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);//waring 这里构建cacheKey的目的是什么?这里会判断参数,更新cacheKey。并且每一个update,都会计算hashcode方法
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);//二级缓存实在sqlSessionFactory中的。默认是不开启的,这里肯定是将二级缓存放在Configuration中。
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 这个缓存是在mapper.xml中配置的。在解析xml文件的时候配置的。
Cache cache = ms.getCache(); // lcnote 这里的cache是干嘛的?
if (cache != null) {
flushCacheIfRequired(ms);
// MapperStatement如果要配置缓存,并且resultHandler为null。就去查二级缓存。
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);//lcnote 这个没看懂
//
@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;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
private void flushCacheIfRequired(MappedStatement ms) {
// ms中配置了cache,并且Mapper标签配置的flushCacheRequired是true。表示是需要清楚缓存
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
//中间还省略了一大段。
}
复制代码
可以看到CachingExecutor
实现了Executor
接口,也就意味着,他拥有Executor
的所有的功能,可以在一些特定的接口上应用自己的处理逻辑,之后将处理下放到被代理对象。交给被代理对象来处理。
这就是二级缓存实现地方,在CachingExecutor
中TransactionalCacheManager
对象就是二级缓存实现的重点。
NameSpace中Cache是怎么解析的?对应的实体类是什么?会和MapperStatement关联吗?
一切的开始还得从解析XML文件开始。这里只是展示这其中几个步骤。如果不清楚解析操作的,可以看看Mybatis中mapper是怎么和XMl关联起来的。
//XMLMapperBuilder#configurationElement
// 解析 mapper 标签。这每一个都是一个标签属性,这里要关注cacheElement方法,看他是怎么解析cache标签的。
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));//解析各个标签元素
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));//waring 这里很重要,真正的开始解析select|insert|update|delete标签
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
复制代码
cacheElement,解析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);
}
}
复制代码
这里就能得到一些信息
信息
- 默认的缓存的类型是PERPETUAL
- 默认的淘汰算法是LRU
- 默认是readOnly和blocking是False
- 默认的flushInterval和size是Null
问题?
注意到这些值都是别名,那么这些别名是在什么时候注册进去的?
// 在创建Configuration的时候 public Configuration() { //省略一部分 typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class); typeAliasRegistry.registerAlias("FIFO", FifoCache.class); typeAliasRegistry.registerAlias("LRU", LruCache.class); typeAliasRegistry.registerAlias("SOFT", SoftCache.class); typeAliasRegistry.registerAlias("WEAK", WeakCache.class); //省略一部分 } 复制代码
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props)干了什么事情?
// 这里的代码看名字就大致知道是干嘛的?创建出新的Cache。
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
// cache的id是当前的nameSpace(就是mapper接口上的nameSpace)
Cache cache = new CacheBuilder(currentNamespace)
// 属性,默认的原始的缓存的实现就是PerpetualCache,在上面的判断之外,在多一次判断
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
// 增加装饰器,再次证明,默认的是LruCache
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
// 下面的都是设置属性了
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
// 这个就是在Cache下面设置的属性。
.properties(props)
.build();
// 将构建好的Cache放在MapperStatement中去。作为一个属性放在里面
configuration.addCache(cache);
currentCache = cache;
return cache;
}
复制代码
TransactionalCacheManager 中是什么?TransactionalCache
是什么
从下面的代码中可以看到,TransactionalCacheManager里面维护了一个HashMap,HashMap的的key是Cache,Value是TransactionalCache
,并且TransactionalCache也是实现了一个Cache的接口。同时,他也是一个装饰的Cache,将真正的操作交给delete来完成。
/**
* @author Clinton Begin
*/
public class TransactionalCacheManager {
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
public void rollback() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.rollback();
}
}
private TransactionalCache getTransactionalCache(Cache cache) {
return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);
}
}
//TransactionalCache
/**<p>二级缓存的buffer,在调用commit或者rollback的时候会将这个缓存添加进去。</p>
* The 2nd level cache transactional buffer.
* <p>
* This class holds all cache entries that are to be added to the 2nd level cache during a Session.
* Entries are sent to the cache when commit is called or discarded if the Session is rolled back.
* Blocking cache support has been added. Therefore any get() that returns a cache miss
* will be followed by a put() so any lock associated with the key can be released.
*
* @author Clinton Begin
* @author Eduardo Macarron
*/
public class TransactionalCache implements Cache {
private static final Log log = LogFactory.getLog(TransactionalCache.class);
//代理
private final Cache delegate;
// 标志位置
private boolean clearOnCommit;
//存放的是put时候的值,在commit之后,才会完全的缓存起来,,具体的可以看commit方法
private final Map<Object, Object> entriesToAddOnCommit;
// 存放的是缓存中没有值的key。在commit的时候,判断,如果在entriesToAddOnCommit里面没有找到这个key就放置一个null。具体的可以看commit方法
private final Set<Object> entriesMissedInCache;
public TransactionalCache(Cache delegate) {
this.delegate = delegate;
this.clearOnCommit = false;
this.entriesToAddOnCommit = new HashMap<>();
this.entriesMissedInCache = new HashSet<>();
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
return delegate.getSize();
}
@Override
public Object getObject(Object key) {
// issue #116
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
// issue #146
if (clearOnCommit) {
return null;
} else {
return object;
}
}
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
@Override
public Object removeObject(Object key) {
return null;
}
@Override
public void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear();
}
//这里的逻辑也是很简答, 放值,清空
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
// 在rollback的时候会将entriesMissedInCache里面的值从缓存里面移除。并且重置entriesToAddOnCommit和entriesMissedInCache
public void rollback() {
unlockMissedEntries();
reset();
}
private void reset() {
clearOnCommit = false;
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
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);
}
}
}
private void unlockMissedEntries() {
for (Object entry : entriesMissedInCache) {
try {
delegate.removeObject(entry);
} catch (Exception e) {
log.warn("Unexpected exception while notifying a rollback to the cache adapter. "
+ "Consider upgrading your cache adapter to the latest version. Cause: " + e);
}
}
}
}
复制代码
重中之重来了 CacheBuilder
的build方法干了什么事情?
// CacheBuilder的build方法
public Cache build() {
//还是设置属性,再次判断一下,如果没有设置并且没有指定的话,默认的实现就是PerpetualCache,并且装饰就是LruCache。
setDefaultImplementations();
// 通过cache的构造方法创建实例
Cache cache = newBaseCacheInstance(implementation, id);
// 设置属性
setCacheProperties(cache);
// issue #352, do not apply decorators to custom caches
// 如果cache不是PerpetualCache,那就不用装饰器了
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;
}
// 设置标准的装饰器
private Cache setStandardDecorators(Cache cache) {
try {
//MetaObject这个方法底层就是通过反射拿到所有的方法和字段。
MetaObject metaCache = SystemMetaObject.forObject(cache);
//如果有size,就设置size字段
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
//如果设置了clearInterval,就将上一步的cache用ScheduledCache包装一下
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);
}
}
复制代码
所以,到这里就很明确了。setting
中配置了cacheEnabled
为true,就会创建CachingExecutor在之前的执行器上面进行代理。CachingExecutor就是二级缓存实现的关键。在查询的时候,会先从二级缓存中查找,在从一级缓存中查找,在从数据库查找。
如果mapper的xml中的NameSpace中配置了cache
标签,就会在CachingExecutor的TransactionalCacheManager
中保存。在commit的时候才会将缓存真正的添加进来,并且清楚entriesToAddOnCommit
和entriesMissedInCache
和重置标记位。在rollback的时候,会从真正的缓存里面将entriesToAddOnCommit
里面的移除掉。并且reset。
问题?
真正的存储元素的缓存是什么?上面看到的有的是包装类,有的是原始类。这种怎么分别?
这就说到
Cache
了,Cache
接口要求实现这个接口的类都应该有一个String类型的构造参数,用于接受NameSpace。
在看看实现类。
听这缓存的名字就应该知道个大概,perpetualCache(永久缓存,持久缓存),缓存的实现百分之九十九都是Map结构,这肯定也不例外,他用的是HashMap。
cache标签里面配置的属性具体是什么意思?有哪些作用?
type: 缓存的具体实现类,(这指的是真正的缓存的实现,有一个构造方法为String的实体类)
eviction:(采用的包装类的类型)
flushInterval:(刷新间隔)
size:(大小)
readOnly:(是否只读)
blocking:(是否阻塞)
二级缓存的整个的 查询流程
调用query
方法,做查询。先获取MapperStatement中的Cache。判断当前Cache是否要刷新(cache不是null并且flushCacheRequired为true)
private void flushCacheIfRequired(MappedStatement ms) {
// ms中配置了cache,并且Mapper标签配置的flushCacheRequired是true。表示是需要清楚缓存
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
复制代码
就会通过tcm来清楚这个缓存对应的TransactionalCache
,在会从二级缓存中获取值
List<E> list = (List<E>) tcm.getObject(cache, key);
//下面的是TransactionalCacheManager的方法
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
// getTransactionalCache,
// 从transactionalCaches中获取这个Cache对应的TransactionalCache,如果没有就创建一个。
private TransactionalCache getTransactionalCache(Cache cache) {
return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);
}
复制代码
如果没有,就会交给delegate
来执行查询。查询结束之后,将值放在二级缓存中,在commit或者当前的SqlSession关闭的时候将数据真正的缓存下来,虽然下面列举的代码是CachingExecutor,在关闭SqlSession的时候会调用过来。
//CachingExecutor
@Override
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();
}
@Override
public void close(boolean forceRollback) {
try {
// issues #499, #524 and #573
if (forceRollback) {
tcm.rollback();
} else {
tcm.commit();
}
} finally {
delegate.close(forceRollback);
}
}
复制代码
关于缓存的具体实现,在下一篇中再说吧
关于Mybatis 缓存的就分析到这里了。 如有不正确的地方,欢迎指出。谢谢。