Mybatis-3 源码之缓存是怎么创建和使用的

微信扫一扫,分享到朋友圈

Mybatis-3 源码之缓存是怎么创建和使用的

Mybatis-3 源码之缓存是怎么创建的

Mybatis 缓存问题其实也是面试高频的问题了,今天我们就从源码级别来谈谈 Mybatis 的缓存实现。

(本文源码均在 https://github.com/ccqctljx/Mybatis-3 中,会持续更新注释和 Demo)。

首先我们了解一下缓存是什么:缓存是一般的 ORM 框架都会提供的功能,目的就是提升查询的效率和减少数据库的压力。直白一点就是,开了缓存后,同样的数据查询不必再次访问数据库,直接从缓存中拿即可。

那么面试官常问的 一级缓存 和 二级缓存 又都是什么呢?

一级缓存:一级缓存又称本地缓存,是在会话(SqlSession)层面进行的缓存。随会话开始而生,结束而死。MyBatis 的一级缓存是默认开启的,不需要任何的配置。

二级缓存:由于一级缓存随会话而生,就不能跨会话共享。二级缓存则是用来解决这个问题的,他的范围是 namespace 级别的,可以被多个SqlSession 共享,生命周期和 SqlSessionFactory 同步。只要是同一个 SqlSessionFactory 创建出来的会话,即可共享相同 namespace 级别的缓存。二级缓存需要配置三个地方:

第一个是在 mybaits-config.xml 配置文件中设置开启缓存:“ <setting name="cacheEnabled" value="true"/>

第二个是要在 Mapper 文件中配置 “ <cache/> “标签

第三个是在需要使用缓存的语句上加入 “ useCache="true"

那么一级二级缓存有没有执行顺序什么的呢?答案是有的,如果开启二级缓存那么执行顺序为:

那么我们写个实例代码,来看下一二级缓存的效果吧

public class Demo {

public static void main (String[] args) throws IOException {

String resource = “mybatis/mybatis-config.xml” ;

InputStream inputStream = Resources.getResourceAsStream(resource);

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

SqlSession sqlSession1 = sqlSessionFactory.openSession();

SqlSession sqlSession2 = sqlSessionFactory.openSession();

List<BookInfo> bookInfoList1 = sqlSession1.selectList( “com.simon.demo.TestMapper.selectBookInfo” );

System.out.println( ” sqlSession 1 query 1 —————————– “ + bookInfoList1);

List<BookInfo> bookInfoList2 = sqlSession1.selectList( “com.simon.demo.TestMapper.selectBookInfo” );

System.out.println( “sqlSession 1 query 2 —————————–“ + bookInfoList2);

sqlSession1.commit();

System.out.println( “sqlSession 1 commit —————————–“ );

List<BookInfo> bookInfoList3 = sqlSession2.selectList( “com.simon.demo.TestMapper.selectBookInfo” );

System.out.println( “sqlSession 2 query 1 —————————– “ + bookInfoList3);

}

}

复制代码

打印结果是:

由此我们能看到,只有第一次查询执行了 sql,其余两次查询均未去数据库中查询。这就是缓存的效用啦。

我们接下来去到源码来看一下究竟是如何生效的吧。

二级缓存创建过程一:加载配置类

首先,我们创建 SqlSessionFactory 工厂时,会从配置文件中加载所有的配置并生成 Configuration 对象,然后将 Configuration 对象放在 SqlSessionFactory 实例对象中维护起来。解析代码如下

package org.apache.ibatis.builder.xml;

public class XMLConfigBuilder extends BaseBuilder {

……

private void parseConfiguration (XNode root) {

try {

//issue #117 read properties first

propertiesElement(root.evalNode( “properties” ));

// 解析配置文件里的 setting 标签

Properties settings = settingsAsProperties(root.evalNode( “settings” ));

loadCustomVfs(settings);

loadCustomLogImpl(settings);

// 生成别名 map 放进 configuration 中后备使用

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” ));

// 解析配置文件里的 mappers 标签

mapperElement(root.evalNode( “mappers” ));

} catch (Exception e) {

throw new BuilderException( “Error parsing SQL Mapper Configuration. Cause: “ + e, e);

}

}

/**

* 把 settings 标签的所有配置加载成 Properties

* @param context

* @return

*/

private Properties settingsAsProperties (XNode context) {

if (context == null ) {

return new Properties();

}

Properties props = context.getChildrenAsProperties();

// Check that all settings are known to the configuration class

MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);

for (Object key : props.keySet()) {

if (!metaConfig.hasSetter(String.valueOf(key))) {

throw new BuilderException( “The setting “ + key + ” is not known. Make sure you spelled it correctly (case sensitive).” );

}

}

return props;

}

/**

* 设置全局上下文属性

*/

