MyBatis从入门到精通—源码剖析之二级缓存细节

网友投稿 566 2022-10-30

MyBatis从入门到精通—源码剖析之二级缓存细节

MyBatis从入门到精通—源码剖析之二级缓存细节

⼆级缓存构建在⼀级缓存之上,在收到查询请求时,MyBatis ⾸先会查询⼆级缓存,若⼆级缓存未命中,再去查询⼀级缓存,⼀级缓存没有,再查询数据库。⼆级缓存------》 ⼀级缓存------》数据库与⼀级缓存不同,⼆级缓存和具体的命名空间绑定,⼀个Mapper中有⼀个Cache,相同Mapper中的MappedStatement共⽤⼀个Cache,⼀级缓存则是和 SqlSession 绑定。

启用二级缓存

分为三步⾛:

开启全局⼆级缓存配置: 在需要使⽤⼆级缓存的Mapper配置⽂件中配置标签 在具体CURD标签上配置 useCache=true 标签 < cache/> 的解析 根据之前的mybatis源码剖析,xml的解析⼯作主要交给XMLConfigBuilder.parse()⽅法来实现 public class XMLConfigBuilder extends BaseBuilder { public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; parseConfiguration(parser.evalNode("/configuration")); return configuration; } // 既然是在xml中添加的,那么我们就直接看关于mappers标签的解析 private void parseConfiguration(XNode root) { try { //issue #117 read properties first propertiesElement(root.evalNode("properties")); Properties settings = settingsAsProperties(root.evalNode("settings")); loadCustomVfs(settings); typeAliasesElement(root.evalNode("typeAliases")); pluginElement(root.evalNode("plugins")); objectFactoryElement(root.evalNode("objectFactory")); objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); reflectorFactoryElement(root.evalNode("reflectorFactory")); settingsElement(settings); // read it after objectFactory and objectWrapperFactory issue #631 environmentsElement(root.evalNode("environments")); databaseIdProviderElement(root.evalNode("databaseIdProvider")); typeHandlerElement(root.evalNode("typeHandlers")); // 这⾥这里,继续进入这个方法跟进去看看 mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } } private void mapperElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { if ("package".equals(child.getName())) { String mapperPackage = child.getStringAttribute("name"); configuration.addMappers(mapperPackage); } else { String resource = child.getStringAttribute("resource"); String url = child.getStringAttribute("url"); String mapperClass = child.getStringAttribute("class"); // 按照我们本例的配置,则直接⾛该if判断 if (resource != null && url == null && mapperClass == null) { ErrorContext.instance().resource(resource); InputStream inputStream = Resources.getResourceAsStream(resource); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); //进入XMLMapperBuilder实例,并执行parse解析方法 mapperParser.parse(); } else if (resource == null && url != null && mapperClass == null) { ErrorContext.instance().resource(url); InputStream inputStream = Resources.getUrlAsStream(url); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); mapperParser.parse(); } else if (resource == null && url == null && mapperClass != null) { Class mapperInterface = Resources.classForName(mapperClass); configuration.addMapper(mapperInterface); } else { throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); } } } } } } follow me,继续看看解析Mapper.xml public class XMLMapperBuilder extends BaseBuilder { public void parse() { if (!configuration.isResourceLoaded(resource)) { // 进入解析mapper属性 configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); bindMapperForNamespace(); } parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); } private void configurationElement(XNode context) { try { String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.equals("")) { throw new BuilderException("Mapper's namespace cannot be empty"); } builderAssistant.setCurrentNamespace(namespace); cacheRefElement(context.evalNode("cache-ref")); // 最终在这⾥看到了关于cache属性的处理,跟进去看看 cacheElement(context.evalNode("cache")); parameterMapElement(context.evalNodes("/mapper/parameterMap")); resultMapElements(context.evalNodes("/mapper/resultMap")); sqlElement(context.evalNodes("/mapper/sql")); // 这⾥会将⽣成的Cache包装到对应的MappedStatement buildStatementFromContext(context.evalNodes("select|insert|update|delete")); } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e); } } private void cacheElement(XNode context) throws Exception { if (context != null) { //解析标签的type属性,这⾥我们可以⾃定义cache的实现类,⽐如redisCache,如果没有⾃定义,这⾥使⽤和⼀级缓存相同的PERPETUAL String type = context.getStringAttribute("type", "PERPETUAL"); Class typeClass = typeAliasRegistry.resolveAlias(type); String eviction = context.getStringAttribute("eviction", "LRU"); Class 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(); // 构建Cache对象 builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props); } } } 让我们继续来看看是如何构建Cache对象的 MapperBuilderAssistant.useNewCache() public class MapperBuilderAssistant extends BaseBuilder { // 1.⽣成Cache对象 public Cache useNewCache(Class typeClass, Class evictionClass, Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) { Cache cache = new CacheBuilder(currentNamespace) //这⾥如果我们定义了中的type,就使⽤⾃定义的Cache,否则使⽤和⼀级缓存相同的PerpetualCache .implementation(valueOrDefault(typeClass, PerpetualCache.class)) .addDecorator(valueOrDefault(evictionClass, LruCache.class)) .clearInterval(flushInterval) .size(size) .readWrite(readWrite) .blocking(blocking) .properties(props) .build(); // 2.添加到Configuration中 configuration.addCache(cache); // 3.并将cache赋值给MapperBuilderAssistant.currentCache currentCache = cache; return cache; } } 我们看到⼀个Mapper.xml只会解析⼀次标签,也就是只创建⼀次Cache对象,放进configuration中,并将cache赋值给MapperBuilderAssistant.currentCache。XMLMapperBuilder的configurationElement(XNode context) 方法中的buildStatementFromContext(context.evalNodes("select|insert|update|delete"));将Cache包装到MappedStatement。 public class XMLMapperBuilder extends BaseBuilder { private void buildStatementFromContext(List list) { if (configuration.getDatabaseId() != null) { buildStatementFromContext(list, configuration.getDatabaseId()); } //调用重载方法 buildStatementFromContext(list, null); } private void buildStatementFromContext(List list, String requiredDatabaseId) { for (XNode context : list) { final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); try { // 每⼀条执⾏语句转换成⼀个MappedStatement statementParser.parseStatementNode(); } catch (IncompleteElementException e) { configuration.addIncompleteStatement(statementParser); } } } } XMLStatementBuilder.parseStatementNode() public class XMLStatementBuilder extends BaseBuilder { public void parseStatementNode() { String id = context.getStringAttribute("id"); String databaseId = context.getStringAttribute("databaseId"); if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) { return; } Integer fetchSize = context.getIntAttribute("fetchSize"); Integer timeout = context.getIntAttribute("timeout"); String parameterMap = context.getStringAttribute("parameterMap"); String parameterType = context.getStringAttribute("parameterType"); Class parameterTypeClass = resolveClass(parameterType); String resultMap = context.getStringAttribute("resultMap"); String resultType = context.getStringAttribute("resultType"); String lang = context.getStringAttribute("lang"); LanguageDriver langDriver = getLanguageDriver(lang); Class resultTypeClass = resolveClass(resultType); String resultSetType = context.getStringAttribute("resultSetType"); StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType); String nodeName = context.getNode().getNodeName(); SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH)); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); boolean useCache = context.getBooleanAttribute("useCache", isSelect); boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false); // Include Fragments before parsing XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); includeParser.applyIncludes(context.getNode()); // Parse selectKey after includes and remove them. processSelectKeyNodes(id, parameterTypeClass, langDriver); // Parse the SQL (pre: and were parsed and removed) SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); String resultSets = context.getStringAttribute("resultSets"); String keyProperty = context.getStringAttribute("keyProperty"); String keyColumn = context.getStringAttribute("keyColumn"); KeyGenerator keyGenerator; String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX; keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true); if (configuration.hasKeyGenerator(keyStatementId)) { keyGenerator = configuration.getKeyGenerator(keyStatementId); } else { keyGenerator = context.getBooleanAttribute("useGeneratedKeys", configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; } //创建MappedStatement对象 builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); } } BuilderAssistant.addMappedStatement() public MappedStatement addMappedStatement( String id, SqlSource sqlSource, StatementType statementType, SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class parameterType, String resultMap, Class resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache, boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId, LanguageDriver lang, String resultSets) { if (unresolvedCacheRef) { throw new IncompleteElementException("Cache-ref not yet resolved"); } id = applyCurrentNamespace(id, false); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; //创建MappedStatement对象 MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType) .resource(resource) .fetchSize(fetchSize) .timeout(timeout) .statementType(statementType) .keyGenerator(keyGenerator) .keyProperty(keyProperty) .keyColumn(keyColumn) .databaseId(databaseId) .lang(lang) .resultOrdered(resultOrdered) .resultSets(resultSets) .resultMaps(getStatementResultMaps(resultMap, resultType, id)) .resultSetType(resultSetType) .flushCacheRequired(valueOrDefault(flushCache, !isSelect)) .useCache(valueOrDefault(useCache, isSelect)) //在这⾥将之前⽣成的Cache封装到MappedStatement .cache(currentCache); ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id); if (statementParameterMap != null) { statementBuilder.parameterMap(statementParameterMap); } MappedStatement statement = statementBuilder.build(); configuration.addMappedStatement(statement); return statement; } 我们看到将Mapper中创建的Cache对象,加⼊到了每个MappedStatement对象中,也就是同⼀个Mapper中所有的MappedStatement实例,有关于标签的解析就到这。 查询源码分析 CachingExecutor @Override public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameterObject); //创建 CacheKey CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); //调用重载方法 return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } @Override public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { // 从 MappedStatement 中获取 Cache,注意这⾥的 Cache 是从MappedStatement中获取的 // 也就是我们上⾯解析Mapper中标签中创建的,它保存在Configration中 // 我们在上⾯解析mapper.xml时分析过每⼀个MappedStatement都有⼀个Cache对象,就是这⾥ Cache cache = ms.getCache(); // 如果配置⽂件中没有配置 ,则 cache 为空 if (cache != null) { //如果需要刷新缓存的话就刷新:flushCache="true" flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, boundSql); // 访问⼆级缓存 @SuppressWarnings("unchecked") List list = (List) tcm.getObject(cache, key); // 缓存未命中 if (list == null) { // 如果没有值,则执⾏查询,这个查询实际也是先⾛⼀级缓存查询,⼀级缓存也没有的话,则进⾏DB查询 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); } 如果设置了flushCache="true",则每次查询都会刷新缓存 如上,注意⼆级缓存是从 MappedStatement 中获取的。由于 MappedStatement 存在于全局配置中,可以多个 CachingExecutor 获取到,这样就会出现线程安全问题。除此之外,若不加以控制,多个事务共⽤⼀个缓存实例,会导致脏读问题。⾄于脏读问题,需要借助其他类来处理,也就是上⾯代码中tcm 变量对应的类型。下⾯分析⼀下。 TransactionalCacheManager /** 事务缓存管理器 */ public class TransactionalCacheManager { // Cache 与 TransactionalCache 的映射关系表 private final Map transactionalCaches = new HashMap(); public void clear(Cache cache) { // 获取 TransactionalCache 对象,并调⽤该对象的 clear ⽅法,下同 getTransactionalCache(cache).clear(); } public Object getObject(Cache cache, CacheKey key) { // 直接从TransactionalCache中获取缓存 return getTransactionalCache(cache).getObject(key); } public void putObject(Cache cache, CacheKey key, Object value) { // 直接存⼊TransactionalCache的缓存中 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) { // 从映射表中获取 TransactionalCache TransactionalCache txCache = transactionalCaches.get(cache); if (txCache == null) { // TransactionalCache 也是⼀种装饰类,为 Cache 增加事务功能 // 创建⼀个新的TransactionalCache,并将真正的Cache对象存进去 txCache = new TransactionalCache(cache); transactionalCaches.put(cache, txCache); } return txCache; }

}

