MyBatis框架原理3:快取
上一篇[MyBatis框架原理2:SqlSession執行過程][1]介紹了MyBatis的工作流程,其中涉及到了MyBatis快取的使用,首先回顧一下工作流程圖:
如果開啟了二級快取,資料查詢執行過程就是首先從二級快取中查詢,如果未命中則從一級快取中查詢,如果也未命中則從資料庫中查詢。MyBatis的一級和二級快取都是基於Cache介面的實現,下面先來看看Cache介面和其各種實現類。
Cache介面及常用裝飾器
public interface Cache { String getId(); //快取中新增資料,key為生成的CacheKey,value為查詢結果 void putObject(Object key, Object value); //查詢 Object getObject(Object key); //刪除 Object removeObject(Object key); //清空快取 void clear(); //獲取快取數量 int getSize(); //獲取讀寫鎖 ReadWriteLock getReadWriteLock(); }
Cache介面位於MyBatis的cache包下,定義了快取的基本方法,其實現類採用了裝飾器模式,通過實現類的組裝,可以實現操控快取的功能。cache包結構如下:
- PerpetualCache是Cache介面的實現類,通過內部的HashMap來對快取進行基本的操作,通常配合裝飾器類一起使用。
- BlockingCache裝飾器:保證只有一個執行緒到資料庫中查詢指定key的資料,如果該執行緒在BlockingCache中未查詢到資料,就獲取key對應的鎖,阻塞其他查詢這個key的執行緒,通過其內部ConcurrentHashMap來實現,原始碼如下:
public class BlockingCache implements Cache { //阻塞時長 private long timeout; private final Cache delegate; //key和ReentrantLock物件一一對應 private final ConcurrentHashMap<Object, ReentrantLock> locks; @Override public Object getObject(Object key) { //獲取key的鎖 acquireLock(key); //根據key查詢 Object value = delegate.getObject(key); //如果命中快取,釋放鎖,未命中則繼續持有鎖 if (value != null) { releaseLock(key); } return value; } @Override //從資料庫獲取結果後,將結果放入BlockingCache,然後釋放鎖 public void putObject(Object key, Object value) { try { delegate.putObject(key, value); } finally { releaseLock(key); } } ...
- FifoCache裝飾器: 先入先出規則刪除最早的快取,通過其內部的Deque實現。
- LruCache裝飾器: 刪除最近使用最少的快取, 通過內部的LinkedHashMap實現。
- SynchronizedCache裝飾器:同步Cache。
- LoggingCache裝飾器: 提供日誌功能,記錄和輸出快取命中率。
- SerializedCache裝飾器:序列化功能。
CacheKey
CacheKey物件是用來確認快取項的唯一標識,由其內部ArrayList新增的所有物件來確認兩個CacheKey是否相同,通常ArrayList內將新增MappedStatement的id,SQL語句,使用者傳遞給SQL語句的引數以及查詢結果集範圍RowBounds等,CacheKey原始碼如下:
public class CacheKey implements Cloneable, Serializable {
...
private final int multiplier;
private int hashcode;
private long checksum;
private int count;
private List<Object> updateList;
public CacheKey() {
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLYER;
this.count = 0;
this.updateList = new ArrayList<Object>();
}
//向updateLis中新增物件
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);
}
@Override
//重寫equals方法判斷CacheKey是否相同
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof CacheKey)) {
return false;
}
final CacheKey cacheKey = (CacheKey) object;
if (hashcode != cacheKey.hashcode) {
return false;
}
if (checksum != cacheKey.checksum) {
return false;
}
if (count != cacheKey.count) {
return false;
}
//比較updateList中每一項
for (int i = 0; i < updateList.size(); i++) {
Object thisObject = updateList.get(i);
Object thatObject = cacheKey.updateList.get(i);
if (!ArrayUtil.equals(thisObject, thatObject)) {
return false;
}
}
return true;
}
}
一級快取
一級快取是session級別快取,只存在當前會話中,在沒有任何配置下,MyBatis預設開啟一級快取,當一個SqlSession第一次執行SQL語句和引數查詢時,將生成的CacheKey和查詢結果放入快取中,下一次通過相同的SQL語句和引數查詢時,就會從快取中獲取,當進行更新或者插入操作時,一級快取會進行清空。在上一篇中說到,MayBatis進行一級快取查詢和寫入是由BaseExecutor執行的,原始碼如下:
- 初始化快取: 一級快取是Cache介面的PerpetualCache實現類物件
public abstract class BaseExecutor implements Executor {
...
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<DeferredLoad>();
//一級快取初始化
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
...
- 生成CacheKey key在CachingExecutor中生成,CacheKey的updateList中放入了MappedStatement,傳入SQL的引數,結果集範圍rowBounds和boundSql:
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);
}
- 將查詢結果和CacheKey放入快取:
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
//快取中放入CacheKey和佔位符
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
//在資料庫中查詢操作
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
//快取中放入CacheKey和結果集
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
//返回結果
return list;
}
- 再次執行相同查詢條件時從快取獲取結果:
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();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
- 更新操作時清空快取:
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
//清空快取
clearLocalCache();
return doUpdate(ms, parameter);
}
通過以下程式碼驗證下,分別開兩個session進行相同的查詢,第一個session查詢兩次:
public void testSelect() {
SqlSession sqlSession = sqlSessionFactory.openSession();
User user = sqlSession.selectOne("findUserById", 1);
System.out.println(user);
User user2 = sqlSession.selectOne("findUserById", 1);
System.out.println(user2);
sqlSession.close();
System.out.println("sqlSession closed!===================================");
//新建會話
SqlSession sqlSession2 = sqlSessionFactory.openSession();
User user3 = sqlSession2.selectOne("findUserById", 1);
System.out.println(user3);
sqlSession2.close();
}
把日誌設定為DEBUG級別得到執行日誌:
DEBUG [main] - ==> Preparing: SELECT * FROM user WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
DEBUG [main] - Resetting autocommit to true on JDBC Connection [[email protected]]
DEBUG [main] - Closing JDBC Connection [[email protected]]
DEBUG [main] - Returned connection 369241501 to pool.
sqlSession closed!===================================
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Checked out connection 369241501 from pool.
DEBUG [main] - Setting autocommit to false on JDBC Connection [[email protected]]
DEBUG [main] - ==> Preparing: SELECT * FROM user WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
DEBUG [main] - Resetting autocommit to true on JDBC Connection [[email protected]]
DEBUG [main] - Closing JDBC Connection [[email protected]]
DEBUG [main] - Returned connection 369241501 to pool.
第一次會話中,雖然查詢了兩次id為1的使用者,但是隻執行了一次SQL,關閉會話後開啟一次新的會話,再次查詢id為1的使用者,SQL再次執行,說明了一級快取只存在SqlSession中,不同SqlSession不能共享。
二級快取
二級快取是Mapper級別快取,也就是同一Mapper下不同的session共享二級快取區域。只需要在XML對映檔案中增加
- blocking : 是否使用阻塞快取
- readOnly : 是否只讀
- eviction: 快取策略,可指定Cache介面下裝飾器類FifoCache、LruCache、SoftCache和WeakCache
- flushInterval : 自動重新整理快取時間
- size : 設定快取個數
- type : 設定快取型別,用於自定義快取類,預設為PerpetualCache
二級快取是在MyBatis的解析配置檔案時初始化,在XMLMapperBuilder中將快取配置解析:
private void cacheElement(XNode context) throws Exception {
if (context != null) {
//指定預設型別為PerpetualCache
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
//預設快取策略為LruCache
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構建二級快取
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)
//設定快取型別,預設為PerpetualCache
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
//設定快取策略,預設使用LruCache裝飾器
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
//設定重新整理時間
.clearInterval(flushInterval)
//設定大小
.size(size)
//設定是否只讀
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
configuration.addCache(cache);
currentCache = cache;
return cache;
}
最終得到預設的二級快取物件結構為:
CachingExecutor將初始化的Cache物件用TransactionalCache包裝後放入TransactionalCacheManager的Map中,下面程式碼中的tmc就是TransactionalCacheManager物件,CachingExecutor執行二級快取操作過程:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//從Configuration的MappedStatement中獲取二級快取
Cache cache = ms.getCache();
if (cache != null) {
//判斷是否需要重新整理快取,SELECT不重新整理,INSERT|UPDATE|DELETE重新整理快取
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
//從二級快取中獲取資料
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
//委託BaseExecutor查詢
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//查詢結果放入二級快取
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
通過之前一級快取的例子驗證二級快取,只需要在UserMapper對映檔案中加入
DEBUG [main] - ==> Preparing: SELECT * FROM user WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
DEBUG [main] - Cache Hit Ratio [com.kkb.mybatis.mapper.UserMapper]: 0.0
User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
DEBUG [main] - Resetting autocommit to true on JDBC Connection [[email protected]]
DEBUG [main] - Closing JDBC Connection [[email protected]]
DEBUG [main] - Returned connection 1543974463 to pool.
sqlSession closed!===================================
DEBUG [main] - Cache Hit Ratio [com.kkb.mybatis.mapper.UserMapper]: 0.3333333333333333
User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
不同session查詢同一條記錄時,總共只執行了一次SQL語句,這時候不同session已經共享了二級快取區域。