private void settingsElement (Properties props) {

……

configuration.setCacheEnabled(booleanValueOf(props.getProperty( “cacheEnabled” ), true ));

configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty( “localCacheScope” , “SESSION” )));

……

}

……

}

复制代码

方法 settingsAsProperties 将配置文件中 setting 标签读为 Properties 对象,然后在 settingsElement 方法中全部赋给 configuration 对象,这其中就有对 cache 标签的处理,将 。这个 Configuration 是 BaseBuilder 中描述全局配置的一个类,后面会将它扔给 SqlSessionFactory ,作为全局上下文。

这里还有个方法比较重要,就是 typeAliasesElement 方法,这个方法是将我们配置好的一些别名类,以键值对的形式存储在 TypeAliasRegistry 类中的一个 HashMap 中,例如 “byte” -> Byte.class。这个 TypeAliasRegistry 也会被放入全局配置 Configuration 中。

二级缓存创建过程二:创建 Cache 对象并绑定 Mapper

解析配置文件后,mybatis 知道自己需要开启二级缓存,于是开始了创建缓存之路,首先,先扫描所有 Mapper 文件位置,然后一个个分析过去(此处以 resource 为例分析):

package org.apache.ibatis.builder.xml;

public class XMLConfigBuilder extends BaseBuilder {

private void mapperElement (XNode parent) throws Exception {

if (parent != null ) {

// 遍历 mybatis-config.xml 文件下面的 mappers 节点的子节点

for (XNode child : parent.getChildren()) {

if ( “package” .equals(child.getName())) {

String mapperPackage = child.getStringAttribute( “name” );

configuration.addMappers(mapperPackage);

} else {

// 如果不是的话,就是 mapper 标签(因为 xml 中只允许写这两种标签)

// 然后拿相应的属性,去分别作解析

String resource = child.getStringAttribute( “resource” );

String url = child.getStringAttribute( “url” );

String mapperClass = child.getStringAttribute( “class” );

// 解析 resource 表明位置的 mapper

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());

// 解析具体的 mapper 文件

mapperParser.parse();

}

// 解析 url 表明位置的 mapper

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();

}

// 解析 mapperClass 表明位置的 mapper

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.” );

}

}

}

}

}

}

复制代码

找到 Mapper 后,开始针对 Mapper 的解析:

package org.apache.ibatis.builder.xml;

public class XMLMapperBuilder extends BaseBuilder {

……

public void parse () {

// 因为是公共方法,多处调用,所以这里先判断有没有加载过

if (!configuration.isResourceLoaded(resource)) {

// 没加载过的话,先去加载资源,这里创建了 Cache 对象

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.isEmpty()) {

throw new BuilderException( “Mapper’s namespace cannot be empty” );

}

builderAssistant.setCurrentNamespace(namespace);

// 这两行是开启二级缓存比较关键的两步

// 这一步拿了别人的 cache 对象 设置给自己了

cacheRefElement(context.evalNode( “cache-ref” ));

// 在这一步中构建了 Cache 对象

cacheElement(context.evalNode( “cache” ));

// 解析参数 Map

parameterMapElement(context.evalNodes( “/mapper/parameterMap” ));

// 解析 resultMap

resultMapElements(context.evalNodes( “/mapper/resultMap” ));

sqlElement(context.evalNodes( “/mapper/sql” ));

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);

}

}

……

}

复制代码

这里我们跟缓存相关的有三步,第一步 cacheRefElement 是看看 mapper 中是否标注了 “ <cache-ref namespace=""/> ` 标签,这个标签的意思是 我可以跟其他 namespace 的 mapper 共用一个 Cache。源码其实就是把 Configuration 中加载好的指定 mapper 的 Cache 对象引用给自己。我们重点看创建 Cache 对象的方法也就是 ` cacheElement(context.evalNode("cache"));

private void cacheElement (XNode context) {

if (context != null ) {

// 如果不指定类型,则默认缓存类型设置为 PERPETUAL

String type = context.getStringAttribute( “type” , “PERPETUAL” );

// 这里指的就是之前加载配置时 typeAliasesElement 方法所做的

Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);

// 代码可以看到,默认是 LRU 即 移除最长时间不被使用的对象。

// 官网文档共设有四种如下:

/**

LRU – Least Recently Used: Removes objects that haven’t been used for the longst period of time.(清除长时间不用的)

FIFO – First In First Out: Removes objects in the order that they entered the cache.(清除最开始放进去的)

SOFT – Soft Reference: Removes objects based on the garbage collector state and the rules of Soft References.(软引用式清除)

WEAK – Weak Reference: More aggressively removes objects based on the garbage collector state and rules of Weak References.(弱引用式清除)

*/

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();

// 此方法构建了一个新的 Cache 对象并设置到了 configuration 中。

builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);

}

