一、快取模組
MyBatis作為一個強大的持久層框架,快取是其必不可少的功能之一,Mybatis中的快取分為一級快取和二級快取。但本質上是一樣的,都是使用Cache介面實現的。快取位於 org.apache.ibatis.cache包下。
通過結構能夠發現Cache其實使用到了裝飾器模式來實現快取的處理。先來看看Cache中的基礎類的API;Cache介面的實現類很多,但是大部分都是裝飾器,只有PerpetualCache提供了Cache介面的基本實現。
1.1 Cache介面
Cache介面是快取模組中最核心的介面,它定義了所有快取的基本行為,Cache介面的定義如下:
public interface Cache { /**
* 快取物件的 ID
* @return The identifier of this cache
*/
String getId(); /**
* 向快取中新增資料,一般情況下 key是CacheKey value是查詢結果
* @param key Can be any object but usually it is a {@link CacheKey}
* @param value The result of a select.
*/
void putObject(Object key, Object value); /**
* 根據指定的key,在快取中查詢對應的結果物件
* @param key The key
* @return The object stored in the cache.
*/
Object getObject(Object key); /**
* As of 3.3.0 this method is only called during a rollback
* for any previous value that was missing in the cache.
* This lets any blocking cache to release the lock that
* may have previously put on the key.
* A blocking cache puts a lock when a value is null
* and releases it when the value is back again.
* This way other threads will wait for the value to be
* available instead of hitting the database.
* 刪除key對應的快取資料
*
* @param key The key
* @return Not used
*/
Object removeObject(Object key); /**
* Clears this cache instance.
* 清空快取
*/
void clear(); /**
* Optional. This method is not called by the core.
* 快取的個數。
* @return The number of elements stored in the cache (not its capacity).
*/
int getSize(); /**
* Optional. As of 3.2.6 this method is no longer called by the core.
* <p>
* Any locking needed by the cache must be provided internally by the cache provider.
* 獲取讀寫鎖
* @return A ReadWriteLock
*/
default ReadWriteLock getReadWriteLock() {
return null;
} }
1.2 PerpetualCache
PerpetualCache在快取模組中扮演了ConcreteComponent的角色,其實現比較簡單,底層使用HashMap記錄快取項,具體的實現如下
/**
* 在裝飾器模式用 用來被裝飾的物件
* 快取中的 基本快取處理的實現
* 其實就是一個 HashMap 的基本操作
* @author Clinton Begin
*/
public class PerpetualCache implements Cache { private final String id; // Cache 物件的唯一標識 // 用於記錄快取的Map物件
private final Map<Object, Object> cache = new HashMap<>(); public PerpetualCache(String id) {
this.id = id;
} @Override
public String getId() {
return id;
} @Override
public int getSize() {
return cache.size();
} @Override
public void putObject(Object key, Object value) {
cache.put(key, value);
} @Override
public Object getObject(Object key) {
return cache.get(key);
} @Override
public Object removeObject(Object key) {
return cache.remove(key);
} @Override
public void clear() {
cache.clear();
} @Override
public boolean equals(Object o) {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
if (this == o) {
return true;
}
if (!(o instanceof Cache)) {
return false;
} Cache otherCache = (Cache) o;
// 只關心ID
return getId().equals(otherCache.getId());
} @Override
public int hashCode() {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
// 只關心ID
return getId().hashCode();
} }
然後可以來看看cache.decorators包下提供的裝飾器。他們都實現了Cache介面。這些裝飾器都在PerpetualCache的基礎上提供了一些額外的功能,通過多個組合實現一些特殊的需求。
1.3 BlockingCache
這是一個阻塞同步的快取,它保證只有一個執行緒到快取中查詢指定的key對應的資料
/**
* Simple blocking decorator
* 阻塞版的快取 裝飾器
* Simple and inefficient version of EhCache's BlockingCache decorator.
* It sets a lock over a cache key when the element is not found in cache.
* This way, other threads will wait until this element is filled instead of hitting the database.
*
* @author Eduardo Macarron
*
*/
public class BlockingCache implements Cache { private long timeout; // 阻塞超時時長
private final Cache delegate; // 被裝飾的底層 Cache 物件
// 每個key 都有物件的 ReentrantLock 物件
private final ConcurrentHashMap<Object, ReentrantLock> locks; public BlockingCache(Cache delegate) {
// 被裝飾的 Cache 物件
this.delegate = delegate;
this.locks = new ConcurrentHashMap<>();
} @Override
public String getId() {
return delegate.getId();
} @Override
public int getSize() {
return delegate.getSize();
} @Override
public void putObject(Object key, Object value) {
try {
// 執行 被裝飾的 Cache 中的方法
delegate.putObject(key, value);
} finally {
// 釋放鎖
releaseLock(key);
}
} @Override
public Object getObject(Object key) {
acquireLock(key); // 獲取鎖
Object value = delegate.getObject(key); // 獲取快取資料
if (value != null) { // 有資料就釋放掉鎖,否則繼續持有鎖
releaseLock(key);
}
return value;
} @Override
public Object removeObject(Object key) {
// despite of its name, this method is called only to release locks
releaseLock(key);
return null;
} @Override
public void clear() {
delegate.clear();
} private ReentrantLock getLockForKey(Object key) {
return locks.computeIfAbsent(key, k -> new ReentrantLock());
} private void acquireLock(Object key) {
Lock lock = getLockForKey(key);
if (timeout > 0) {
try {
boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
if (!acquired) {
throw new CacheException("Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId());
}
} catch (InterruptedException e) {
throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);
}
} else {
lock.lock();
}
} private void releaseLock(Object key) {
ReentrantLock lock = locks.get(key);
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
} public long getTimeout() {
return timeout;
} public void setTimeout(long timeout) {
this.timeout = timeout;
}
}
通過原始碼我們能夠發現,BlockingCache本質上就是在操作快取資料的前後通過ReentrantLock物件來實現了加鎖和解鎖操作。
快取實現類 | 快取實現類 | 作用 | 裝飾條件 |
基本快取 |
快取基本實現類 |
預設是PerpetualCache,也可以自定義比如RedisCache、EhCache等,具備基本功能的快取類 | 無 |
LruCache |
LRU策略的快取 |
當快取到達上限時候,刪除最近最少使用的快取(Least Recently Use) |
eviction="LRU"(預設) |
FifoCache |
FIFO策略的快取 |
當快取到達上限時候,刪除最先入隊的快取 |
eviction="FIFO" |
SoftCacheWeakCache |
帶清理策略的快取 |
通過JVM的軟引用和弱引用來實現快取,當JVM記憶體不足時,會自動清理掉這些快取,基於SoftReference和WeakReference |
eviction="SOFT"eviction="WEAK" |
LoggingCache |
帶日誌功能的快取 |
比如:輸出快取命中率 | 基本 |
SynchronizedCache |
同步快取 |
基於synchronized關鍵字實現,解決併發問題 |
基本 |
BlockingCache |
阻塞快取 |
通過在get/put方式中加鎖,保證只有一個執行緒操作快取,基於Java重入鎖實現 |
blocking=true |
SerializedCache |
支援序列化的快取 |
將物件序列化以後存到快取中,取出時反序列化 |
readOnly=false(預設) |
ScheduledCache |
定時排程的快取 |
在進行get/put/remove/getSize等操作前,判斷快取時間是否超過了設定的最長快取時間(預設 |
flushInterval不為空 |
TransactionalCache |
事務快取 |
在二級快取中使用,可一次存入多個快取,移除多個快取 |
在TransactionalCacheManager中用Map維護對應關係 |
1.4 快取的應用
1.4.1 快取對應的初始化
在之前寫的程式碼中斷個點看下可能直接點,在斷點前說明下要求,如要開啟快取要在配置檔案開啟一級和二級快取
然後呢在mapper.XML檔案加入<cache/>標籤就可以了
下面來斷點看下
通過上面截圖可以很清楚的看到這是一個裝飾器過程,接下來看下在Configuration初始化的時候怎麼給我們的各種Cache實現註冊對應的別名
在解析settings標籤的時候,設定的預設值有如下;因為前面原始碼跟了好多次,這裡面我直接進到解析這一段程式碼了
public Configuration parse() {
//檢查是否已經解析過了
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// XPathParser,dom 和 SAX 都有用到 >>
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
// 對於全域性配置檔案各種標籤的解析
propertiesElement(root.evalNode("properties"));
// 解析 settings 標籤
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"));
// settings 子標籤賦值,預設值就是在這裡提供的 >>
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
// 建立了資料來源 >>
environmentsElement(root.evalNode("environments"));
//解析databaseIdProvider標籤,生成DatabaseIdProvider物件(用來支援不同廠商的資料庫)。
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
// 解析引用的Mapper對映器
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
在上面的全域性配置檔案中在settingsElement(settings);的賦值中會做一些預設的處理,點進去看下
通過上面發現cacheEnabled預設為true,localCacheScope預設為 SESSION,在初始化過程中關鍵的還是對映檔案的解析,點選mapperElement(root.evalNode("mappers"));進去看下
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 不同的定義方式的掃描,最終都是呼叫 addMapper()方法(新增到 MapperRegistry)。這個方法和 getMapper() 對應
// package 包
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) {
// resource 相對路徑
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 解析 Mapper.xml,總體上做了兩件事情 >>
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
// url 絕對路徑
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 單個介面
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.");
}
}
}
}
}
直接進入他的關鍵程式碼mapperParser.parse();,
public void parse() {
// 總體上做了兩件事情,對於語句的註冊和介面的註冊
if (!configuration.isResourceLoaded(resource)) {
// 1、具體增刪改查標籤的解析。
// 一個標籤一個MappedStatement。 >>
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
// 2、把namespace(介面型別)和工廠類繫結起來,放到一個map。
// 一個namespace 一個 MapperProxyFactory >>
bindMapperForNamespace();
} parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
上面是對映檔案的解析操作,可以看他進了標籤的解析,進去看下
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
// 新增快取物件
cacheRefElement(context.evalNode("cache-ref"));
// 解析 cache 屬性,新增快取物件
cacheElement(context.evalNode("cache"));
// 建立 ParameterMapping 物件
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 建立 List<ResultMapping>
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 解析可以複用的SQL
sqlElement(context.evalNodes("/mapper/sql"));
// 解析增刪改查標籤,得到 MappedStatement >>
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
看到這裡好像找到了想找的東西,可以看到上面程式碼我標的兩個地方的標籤解析,跟進去看下
private void cacheElement(XNode context) {
// 只有 cache 標籤不為空才解析
if (context != null) {
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
Properties props = context.getChildrenAsProperties();
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
會發現上面開始解析相關的屬性資訊了,並在最後一步進行了儲存,繼續跟進去看下
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();
configuration.addCache(cache);
currentCache = cache;
return cache;
}
然後可以發現 如果儲存 cache 標籤,那麼對應的 Cache物件會被儲存在 currentCache 屬性中。
進而在 Cache 物件 儲存在了 MapperStatement 物件的 cache 屬性中。這就是cache節點建立的整個過程。
1.4.2 一級快取
一級快取也叫本地快取(Local Cache),MyBatis的一級快取是在會話(SqlSession)層面進行快取的。MyBatis的一級快取是預設開啟的,不需要任何的配置(如果要關閉,localCacheScope設定為STATEMENT)。在BaseExecutor物件的query方法中有關閉一級快取的邏輯
從上面的效果可以很清楚的感受到在一個會話內,第二次查詢是直接走快取的,在不同會話內快取是不起效的。下面會了解快取做了啥跟進程式碼看下。入口從上面演示就可以猜到是從
SqlSession sqlSession = factory.openSession();進入的
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
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);
// 根據事務工廠和預設的執行器型別,建立執行器 >>執行SQL語句操作
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;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {//針對Statement做快取
executor = new ReuseExecutor(this, transaction);
} else {
// 預設 SimpleExecutor,每一次只是SQL操作都建立一個新的Statement物件
executor = new SimpleExecutor(this, transaction);
}
// 二級快取開關,settings 中的 cacheEnabled 預設是 true
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 植入外掛的邏輯,至此,四大物件已經全部攔截完畢;這裡面是一個攔截器鏈
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
從上面程式碼可以知道如果 cacheEnabled 為 true 就會通過 CachingExecutor 來裝飾executor 物件,然後就是在執行SQL操作的時候會涉及到快取的具體使用。這個就分為一級快取和二級快取,通過這個跟蹤會發現在建立會話時會建立執行器,而執行器裡面跟快取有關係的是二級快取,跟我想找的一級快取沒什麼關係;那麼一級快取在哪呢,這時候我想一級快取是跟會話有關,那麼他的位置一定在會話內的這段程式碼裡,那我就找下一段程式碼
// 4.通過SqlSession中提供的 API方法來操作資料庫
List<User1> list = sqlSession.selectList("com.ghy.mapper.UserMapper.selectUserList");
進入selectList看下
@Override
public <E> List<E> selectList(String statement) {
return this.selectList(statement, null);
}
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
// 如果 cacheEnabled = true(預設),Executor會被 CachingExecutor裝飾
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
在上面程式碼中可以看到一個查詢操作,那肯定是要進去看下他在查詢前有沒有快取判斷,如果沒有說明selectList程式碼是不走快取的;
在上面程式碼中發現了一些跟快取相關的操作CacheKey
@Override
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()); // 0
cacheKey.update(rowBounds.getLimit()); // 2147483647 = 2^31-1
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
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); // development
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
發現上面是一個快取建立的邏輯,這個東西debugger看一下其實就明白了;其實這寫了一堆就是生成一個東西,生成一個快取的KEY,而且這個KEY是跟我們寫的SQL有關;明白了這個key的作用後回退一步跟進query看他拿這個key去做了什麼
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 異常體系之 ErrorContext
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()) {
// flushCache="true"時,即使是查詢,也清空一級快取
clearLocalCache();
}
List<E> list;
try {
// 防止遞迴查詢重複處理快取
queryStack++;
// 查詢一級快取
// ResultHandler 和 ResultSetHandler的區別
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();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
從上面就找到了查詢一級快取的位置了,如果list判斷是空說明快取沒資料他會走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 {
// 三種 Executor 的區別,看doUpdate
// 預設Simple
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;
}
1.4.3 二級快取
二級快取是用來解決一級快取不能跨會話共享的問題的,範圍是namespace級別的,可以被多個SqlSession共享(只要是同一個接口裡面的相同方法,都可以共享),生命週期和應用同步。二級快取的設定,首先是settings中的cacheEnabled要設定為true,當然預設的就是為true,這個步驟決定了在建立Executor物件的時候是否通過CachingExecutor來裝飾。前面原始碼中也有說明過;要想看二級快取效果,</cache>標籤要開啟
然後把一級快取配置關閉了,其實由於一級快取的作用域太小,在實際生產中用的也比較少
從上面可以發現第二次查詢就沒走資料庫查詢,說明二級快取生效了。接下來看下二級快取原始碼,其實在上面已經寫出來了,入口是factory.openSession();
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
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);
// 根據事務工廠和預設的執行器型別,建立執行器 >>執行SQL語句操作
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;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {//針對Statement做快取
executor = new ReuseExecutor(this, transaction);
} else {
// 預設 SimpleExecutor,每一次只是SQL操作都建立一個新的Statement物件
executor = new SimpleExecutor(this, transaction);
}
// 二級快取開關,settings 中的 cacheEnabled 預設是 true
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 植入外掛的邏輯,至此,四大物件已經全部攔截完畢;這裡面是一個攔截器鏈
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
從這裡可以看到如果判斷成立,那麼會對executor做一個裝飾;後面做查詢操作時就要從sqlSession.selectList("com.ghy.mapper.UserMapper.selectUserList");跟蹤起了
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
// 如果 cacheEnabled = true(預設),Executor會被 CachingExecutor裝飾
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
一樣,進入query方法看他是怎麼執行的
這裡要進的就是CachingExecutor裡面了,這裡面是二級快取的東西
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 獲取SQL
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 建立CacheKey:什麼樣的SQL是同一條SQL? >>
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
這裡面建立createCacheKey的過程和一級快取一樣,這裡就不想再寫一次了;
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
// cache 物件是在哪裡建立的? XMLMapperBuilder類 xmlconfigurationElement()
// 由 <cache> 標籤決定
if (cache != null) {
// flushCache="true" 清空一級二級快取 >>
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
// 獲取二級快取
// 快取通過 TransactionalCacheManager、TransactionalCache 管理
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 寫入二級快取
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 走到 SimpleExecutor | ReuseExecutor | BatchExecutor
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
這就是二級快取過程;
cache屬性詳解:
屬性 | 含義 | 取值 |
type |
快取實現類 |
需要實現Cache介面,預設是PerpetualCache,可以使用第三方快取 |
size |
最多快取物件個數 |
預設1024 |
eviction |
回收策略(快取淘汰演算法) |
LRU – 最近最少使用的:移除最長時間不被使用的物件(預設)。FIFO |
flushInterval |
定時自動清空快取間隔 |
自動重新整理時間,單位 ms,未配置時只有呼叫時重新整理 |
readOnly |
是否只讀 |
true:只讀快取;會給所有呼叫者返回快取物件的相同例項。因此這些 |
blocking |
啟用阻塞快取 |
通過在get/put方式中加鎖,保證只有一個執行緒操作快取,基於Java重入鎖實現 |
1.4.4 第三方快取
在實際開發的時候我們一般也很少使用MyBatis自帶的二級快取,這時我們會使用第三方的快取工具Ehcache獲取Redis來實現https://github.com/mybatis/redis-cache
新增依賴
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
然後加上Cache標籤的配置
<cache type="org.mybatis.caches.redis.RedisCache"
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
然後新增redis的屬性檔案
這樣快取就存入redis中了,至於怎麼讀到redis.properites檔案的,這個可以從原始碼中找下
從上面看到在構造方法中會做一些初始的操作,其中的JedisPool是操作連線去操作redis的;
public RedisConfig parseConfiguration() {
return parseConfiguration(getClass().getClassLoader());
}
從原始碼中可以發現他已經做好了redis連線配置檔案的預設命名了;