TransactionalCacheManager 内部维护了 Cache 实例与 TransactionalCache 实例间的映射关系,该类也仅负责维护两者的映射关系,真正做事的还是 TransactionalCache。TransactionalCache 是⼀种缓存装饰器,可以为Cache 实例增加事务功能。我在之前提到的脏读问题正是由该类进⾏处理的。下⾯分析⼀下该类的逻辑。 ### TransactionalCache ```java public class TransactionalCache implements Cache { private static final Log log = LogFactory.getLog(TransactionalCache.class); //真正的缓存对象,和上⾯的Map中的Cache是同⼀个 private final Cache delegate; private boolean clearOnCommit; // 在事务被提交前,所有从数据库中查询的结果将缓存在此集合中 private final Map entriesToAddOnCommit; // 在事务被提交前,当缓存未命中时,CacheKey 将会被存储在此集合中 private final Set 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 // 查询的时候是直接从delegate中去查询的,也就是从真正的缓存对象中查询 Object object = delegate.getObject(key); if (object == null) { // 缓存未命中,则将 key 存⼊到 entriesMissedInCache 中 entriesMissedInCache.add(key); } // issue #146 if (clearOnCommit) { return null; } else { return object; } } @Override public ReadWriteLock getReadWriteLock() { return null; } @Override public void putObject(Object key, Object object) { // 将键值对存⼊到 entriesToAddOnCommit 这个Map中中,⽽⾮真实的缓存对象delegate 中 entriesToAddOnCommit.put(key, object); } @Override public Object removeObject(Object key) { return null; } @Override public void clear() { clearOnCommit = true; // 清空 entriesToAddOnCommit,但不清空 delegate 缓存 entriesToAddOnCommit.clear(); } public void commit() { // 根据 clearOnCommit 的值决定是否清空 delegate if (clearOnCommit) { delegate.clear(); } // 刷新未缓存的结果到 delegate 缓存中 flushPendingEntries(); // 重置 entriesToAddOnCommit 和 entriesMissedInCache reset(); } public void rollback() { unlockMissedEntries(); reset(); } private void reset() { clearOnCommit = false; // 清空集合 entriesToAddOnCommit.clear(); entriesMissedInCache.clear(); } private void flushPendingEntries() { for (Map.Entry entry : entriesToAddOnCommit.entrySet()) { // 将 entriesToAddOnCommit 中的内容转存到 delegate 中 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 { // 调⽤ removeObject 进⾏解锁 delegate.removeObject(entry); } catch (Exception e) { log.warn("Unexpected exception while notifiying a rollback to the cache adapter." + "Consider upgrading your cache adapter to the latest version. Cause: " + e); } } } }