public Cache useNewCache (Class<? extends Cache> typeClass,

Class<? extends Cache> evictionClass,

Long flushInterval,

Integer size,

boolean readWrite,

boolean blocking,

Properties props) {

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();

// 构建好Cache后,加入到 configuration 中等待调用。

configuration.addCache(cache);

currentCache = cache;

return cache;

}

复制代码

创建完毕后,这里调用了 “ configuration.addCache(cache) “ 方法将生成好的 cache 放进了 configuration 对象中,实际上就是将 cache 对象 put 进了 Configuration 类内部维护的一个 StrictMap中,而这个 StrictMap 则是继承自 HashMap, 也就是说归根结底这里是将 cache 以 currentNamespace 为Key 放入了一个 HashMap 中。

二级缓存创建过程三:为每个sql语句绑定 cache

在生成 Cache 对象后,Mapper 文件会将本 mapper 中所有的语句标签生成一个个 MappedStatement ,在这个过程中,会给每个 statement 绑定上二级缓存,使得他可以直接使用。

public void parseStatementNode () {

String id = context.getStringAttribute( “id” );

String databaseId = context.getStringAttribute( “databaseId” );

// 如果数据库 id 不为空且匹配不上的话,不进行下面的加载工作

if (!databaseIdMatchesCurrent(id, databaseId, this .requiredDatabaseId)) {

return ;

}

String nodeName = context.getNode().getNodeName();

// 此处拿的是标签,insert | update | delete | select

SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));

// 是否是 select 语句

boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

// 是否清除缓存

boolean flushCache = context.getBooleanAttribute( “flushCache” , !isSelect);

// 是否使用二级缓存

boolean useCache = context.getBooleanAttribute( “useCache” , isSelect);

// 结果是否排序

boolean resultOrdered = context.getBooleanAttribute( “resultOrdered” , false );

······

// 配置一系列属性,标签上的对应属性可以在这里看到

StatementType statementType = StatementType.valueOf(context.getStringAttribute( “statementType” , StatementType.PREPARED.toString()));

Integer fetchSize = context.getIntAttribute( “fetchSize” );

Integer timeout = context.getIntAttribute( “timeout” );

String parameterMap = context.getStringAttribute( “parameterMap” );

String resultType = context.getStringAttribute( “resultType” );

Class<?> resultTypeClass = resolveClass(resultType);

String resultMap = context.getStringAttribute( “resultMap” );

String resultSetType = context.getStringAttribute( “resultSetType” );

ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

if (resultSetTypeEnum == null ) {

resultSetTypeEnum = configuration.getDefaultResultSetType();

}

String keyProperty = context.getStringAttribute( “keyProperty” );

String keyColumn = context.getStringAttribute( “keyColumn” );

String resultSets = context.getStringAttribute( “resultSets” );

// 此处绑定了二级缓存

builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,

fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,

resultSetTypeEnum, flushCache, useCache, resultOrdered,

keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);

}

复制代码

构造 mappedStatement 的过程像构建 Cache 一样又臭又长,此处就不再赘述,感兴趣的小伙伴可以自行去看~

以上就是二级缓存的创建过程。二级缓存如此复杂,那么一级缓存呢?

一级缓存创建过程:

一级缓存的创建过程其实比二级缓存要简单得多,他不用考虑跨会话执行的问题,所以仅仅在创建当前会话(SQLSession)时,新建一个缓存对象即可,也就是代码中的 localCache ,如:

private SqlSession openSessionFromDataSource (ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {

Transaction tx = null ;

try {

final Environment environment = configuration.getEnvironment();

final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);

tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);

// 这里返回的 Executor 每次都是新的

final Executor executor = configuration.newExecutor(tx, execType);

return new DefaultSqlSession(configuration, executor, autoCommit);

} catch (Exception e) {

closeTransaction(tx); // may have fetched a connection so lets call close()

throw ExceptionFactory.wrapException( “Error opening session. Cause: “ + e, e);

} finally {

ErrorContext.instance().reset();

}

}

public Executor newExecutor (Transaction transaction, ExecutorType executorType) {

executorType = executorType == null ? defaultExecutorType : executorType;

executorType = executorType == null ? ExecutorType.SIMPLE : executorType;

Executor executor;

// 注意这里创建的所有类型的 Executor 实际上都继承自 BaseExecutor

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);

}

if (cacheEnabled) {

executor = new CachingExecutor(executor);

}

executor = (Executor) interceptorChain.pluginAll(executor);

return executor;

}

public class SimpleExecutor extends BaseExecutor {

public SimpleExecutor (Configuration configuration, Transaction transaction) {

// 这里执行了父类的构造方法

super (configuration, transaction);

}

······

}

