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 {
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
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);
}
}
* 把 settings 标签的所有配置加载成 Properties
* @param context
* @return
*/
private Properties settingsAsProperties(XNode context) {
if (context == null) {
return new Properties();
}
Properties props = context.getChildrenAsProperties();
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) {
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 (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
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.");
}
}
}
}
}
}
复制代码
找到 Mapper 后,开始针对 Mapper 的解析:
package org.apache.ibatis.builder.xml;
public class XMLMapperBuilder extends BaseBuilder {
……
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
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);
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"));
} 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) {
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
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();
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");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
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);
······
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);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx);
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;
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);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx);
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;
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;
}
复制代码
先说一句题外话,我们看到,根据传入的类型会创建不同类型的 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);
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 = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
for (ParameterMapping parameterMapping : parameterMappings) {
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 = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
复制代码
不知道你们好不好奇这个 update 方法,不管了,我们继续跟进去看看他到底对这些个东西们做了什么
package org.apache.ibatis.cache;
public class CacheKey implements Cloneable, Serializable {
private static final int DEFAULT_MULTIPLIER = 37;
private static final int DEFAULT_HASHCODE = 17;
private final int multiplier;
private int 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.
*/
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) {
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
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) {
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);
}
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) {
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) {
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
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.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
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) {
deferredLoad.load();
}
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
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 {
localCache.removeObject(key);
}
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.
谷歌翻译: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) {
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) {
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();
}
}
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
* 这个方法是将本次事务缓存中的所有缓存刷到 delegate 中
* 做到了缓存的事务隔离
*/
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);
}
}
}
复制代码
这里说到了我们之前放过的 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);
return delegate.update(ms, parameterObject);
}
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
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)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
}
复制代码
第二个重点就是 tcm 的清理方法,即 tcm.clear
方法:
public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}
复制代码
这里实际上调用的是 map 中所存的 TransactionalCache
实例的
clear
方法:
@Override
public void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear();
}
复制代码
大家有没有发现一个事情,这里执行完,实际上并没有清掉主缓存,而是只是清掉了当前事务的临时缓存。大家还记得我们的提交方法嘛?
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 之类的缓存手动做缓存失效和刷新。
整个缓存的流程到这里就基本结束了,其实其中还略过了很多东西,例如缓存回收策略类的包装是如何构建的,缓存是如何回收的 ,缓存失效策略具体是如何实现的等,还有待大家细细探寻。
评论