【Mybatis原始碼】一級快取
參考:
Mybatis一級快取配置:
<setting name="localCacheScope" value="SESSION"/>
value有兩個值可選:
session:快取對一次會話中所有的執行語句有效,也就是SqlSession級別的。
statement:快取只對當前執行的這一個Statement有效。
在一級快取中對快取的查詢和寫入是在Executor中完成的,以BaseExecutor為例,檢視query方法:
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; ...... @Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); //構建CacheKey CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql);//呼叫了下面的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."); } //如果queryStack為0或者並且有必要重新整理快取 if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache();//清空本地快取 } List<E> list; try { queryStack++; //從快取中獲取資料,key的型別為CacheKey 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(); //如果是STATEMENT級別的快取 if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // 清空快取 clearLocalCache(); } } return list; } }
(1)從BaseExecutor的成員變數中,可以看到有一個型別為PerpetualCache變數名為localCache的欄位,快取就是用它來實現的。PerpetualCache類的成員變數也很簡單,包含一個id和一個HashMap,快取資料就儲存在HashMap中。
public class PerpetualCache implements Cache { private final String id; private Map<Object, Object> cache = new HashMap<Object, Object>();//使用一個map做儲存 get set方法省略 ...... }
(2)在BaseExecutor的quey方法中,有一個構建CacheKey的語句,既然快取資料儲存在HashMap中,那麼資料格式一定是鍵值對的形式,這個CacheKey就是HashMap中的key,value是資料庫返回的資料。
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
(3)第二個query方法中,當執行查詢時,首先通過localCache.getObject(key)從快取中獲取資料,如果獲取的資料為空,再從資料庫中查詢。
//從快取中獲取資料,key的型別為CacheKey list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
//如果獲取結果為空,從資料庫中查詢
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
(4)如果開啟了flushcache,將會清空快取
//如果queryStack為0或者並且有必要重新整理快取
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();//清空本地快取
}
配置flushcache:
<select id="getStudent" parameterType="String" flushCache="true">
……
</select>
(5)如果一級快取的級別為Statement,將會清空快取,這也是如果設定一級快取的級別為Statement時快取只對當前執行的這一個Statement有效的原因。
//如果是STATEMENT級別的快取
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// 清空快取
clearLocalCache();
}
配置:
<setting name="localCacheScope" value="STATEMENT"/>
CacheKey如何生存的
(1)在query方法中,呼叫了createCacheKey方法生成CacheKey,然後多次呼叫了cachekey的update方法,將標籤的ID、分頁資訊、SQL語句、引數等資訊作為引數傳入:
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
//設定ID,也就是標籤所在的Mapper的namespace + 標籤的id
cacheKey.update(ms.getId());
//偏移量,Mybatis自帶分頁類RowBounds中的屬性
cacheKey.update(rowBounds.getOffset());
//每次查詢大小,同樣是Mybatis自帶分頁類RowBounds中的屬性
cacheKey.update(rowBounds.getLimit());
//標籤中定義的SQL語句
cacheKey.update(boundSql.getSql());
//獲取引數
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// 處理SQL中的引數
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) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
(2)通過原始碼,看一下CacheKey的update方法,update方法中記錄了呼叫update傳入引數的次數、每個傳入引數的hashcode之和checksum、以及計算CacheKey的成員變數hashcode的值。
public class CacheKey implements Cloneable, Serializable {
private static final long serialVersionUID = 1146682552656046210L;
public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();
private static final int DEFAULT_MULTIPLYER = 37;
private static final int DEFAULT_HASHCODE = 17;
private final int multiplier;//一個乘數
private int hashcode;//hashcode
private long checksum;//update方法中傳入引數的hashcode之和
private int count; //呼叫update方法向updatelist新增引數的的次數
private List<Object> updateList;//呼叫update傳入的引數會被放到updateList
public CacheKey() {
//初始化
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLYER;
this.count = 0;
this.updateList = new ArrayList<Object>();
}
public CacheKey(Object[] objects) {
this();
updateAll(objects);
}
public int getUpdateCount() {
return updateList.size();
}
public void update(Object object) {
//獲取引數的hash值
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
//計數
count++;
//hash值累加
checksum += baseHashCode;
//更新hash,hash=hash*count
baseHashCode *= count;
//計算CacheKey的hashcode
hashcode = multiplier * hashcode + baseHashCode;
//將引數新增到updateList
updateList.add(object);
}
......
}
(3)CacheKey中的成員變數的作用是什麼呢,接下來看一下它的equals方法,CacheKey中重寫了equals方法,CacheKey中的成員變數其實就是為了判斷兩個CacheKey的例項是否相同:
如果滿足以下條件,兩個CacheKey將判為不相同:
1. 要比較的物件不是CacheKey的例項
2. CacheKey物件中的hashcode不相同、count不相同、checksum不相同(它們之間是或的關係)
3. CacheKey物件的updateList成員變數不相同
總結:
如果Statement Id + Offset + Limmit + Sql + Params 都相同將被認為是相同的SQL,第一次將CacheKey作為HashMap中的key,資料庫返回的資料作為value放入到集合中,第二次查詢時由於被認為是相同的SQL,HashMap中已經存在該SQL的CacheKey物件,可直接從localCache中獲取資料來實現mybatis的一級快取。
@Override
public boolean equals(Object object) {
//如果物件為空
if (this == object) {
return true;
}
//如果不是CacheKey的例項
if (!(object instanceof CacheKey)) {
return false;
}
final CacheKey cacheKey = (CacheKey) object;
//如果hashcode值不相同
if (hashcode != cacheKey.hashcode) {
return false;
}
// 如果checksum不相同
if (checksum != cacheKey.checksum) {
return false;
}
//如果count不相同
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);
//如果有不相同的,返回false
if (!ArrayUtil.equals(thisObject, thatObject)) {
return false;
}
}
return true;
}
總結:
(1)mybatis的一級快取是SqlSession級別的,不同的SqlSession不共享快取;
(2)mybatis一級快取是通過HashMap實現的,在PerpetualCache中定義,沒有容量控制;
(3)分散式環境下使用一級快取,資料庫寫操作會引起髒資料問題;