public abstract class BaseExecutor implements Executor {

private static final Log log = LogFactory.getLog(BaseExecutor.class);

protected Transaction transaction;

protected Executor wrapper;

protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;

protected PerpetualCache localCache;

protected PerpetualCache localOutputParameterCache;

protected Configuration configuration;

protected int queryStack;

private boolean closed;

protected BaseExecutor (Configuration configuration, Transaction transaction) {

this .transaction = transaction;

this .deferredLoads = new ConcurrentLinkedQueue<>();

// 这里新建了一个新的缓存

this .localCache = new PerpetualCache( “LocalCache” );

this .localOutputParameterCache = new PerpetualCache( “LocalOutputParameterCache” );

this .closed = false ;

this .configuration = configuration;

this .wrapper = this ;

}

······

}

复制代码

这个 PerpetualCache 是最普通的缓存,内部维护了一个 HashMap 作为缓存承载体。

正如注释所说,每次新开一个会话时,这个 Executor 都会被新建。于是内部维护的缓存自然是每次都更新,也就不存在跨 SQLSession 一说了。

总结一下:

– 一级缓存的创建随着每次 SQLSession 的开启而创建,仅仅是 Executor 中维护的一个 简单缓存对象,内部以 HashMap 做实现。

– 二级缓存的创建过程是先读取 mybatis-config.xml 文件确认缓存开启,然后根据 mapper 文件中的 cache 或 cache-ref 标签来创建缓存对象,以 namespace 为id 放在 Configuration 中,并且在解析 mapper 文件中每个 sql 语句时将 cache 对象绑定上。

上面主要讲述了 mybatis 一、二级缓存的创建过程,重点主要放在了二级缓存的创建过程。那么缓存具体是如何使用的,缓存又在什么时候被清空呢?还请大家跟着我继续往下看

Mybatis-3 源码之缓存是如何使用的

下面呢,则主要讲讲这个缓存对象创建出来后,到底是怎么给他用的。借用前面的图,由于开启二级缓存后,我们查询数据库的执行顺序如下,所以我们按照顺序来一步步深入:

使用缓存第一步:创建 Executor 对象

有过一定源码基础的同学肯定知道,我们 Mybatis 底层执行增删改查操作时,执行对象实际上就是一个个 Executor。那么不例外,我们使用缓存肯定也要在 Executor 上做手脚,那么我们跟随源码来看下 Mybatis 究竟做了什么“手脚”吧:

首先是 “ sqlSessionFactory.openSession() “时调用的 openSessionFromDataSource 方法

private SqlSession openSessionFromDataSource (ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {

Transaction tx = null ;

try {

final Environment environment = configuration.getEnvironment();

final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);

// 每次新建 SQLSession 都新创建一个事务

tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);

// 这里每次新建 SQLSession 时都返回新的 Executor

final Executor executor = configuration.newExecutor(tx, execType);

return new DefaultSqlSession(configuration, executor, autoCommit);

} catch (Exception e) {

closeTransaction(tx); // may have fetched a connection so lets call close()

throw ExceptionFactory.wrapException( “Error opening session. Cause: “ + e, e);

} finally {

ErrorContext.instance().reset();

}

}

复制代码

然后我们跟着代码进入这里的 newExecutor 方法:

public Executor newExecutor (Transaction transaction, ExecutorType executorType) {

executorType = executorType == null ? defaultExecutorType : executorType;

executorType = executorType == null ? ExecutorType.SIMPLE : executorType;

Executor executor;

// 注意这里创建的所有类型的 Executor 实际上都继承自 BaseExecutor

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);

}

// 判断之前传进来的 configuration 里是否开启缓存

if (cacheEnabled) {

// 这里传进去的 executor 就是后面 query 方法中的 delegate。

executor = new CachingExecutor(executor);

}

executor = (Executor) interceptorChain.pluginAll(executor);

return executor;

}

复制代码

先说一句题外话,我们看到,根据传入的类型会创建不同类型的 Executor ,而这里的 “ BatchExecutor ` ` ReuseExecutor ` ` SimpleExecutor ` 实际上都继承了 ` BaseExecutor “方法,这里 Mybatis 采用了模板模式。定义了很多操作顺序,而由子类实现具体方法。后期会出一个设计模式的板块,敬请期待。

好了,言归正传。我们发现这里有一个很让人欣喜的判断:“ if (cacheEnabled) ` ,嘿我们昨天从 mybatis-config.xml 配置文件里读进来的好像就是这玩意儿!没错就是他,这里会根据你设置 cacheEnabled 的值来决定是否创建 ` CachingExecutor ` 。也就是说如果我们设置为 true,这里就会为这些 Executor 们包装上一层 ` CachingExecutor ` 。而这个 ` CachingExecutor “则是二级缓存的关键包装类。

OK,创建 SQLSession 的步骤完成了,我们紧接着来看他的查询方法究竟是怎么使用缓存的吧!

使用缓存第二步:生成缓存 Key

话不多说,我们直接上查询的源码吧,这里以 selectList 为例:

这里追踪源码时,不要忘记实现类是 CachingExecutor

@Override