存储⼆级缓存对象的时候是放到了TransactionalCache.entriesToAddOnCommit这个map中,但是每次查询的时候是直接从TransactionalCache.delegate中去查询的,所以这个⼆级缓存查询数据库后,设置缓存值是没有⽴刻⽣效的,主要是因为直接存到 delegate 会导致脏数据问题。SqlSession.commit()⽅法做了什么

SqlSession.commit()

public class DefaultSqlSession implements SqlSession { @Override public void commit(boolean force) { try { // 主要是这句 executor.commit(isCommitOrRollbackRequired(force)); dirty = false; } catch (Exception e) { throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } } } // CachingExecutor.commit() @Override public void commit(boolean required) throws SQLException { delegate.commit(required); //调用TransactionalCacheManager.commit() tcm.commit(); } // TransactionalCacheManager.commit() public void commit() { for (TransactionalCache txCache : transactionalCaches.values()) { //调用TransactionalCache.commit() txCache.commit(); } } // TransactionalCache.commit() public void commit() { if (clearOnCommit) { delegate.clear(); } flushPendingEntries();//这⼀句 reset(); } // TransactionalCache.flushPendingEntries() private void flushPendingEntries() { for (Map.Entry entry : entriesToAddOnCommit.entrySet()) { // 在这⾥真正的将entriesToAddOnCommit的对象逐个添加到delegate中,只有这时,⼆级缓存才真正的⽣效 delegate.putObject(entry.getKey(), entry.getValue()); } for (Object entry : entriesMissedInCache) { if (!entriesToAddOnCommit.containsKey(entry)) { delegate.putObject(entry, null); } } }

⼆级缓存的刷新

我们来看看SqlSession的更新操作

//DefaultSqlSession.update public int update(String statement, Object parameter) { int var4; try { this.dirty = true; MappedStatement ms = this.configuration.getMappedStatement(statement); var4 = this.executor.update(ms, this.wrapCollection(parameter)); } catch (Exception var8) { throw ExceptionFactory.wrapException("Error updating database. Cause:" + var8, var8); } finally { ErrorContext.instance().reset(); } return var4; } } //CachingExecutor.update public int update(MappedStatement ms, Object parameterObject) throws SQLException { this.flushCacheIfRequired(ms); return this.delegate.update(ms, parameterObject); } private void flushCacheIfRequired(MappedStatement ms) { //获取MappedStatement对应的Cache,进⾏清空 Cache cache = ms.getCache(); //SQL需设置flushCache="true" 才会执⾏清空 if (cache != null && ms.isFlushCacheRequired()) { this.tcm.clear(cache); } }

MyBatis⼆级缓存只适⽤于不常进⾏增、删、改的数据,⽐如国家⾏政区省市区街道数据。⼀但数据变更,MyBatis会清空缓存。因此⼆级缓存不适⽤于经常进⾏更新的数据。总结:

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Spring Boot如何利用拦截器加缓存完成接口防刷操作
下一篇:iOS知识小集
相关文章

 发表评论

暂时没有评论,来抢沙发吧~