Categories
程式開發

Mybatis-3 源碼之緩存是怎麼創建和使用的


Mybatis-3 源碼之緩存是怎麼創建的

Mybatis 緩存問題其實也是面試高頻的問題了,今天我們就從源碼級別來談談Mybatis 的緩存實現。

(本文源碼均在https://github.com/ccqctljx/Mybatis-3 中,會持續更新註釋和Demo)。

首先我們了解一下緩存是什麼:緩存是一般的ORM 框架都會提供的功能,目的就是提升查詢的效率和減少數據庫的壓力。直白一點就是,開了緩存後,同樣的數據查詢不必再次訪問數據庫,直接從緩存中拿即可。

那麼面試官常問的一級緩存和二級緩存又都是什麼呢?

一級緩存:一級緩存又稱本地緩存,是在會話(SqlSession)層面進行的緩存。隨會話開始而生,結束而死。 MyBatis 的一級緩存是默認開啟的,不需要任何的配置。

二級緩存:由於一級緩存隨會話而生,就不能跨會話共享。二級緩存則是用來解決這個問題的,他的範圍是namespace 級別的,可以被多個SqlSession 共享,生命週期和SqlSessionFactory 同步。只要是同一個SqlSessionFactory 創建出來的會話,即可共享相同namespace 級別的緩存。二級緩存需要配置三個地方:

第一個是在mybaits-config.xml 配置文件中設置開啟緩存:““

第二個是要在Mapper 文件中配置“ “標籤

第三個是在需要使用緩存的語句上加入“useCache=”true” “

那麼一級二級緩存有沒有執行順序什麼的呢?答案是有的,如果開啟二級緩存那麼執行順序為:

Mybatis-3 源碼之緩存是怎麼創建和使用的 1

那麼我們寫個實例代碼,來看下一二級緩存的效果吧

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 bookInfoList1 = sqlSession1.selectList("com.simon.demo.TestMapper.selectBookInfo");
System.out.println(" sqlSession 1 query 1 ----------------------------- " + bookInfoList1);

List 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 bookInfoList3 = sqlSession2.selectList("com.simon.demo.TestMapper.selectBookInfo");
System.out.println("sqlSession 2 query 1 ----------------------------- " + bookInfoList3);
}
}

打印結果是:

Mybatis-3 源碼之緩存是怎麼創建和使用的 2