public <E> List<E> query (MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {

BoundSql boundSql = ms.getBoundSql(parameterObject);

// 根据 ms、参数、分页参数、sql 生成这个 statement 唯一的缓存 key

CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);

return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

}

复制代码

我们继续追踪生成 key 的方法:

public CacheKey createCacheKey (MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {

if (closed) {

throw new ExecutorException( “Executor was closed.” );

}

// 新建一个 CacheKey,并更新 cacheKey 的 hashcode

CacheKey cacheKey = new CacheKey();

// 附加计算当前 sql 的 id,即 <select id = “xxxx”><select>

cacheKey.update(ms.getId());

// 附加计算分页中的 offset

cacheKey.update(rowBounds.getOffset());

// 附加计算分页中的 limit

cacheKey.update(rowBounds.getLimit());

// 附加计算 sql 语句

cacheKey.update(boundSql.getSql());

// 取到参数映射

List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();

// 拿到配置中加载好的 处理类 注册簿,内部维护了一个 HashMap

TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();

// mimic DefaultParameterHandler logic

// 模仿DefaultParameterHandler逻辑

for (ParameterMapping parameterMapping : parameterMappings) {

// 判断这里的参数不是存储过程的 out 类参数

if (parameterMapping.getMode() != ParameterMode.OUT) {

Object value;

// 拿到属性名称

String propertyName = parameterMapping.getProperty();

if (boundSql.hasAdditionalParameter(propertyName)) {

// 如果有附加参数,取出附加参数

value = boundSql.getAdditionalParameter(propertyName);

} else if (parameterObject == null ) {

// 参数为空的情况

value = null ;

} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {

// 如果有相应的类型处理器,参数为本身

value = parameterObject;

} else {

// 创建一个 MetaObject

MetaObject metaObject = configuration.newMetaObject(parameterObject);

value = metaObject.getValue(propertyName);

}

// 将参数也附加到 CacheKey 的 hashcode 计算中

cacheKey.update(value);

}

}

if (configuration.getEnvironment() != null ) {

// 如果配置文件中 environment 标签不为空

// issue #176

// 再加上当前环境的 id 即 <environment id=”development”>

cacheKey.update(configuration.getEnvironment().getId());

}

return cacheKey;

}

复制代码

不知道你们好不好奇这个 update 方法,不管了,我们继续跟进去看看他到底对这些个东西们做了什么

package org.apache.ibatis.cache;

public class CacheKey implements Cloneable , Serializable {

// 乘数,固定初始值质数37,不会变

private static final int DEFAULT_MULTIPLIER = 37 ;

// 当前hashCode值,初始值是质数17,

private static final int DEFAULT_HASHCODE = 17 ;

// 乘数,默认值为质数37,不会变

private final int multiplier;

// 当前hashCode值,默认值为质数17,

private int hashcode;

// 所有更新对象的初始hashCode的和

private long checksum;

// 更新的对象总数

private int count;

/*

8/21/2017 – Sonar lint flags this as needing to be marked transient.

While true if content is not serializable,

this is not always true and thus should not be marked transient.

*/

// 已更新的所有 obj 的列表

private List<Object> updateList;

public CacheKey () {

this .hashcode = DEFAULT_HASHCODE;

this .multiplier = DEFAULT_MULTIPLIER;

this .count = 0 ;

this .updateList = new ArrayList<>();

}

public void update (Object object) {

// 先计算传进来的这个 obj 的基础 HashCode,如果为空的话则是 1

int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);

// 记录更新个数

count++;

// 计算 hashCode 的总和

checksum += baseHashCode;

// 将基础 HashCode 跟更新个数相乘

baseHashCode *= count;

hashcode = multiplier * hashcode + baseHashCode;

// 将传进来的 obj 放到已更新列表中

updateList.add(object);

}

}

复制代码

具体的代码在这里,深刻的思想我也并没有研究出来。他这样做的原理我也没思考出来。但是目的我猜一定是为了让 hashcode 尽量的不重复,以做到在 map 中尽量散列分布,避免 hash 冲突。

生成了 缓存键 后,我们终于来到了查询步骤,话不多说,我们来看看 query 方法做了什么!

使用缓存第三步:查询使用二级缓存!

我们来详细看下 query 方法到底做了什么

@Override

public <E> List<E> query (MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)

throws SQLException {

Cache cache = ms.getCache();

if (cache != null ) {

// 如果标签属性上标注了 flushCache=”true” ,这里会先清空缓存

flushCacheIfRequired(ms);

if (ms.isUseCache() && resultHandler == null ) {

// 确定本条不是一个有 OutParams 的存储过程,否则抛出异常

ensureNoOutParams(ms, boundSql);

@SuppressWarnings(“unchecked”)

// 内部方法是尝试从 cache 中拿值

List<E> list = (List<E>) tcm.getObject(cache, key);

if (list == null ) {

// 但是如果真的没拿出来的话,真正查询还是交由传入的执行器来执行

// 也就是传说中的 装饰器模式

list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

// 这里是往 TransactionalCache 中赋值

tcm.putObject(cache, key, list); // issue #578 and #116

}

return list;

}

}

return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

}

