Mybatis中的多級緩存
一級緩存
一、Cachekey
要了解一級緩存,先要了解一個類:CacheKey,它是查詢條件的抽象封裝體,也就是說,判斷一個條件是否是之前查過的,那麼就是通過判斷Cachekey的相等性也就是Equals方法了
當下列特徵值相同時,我們認爲是相同的查詢。
- statementId
- 要求的查詢結果集的範圍(RowBounds的offset和limit)
- 傳給statement的sql語句
- 傳給statement的參數集
特徵值是通過update方法來添加到CacheKey對象內的
public void update(Object object) {
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
//特徵值數量
count++;
//特徵值的hashcode之和
checksum += baseHashCode;
baseHashCode *= count;
//hashcode = 原來的hashcode * 擴展因子(37) + 新特徵值的hashcode
hashcode = multiplier * hashcode + baseHashCode;
updateList.add(object);
}
可以看出,不同CacheKey對象的hashcode的碰撞率可以控制在一個極小的概率上。
@Override
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;
}
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;
}
二、PerpetualCache
PerpetualCache除了一個id外,保存緩存的屬性cache只是一個HashMap
private Map<Object, Object> cache = new HashMap<Object, Object>();
CachKey作爲map的key值,查詢結果作爲map的value值。 所有對緩存的操作實際上就是對HashMap的操作。
@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.");
}
//flush爲true,強制清除緩存
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
//從緩存中進行查找
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
//callable時的參數處理
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;
}
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;
}
那麼什麼時候會需要清除緩存呢?首先從上面的源碼看來,如果已經用數據庫查詢了,那麼就會把之前的緩存清除
其他情況是否還能出發緩存清除呢?
update方法(實際上delete、insert都會清除緩存)
@Override
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);
}
commit方法
@Override
public void commit(boolean required) throws SQLException {
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
clearLocalCache();
flushStatements();
if (required) {
transaction.commit();
}
}
close方法
@Override
public void close(boolean forceRollback) {
try {
try {
rollback(forceRollback);
} finally {
if (transaction != null) {
transaction.close();
}
}
} catch (SQLException e) {
// Ignore. There's nothing that can be done at this point.
log.warn("Unexpected exception on closing transaction. Cause: " + e);
} finally {
transaction = null;
deferredLoads = null;
localCache = null;
localOutputParameterCache = null;
closed = true;
}
}
三、一級緩存的性能
從前面的分析可以看到,一級緩存是一個粗粒度的緩存,設計的也比較簡單。僅僅只是一個HashMap,也沒有對HashMap的大小進行管理,也沒有緩存更新和過期的概念。
這是因爲一級緩存的生命週期很短,不會存活多長時間。
- 每次調用update方法(insert、delete、update的sql),都會將一級緩存清空。
- 一級緩存是SqlSession級別的,SqlSession一旦關閉,對應的一級緩存也就不會存在。(會話結束,事務提交或回滾)
- 可以通過BaseExecutor#clearLocalCache()手動清空緩存。
因此,一下情況時要控制好SqlSession的生存時間,必要時手動清除一級緩存
(1)對於數據的時效性準確性要求比較高的查詢,防止一級緩存的長時間存活導致髒數據的讀取。
(2)對於頻率且大數據量的查詢,防止一級緩存佔用的內存過大。