由此我們能看到,只有第一次查詢執行了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()) {
// 判断是否是 Package,如果是的话可以直接拿 Package 去加载包下的 mapper 文件
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) {
// 此处定义错误上下文,如果这里加载出错日志打印 ("### The error may exist in xxx");
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"));
// 解析每个 sql 标签(mapper 中有两种 sql,一种是 下面要解析的四打标签,还有直接用 sql 标签的)
sqlElement(context.evalNodes("/mapper/sql"));
// 解析四大标签,并放入 configuration 中,这里也会为每个开启缓存的 statement 设置上面生成好的缓存对象,也就是 Cache
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 中是否標註了“` 標籤,這個標籤的意思是我可以跟其他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");
// typeAliasRegistry 内部维护了一个 HashMap 并且预设了很多类别名,例如 "byte" -> Byte.class
// 这里指的就是之前加载配置时 typeAliasesElement 方法所做的
Class typeClass = typeAliasRegistry.resolveAlias(type);
// eviction 意为驱逐、赶出。这里则代表着 缓存清除策略,即如何清除无用的缓存
// 代码可以看到,默认是 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 evictionClass = typeAliasRegistry.resolveAlias(eviction);
// 刷新间隔,单位 毫秒,代表一个合理的毫秒形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用 update 语句时刷新。
Long flushInterval = context.getLongAttribute("flushInterval");
// 引用数目,要记住你缓存的对象数目和你运行环境的可用内存资源数目。默认值是1024。
Integer size = context.getIntAttribute("size");

// 下面是针对缓存对象实例是否只读的配置
// 只读的缓存会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改(一旦修改,别人取到的也是修改后的)。这提供了很重要的性能优势。
// 可读写的缓存会返回缓存对象的拷贝(通过序列化)。这会慢一些,但是安全,因此默认是false。
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
// 设置是否是阻塞缓存,如果是 true ,则在创建缓存的时候会包装一层 BlockingCache 。默认为 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 typeClass,
Class evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
// 此处使用建造者模式创建了 Cache,并且绑定了当前 Mapper 的命名空间并作为此 Cache 的 ID。
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");

// 构建解析完成的 MappedStatement ,也就是将 标签中的东西转为对象
// 此处绑定了二级缓存
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 为空,则采用默认的,如果默认的为空,则采用 simple
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 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 为空,则采用默认的,如果默认的为空,则采用 simple
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

Mybatis-3 源碼之緩存是怎麼創建和使用的 3

@Override
public List 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,即
cacheKey.update(ms.getId());
// 附加计算分页中的 offset
cacheKey.update(rowBounds.getOffset());
// 附加计算分页中的 limit
cacheKey.update(rowBounds.getLimit());
// 附加计算 sql 语句
cacheKey.update(boundSql.getSql());
// 取到参数映射
List parameterMappings = boundSql.getParameterMappings();
// 拿到配置中加载好的 处理类 注册簿,内部维护了一个 HashMap
// 加载步骤为 org.apache.ibatis.builder.xml.XMLConfigBuilder.parseConfiguration 方法中的 typeHandlerElement 方法
// 以键值对形式存储每个类型的 typeHandler 如 Boolean.class -> new BooleanTypeHandler()
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 即
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 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 为 固定数字 37 * 最新 hashcode 再加上 计算后的参数对象的 hashcode
hashcode = multiplier * hashcode + baseHashCode;
// 将传进来的 obj 放到已更新列表中
updateList.add(object);
}
}

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

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

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

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

@Override
public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 这里是看我们有没有定义 Cache 对象,也就是我们在 Mapper 文件中有没有定义 标签
// 如果有标签,在读取 Mapper 文件时会创建 Cache 对象来存储这个 Mapper 文件中所有需要缓存的东西
Cache cache = ms.getCache();
if (cache != null) {
// 如果标签属性上标注了 flushCache="true" ,这里会先清空缓存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
// 确定本条不是一个有 OutParams 的存储过程,否则抛出异常
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 这里 TransactionalCacheManager 维护了一个以 Cache 为键,TransactionalCache 为值的一个 Map
// 内部方法是尝试从 cache 中拿值
List list = (List) tcm.getObject(cache, key);
if (list == null) {
// 这里的 delegate 代表的是根据ExecutorType创建的几大执行器,例如 SimpleExecutor。
// 也就是说,他这里只不过是先根据是否开启二级缓存,尝试是否能从缓存中拿到数据,
// 但是如果真的没拿出来的话,真正查询还是交由传入的执行器来执行
// 也就是传说中的 装饰器模式
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 transactionalCaches = new HashMap();

public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}

public Object getObject(Cache cache, CacheKey key) {
// 这里看上去是先根据 Cache 拿出内部 TransactionalCache,然后再从 TransactionalCache 中拿值。
// 但实际上 TransactionalCache 是一个装饰器类,它负责装饰了 cache ,最终还是从 cache 中拿的值
return getTransactionalCache(cache).getObject(key);
}

public void putObject(Cache cache, CacheKey key, Object value) {
// 这里看上去跟上面的 getObject 方法一样,但是这里却不是给 cache put 值,
// 而是给 TransactionalCache 内部维护的一个 HashMap 类型的变量 entriesToAddOnCommit put值
// 这么做是为了保证事务的隔离性,缓存同样要等事务提交后统一刷到公共 cache 中
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 entriesToAddOnCommit;
private final Set 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) {
// 记录未命中缓存的 CacheKey,后面 commit 的时候会放置一个 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 List 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 list;
try {
queryStack++;
// 这里判断是否指定 ResultHandler,如果没指定则尝试从缓存中拿,指定了则直接查数据库
// 此处的缓存是一级缓存,因为 localCache 是每个 Executor 自己维护的。
// 随着每次close,都会被清空。 新建的 Executor 也无法使用上次的。
list = resultHandler == null ? (List) 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 级别的话,清空本地缓存
// 即
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}

這個``localCache `就是我們一直說的一級緩存對象,看完這里大家一定很好奇,這裡只見到了拿緩存的方法(`localCache.getObject`)但是沒看到在哪放的呀。大家稍安勿躁,我們來看看這個`queryFromDatabase`` 方法:

private List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List list;
// localCache 内部维护了一个空的 HashMap ,这一步是先在localCache中放一个占位对象。
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) {
// 当 statementType="CALLABLE"的时候,也就是调用存储过程的时候,设置 out 类参数
localOutputParameterCache.putObject(key, parameter);
}
return list;
}

吶,看到了吧。查完後``localCache.putObject`` 方法就是放緩存的。這里為什麼放置佔位對象筆者也沒太想懂,各位看官大佬有想法可以留言討論哦。

我們再看回``query` 方法,會發現這裡有一步清除緩存的判斷,這裡的`localCacheScope`` 我覺得還是有必要拿出來說一下的,這是禁用一級緩存的必要手段。我們可以在mybatis-config.xml 這個配置文件中,設置相應的settings 來關閉一級緩存例如:

官網給這個配置的解釋是:

MyBatis使用本地緩存來防止循環引用並加快重複的嵌套查詢。 默認情況下(SESSION)在會話期間執行的所有查詢都將被緩存。 如果localCacheScope = STATEMENT本地會話僅用於語句執行,則對同一SqlSession的兩個不同調用之間不會共享任何數據。
谷歌翻譯:MyBatis使用本地緩存來防止循環引用並加快重複的嵌套查詢。默認情況下(會話),將緩存會話期間執行的所有查詢。如果localCacheScope = STATEMENT 本地會話僅用於語句執行,則對同一SqlSession的兩個不同調用之間不會共享數據。

欸,是不是奇怪的知識又增加了。話不多說我們接著看query 查詢完成後的事情吧:

使用緩存第五步:放置二級緩存!

查詢完畢後,就調用了tcm.putObject,好我知道大家肯定找不到了,這裡我再放一邊``put`` 方法的源碼:

public void putObject(Cache cache, CacheKey key, Object value) {
// 这里看上去跟上面的 getObject 方法一样,但是这里却不是给 cache put 值,
// 而是给 TransactionalCache 内部维护的一个 HashMap 类型的变量 entriesToAddOnCommit put值
// 这么做是为了保证事务的隔离性,缓存同样要等事务提交后统一刷到公共 cache 中
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) {
// 这里的putObject 方法只是将 obj 放到了当前事务的缓存中即 entriesToAddOnCommit 中。
// 所以事务不提交的话,在 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 entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
// 如果未命中的 CacheKey 在 当前内部缓存中没有的话,则放置一个 null 进主缓存
// 目的应该是防止缓存击穿(大量查询一个不存在的值)
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 方法

Mybatis-3 源碼之緩存是怎麼創建和使用的 4

那麼這個update 方法內部對緩存又進行了什麼操作呢? (注意這裡選擇實現類時,要選擇CachingExecutor )

Mybatis-3 源碼之緩存是怎麼創建和使用的 5

@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();
// 除非配置,不然 insert | update | delete 三大标签的 flushCacheRequired 默认为 true
// 这里可以看加载生成 Mapper 的默认赋值 ->
// org.apache.ibatis.builder.xml.XMLStatementBuilder.parseStatementNode ->
// org.apache.ibatis.builder.MapperBuilderAssistant.addMappedStatement
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 对象
// 也就是说不管是什么类型的语句(包括 insert update delete)都有绑定缓存对象
.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 之類的緩存手動做緩存失效和刷新。

整個緩存的流程到這裡就基本結束了,其實其中還略過了很多東西,例如緩存回收策略類的包裝是如何構建的,緩存是如何回收的,緩存失效策略具體是如何實現的等。還有待大家細細探尋。