复制代码

一步一步来,我们先看获取缓存,也就是 “ tcm.getObject ` 方法。这里 tcm 代表的是 ` TransactionalCacheManager ` 对象,是 ` CachingExecutor ` 的一个成员变量,也就是说随着 ` CachingExecutor ` 实例的创建而创建,随 CachingExecutor 实例回收而回收。那它是干啥的呢,它其实内部维护了一个以 ` Cache ` 为键, ` TransactionalCache “为值的一个 Map。我们来看看这个类的具体实现和方法:

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) {

// 这里的 computeIfAbsent 相当于如下代码:

/*

if(null == transactionalCaches.get(cache)){

transactionalCaches.put(cache, new TransactionalCache(cache));

}

transactionalCaches.computeIfAbsent(cache, k -> new TransactionalCache(k));

*/

return transactionalCaches.computeIfAbsent(cache, TransactionalCache:: new );

}

}

复制代码

我们看回到 “ getObject ` 方法,这里调用了 ` getTransactionalCache ` 方法从内部维护的 HashMap 中拿到了一个 ` TransactionalCache ` 实例并调用它的 get 方法。这里的 ` computeIfAbsent “方法是 1.8 中针对 HaspMap 的方法,具体示意我写在注释里了,大家感兴趣的话可以自行查询~

这一步需要注意的是,在 get 不到值的时候 new 出来的 “ TransactionalCache “实际上是一个包装类,进一步包装了 cache。

我们来看下 “ TransactionalCache “的构造方法和 get 方法你就懂了:

public class TransactionalCache implements Cache {

private static final Log log = LogFactory.getLog(TransactionalCache.class);

private final Cache delegate;

private boolean clearOnCommit;

private final Map<Object, Object> entriesToAddOnCommit;

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 Object getObject (Object key) {

// issue #116

// 注意这里拿是在 delegate 中拿的而不是 entriesToAddOnCommit 中

Object object = delegate.getObject(key);

if (object == null ) {

entriesMissedInCache.add(key);

}

// issue #146: https://github.com/mybatis/mybatis-3/issues/146

if (clearOnCommit) {

return null ;

} else {

return object;

}

}

}

复制代码

也就是这里的 get 实际上是从 “ delegate ` 即 传入的 cache 中拿的。这里如果没拿到,会记录一个 未命中 CacheKey,这个操作后面 commit 的时候我们详说。总之,这里第一次进来肯定是查不到的,也就是这会返回一个 null。返回到我们的 ` query ` 的代码,这里他判断如果拿出来的 list 为空,则调用被包装类的 ` query ` 方法,即 ` SimpleExecutor ` ` query ` 方法,即 ` BaseExecutor ` ` query “方法。这里就涉及到了一级缓存使用的过程。

使用缓存第四步:查询使用一级缓存!

我们来看下这个方法做了些什么。

@SuppressWarnings(“unchecked”)

@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.” );

}

// 判断有没有刷新缓存的必要(属性 flushCache=”true” )

if (queryStack == 0 && ms.isFlushCacheRequired()) {

clearLocalCache();

}

List<E> list;

try {

queryStack++;

// 此处的缓存是一级缓存,因为 localCache 是每个 Executor 自己维护的。

// 随着每次close,都会被清空。 新建的 Executor 也无法使用上次的。

list = resultHandler == null ? (List<E>) localCache.getObject(key) : null ;

if (list != null ) {

// 如果从缓存中拿出数据,这里处理的是存储过程相关的 sql 和 参数

handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);

} else {

list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

}

} finally {

queryStack–;

}

if (queryStack == 0 ) {

for (DeferredLoad deferredLoad : deferredLoads) {

deferredLoad.load();

}

// issue #601

deferredLoads.clear();

// 这里判断缓存范围如果是 STATEMENT 级别的话,清空本地缓存

// 即 <setting name=”localCacheScope” value=”STATEMENT”/>

if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {

// issue #482

clearLocalCache();

}

}

return list;

}

复制代码

这个 “ localCache ` 就是我们一直说的 一级缓存 对象,看完这里大家一定很好奇,这里只见到了拿缓存的方法( ` localCache.getObject ` )但是没看到在哪放的呀。大家稍安勿躁,我们来看看这个 ` 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 {

// 不管查询是否失败,先从map中删掉占位对象

localCache.removeObject(key);

}

// 这里把 list 存到本地缓存中

localCache.putObject(key, list);

if (ms.getStatementType() == StatementType.CALLABLE) {

localOutputParameterCache.putObject(key, parameter);

}

return list;

}

复制代码

呐,看到了吧。查完后 “ localCache.putObject “ 方法就是放缓存的。这里为什么放置占位对象笔者也没太想懂,各位看官大佬有想法可以留言讨论哦。

我们再看回 “ query ` 方法,会发现这里有一步清除缓存的判断,这里的 ` localCacheScope “ 我觉得还是有必要拿出来说一下的,这是禁用一级缓存的必要手段。我们可以在 mybatis-config.xml 这个配置文件中,设置相应的 settings 来关闭一级缓存例如:

< settings >

< setting name = “localCacheScope” value = “STATEMENT” />

</ settings >

复制代码

官网给这个配置的解释是:

MyBatis uses local cache to prevent circular references and speed up repeated nested queries. By default (SESSION) all queries executed during a session are cached. If localCacheScope=STATEMENT local session will be used just for statement execution, no data will be shared between two different calls to the same SqlSession.<br>

谷歌翻译:MyBatis使用本地缓存来防止循环引用并加快重复的嵌套查询。 默认情况下(会话),将缓存会话期间执行的所有查询。 如果 localCacheScope = STATEMENT 本地会话仅用于语句执行,则对同一SqlSession的两个不同调用之间不会共享数据。

欸,是不是奇怪的知识又增加了。话不多说我们接着看 query 查询完成后的事情吧:

使用缓存第五步:放置二级缓存!

查询完毕后,就调用了 tcm.putObject ,好我知道大家肯定找不到了,这里我再放一边 “ put “ 方法的源码:

public void putObject (Cache cache, CacheKey key, Object value) {

getTransactionalCache(cache).putObject(key, value);

}

private TransactionalCache getTransactionalCache (Cache cache) {

// 这里的 computeIfAbsent 相当于如下代码:

/*

if(null == transactionalCaches.get(cache)){

transactionalCaches.put(cache, new TransactionalCache(cache));

}

transactionalCaches.computeIfAbsent(cache, k -> new TransactionalCache(k));

*/

return transactionalCaches.computeIfAbsent(cache, TransactionalCache:: new );

}

复制代码

这里我们再进一步追入 “ putObject “ 方法来看看。

@Override

public void putObject (Object key, Object object) {

// 所以事务不提交的话,在 delegate 中是拿不到的。用以保证事务缓存隔离

entriesToAddOnCommit.put(key, object);

}

复制代码

这里可以看到,这仅仅是在 “ TransactionalCache ` 实例内部的一个 HashMap 中暂存了一下,而并没有调用 delegate 的 put 方法。这也就是说为什么两个事务在提交前都读不到互相的缓存。其实这里可以衍生出很多有趣的 demo,例如 关闭一级缓存后,即使在同一个开启了二级缓存 ` sqlsession “中查询两次,也需要查询两次数据库。具体更多有意思的 demo 可以留言一起交流~

这里 put 进了临时的 map 中,那么什么时候合并进主存中呢?是的,就是当事务提交时,当 “ CachingExecutor ` 执行 ` commit “时,会顺带调用 tcm 的提交方法:

@Override

public void commit ( boolean required) throws SQLException {

delegate.commit(required);

tcm.commit();

}

复制代码

这里面就将当前事务的临时缓存存入了主缓存:

public void commit () {

for (TransactionalCache txCache : transactionalCaches.values()) {

txCache.commit();

}

}

// txCache.commit

public void commit () {

if (clearOnCommit) {

delegate.clear();

}

// 当事务提交时,这里统一刷缓存

flushPendingEntries();

reset();

}

/**

* 这个方法是将本次事务缓存中的所有缓存刷到 delegate 中

* 做到了缓存的事务隔离

*/

private void flushPendingEntries () {

// 遍历 entry

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 );

}

}

}

复制代码

这里说到了我们之前放过的 “ entriesToAddOnCommit “,这里如果没命中缓存,且在提交的时候也没查出来,那么就会向主缓存中放一个 null 值占位。目的我猜测是防止缓存击穿。

那么这里有缓存,我们进行增删改的时候,会刷新缓存嘛?我们继续看

使用缓存第六步:更新时清除缓存!

我们分别写了三个语句,并用 insert | update | delete 三个方法执行:

sqlSession1.insert( “com.simon.demo.TestMapper.insertBookInfo” );

sqlSession1.update( “com.simon.demo.TestMapper.updateBookInfo” );

sqlSession1.delete( “com.simon.demo.TestMapper.deleteBookInfo” );

复制代码

有点源码基础的同学其实知道这里三个方法 共用了同一个 update 方法

那么这个 update 方法内部对缓存又进行了什么操作呢?(注意这里选择实现类时,要选择 CachingExecutor )

@Override

public int update (MappedStatement ms, Object parameterObject) throws SQLException {

// 先根据需要看是否清除缓存

flushCacheIfRequired(ms);

// 在调用 被包装类的 update 方法

return delegate.update(ms, parameterObject);

}

private void flushCacheIfRequired (MappedStatement ms) {

// 获取当前缓存

Cache cache = ms.getCache();

// 这里可以看加载生成 Mapper 的默认赋值 ->

if (cache != null && ms.isFlushCacheRequired()) {

// 调用缓存清除方法

tcm.clear(cache);

}

}

复制代码

这里有两个重点,一个是 isFlushCacheRequired 是在哪加载到的,实际上这就是在我们生成 MappedStatement 时加载进 ms 的:

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.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)

// 这里定义了是否清除缓存区,默认值取决于是否是 select 类型的 sql

// 如果是 select 的话,默认不清除缓存,不是 select 默认清除

.flushCacheRequired(valueOrDefault(flushCache, !isSelect))

// 这里定义了是否使用缓存,默认值也取决于是否是 select 类型的 sql

// 如果是 select 的话,默认开启缓存

.useCache(valueOrDefault(useCache, isSelect))

// 这里将前面创造好的 Cache 对象绑定进 mappedStatement 对象

// 这里将已有的缓存绑定入 MappedStatement 对象

.cache(currentCache);

ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);

if (statementParameterMap != null ) {

statementBuilder.parameterMap(statementParameterMap);

}

// 做必要参数的非空校验

MappedStatement statement = statementBuilder.build();

// 在上下文中加入处理好的MappedStatement,以 id 为 key,实例为 value

configuration.addMappedStatement(statement);

return statement;

}

复制代码

第二个重点就是 tcm 的清理方法,即 “ tcm.clear “ 方法:

// TransactionalCacheManager

public void clear (Cache cache) {

getTransactionalCache(cache).clear();

}

复制代码

这里实际上调用的是 map 中所存的 “ TransactionalCache ` 实例的 ` clear “方法:

@Override

public void clear () {

// 提交时清除的 标志位

clearOnCommit = true ;

// 当前内部缓存清除

entriesToAddOnCommit.clear();

}

复制代码

大家有没有发现一个事情,这里执行完,实际上并没有清掉主缓存,而是只是清掉了当前事务的临时缓存。大家还记得我们的提交方法嘛?

// txCache.commit

public void commit () {

if (clearOnCommit) {

delegate.clear();

}

// 当事务提交时,这里统一刷缓存

flushPendingEntries();

reset();

}

复制代码

看到没,这里只有在提交(commit)的时候,才会去清主存。这么做也是防止不同事务之间的脏读。这里也可延伸出很多好玩的 demo,比如 sqlSession1 先 select 然后 commit 然后 insert ,sqlsession2 执行相同查询时不查数据库,而是返回 sqlSession1 第一次查询的值。

说到这里,我们的缓存好强大啊,那我们的缓存是完美的嘛?当然不是,我们接着来看:

使用缓存第七步:明白优缺点!

我们使用缓存当然要明白他的优势和缺点在哪里:

– 优点:优点自然不用多说,我们可以减少查询数据库的次数,降低打开、关闭数据库连接的性能消耗。提高查询速度,缩短查询时间。

– 缺点:其实最大的缺点在于很容易发生数据的不一致性,为什么这么说呢。我们知道,每个缓存是基于 Mapper 的,缓存的清空也是基于当前 Mapper 的 insert | update | delete 等更新操作。那么我们分两点来看:

* 第一点是网上普遍说的针对一个表中的所有操作必须放到一个 Mapper 中,比如现在有 Mapper A 和 Mapper B,A 中有针对表 T 的读 sql,B 中则是对表的写 sql,那么这就会导致 A 中修改数据未刷新 B 的缓存,那么读到的数据就是有问题的。针对这个问题实际上是有解法的,我们大可使用 “ cache-ref “ 标签解决。在文章的一开始介绍了 cache-ref 标签。可以让两个 Mapper 使用同一个 Cache ,这样就解决了不刷新的问题

* 第二个问题是第一个问题的加深版。因为我发现,分布式是无法解决上述问题的。针对两台机器上部署相同的微服务,假如 A 机器读,B机器写且提交,A再去读的话,就有可能会读到二级缓存的东西而导致数据出错。所以才会采用 Redis 之类的缓存手动做缓存失效和刷新。

整个缓存的流程到这里就基本结束了,其实其中还略过了很多东西,例如 缓存回收策略类的包装 是如何构建的,缓存是如何回收的 ,缓存失效策略具体是如何实现的等。还有待大家细细探寻。

微信扫一扫,分享到朋友圈

Mybatis-3 源码之缓存是怎么创建和使用的

苹果产品经理:美版iPhone 12支持毫米波5G频段、网速是其它型号十几倍

上一篇

深圳区块链技术应用峰会圆满结束

下一篇

你也可能喜欢

Mybatis-3 源码之缓存是怎么创建和使用的

长按储存图像,分享给朋友