MyBatis是一個簡單,小巧但功能非常強大的ORM開源框架,它的功能強大也體現(xiàn)在它的緩存機制上。MyBatis提供了一級緩存、二級緩存 這兩個緩存機制,能夠很好地處理和維護緩存,以提高系統(tǒng)的性能。本文將介紹MyBatis的一級緩存,并深入源碼解析MyBatis一級緩存的實現(xiàn)原理。
什么是一級緩存?
每當我們使用MyBatis開啟一次和數(shù)據(jù)庫的會話,MyBatis會創(chuàng)建出一個SqlSession對象表示一次數(shù)據(jù)庫會話。
在對數(shù)據(jù)庫的一次會話中,我們有可能會反復地執(zhí)行完全相同的查詢語句,如果不采取一些措施的話,每一次查詢都會查詢一次數(shù)據(jù)庫,而我們在極短的時間內(nèi)做了完全相同的查詢,那么它們的結(jié)果極有可能完全相同,由于查詢一次數(shù)據(jù)庫的代價很大,這有可能造成很大的資源浪費。
為了解決這一問題,減少資源的浪費,MyBatis會在表示會話的SqlSession對象中建立一個簡單的緩存,將每次查詢到的結(jié)果結(jié)果緩存起來,當下次查詢的時候,如果判斷先前有個完全一樣的查詢,會直接從緩存中直接將結(jié)果取出,返回給用戶,不需要再進行一次數(shù)據(jù)庫查詢了。
如下圖所示,MyBatis會在一次會話的表示(一個SqlSession對象)中創(chuàng)建一個本地緩存(local cache),對于每一次查詢,都會嘗試根據(jù)查詢的條件去本地緩存中查找是否在緩存中,如果在緩存中,就直接從緩存中取出,然后返回給用戶;否則,從數(shù)據(jù)庫讀取數(shù)據(jù),將查詢結(jié)果存入緩存并返回給用戶。

對于會話(Session)級別的數(shù)據(jù)緩存,我們稱之為一級數(shù)據(jù)緩存,簡稱一級緩存。
一級緩存的實現(xiàn)原理
由于MyBatis使用SqlSession對象表示一次數(shù)據(jù)庫的會話,那么,對于會話級別的一級緩存也應該是在SqlSession中控制的。
實際上, MyBatis只是一個MyBatis對外的接口,SqlSession將它的工作交給了Executor執(zhí)行器這個角色來完成,負責完成對數(shù)據(jù)庫的各種操作。當創(chuàng)建了一個SqlSession對象時,MyBatis會為這個SqlSession對象創(chuàng)建一個新的Executor執(zhí)行器,而緩存信息就被維護在這個Executor執(zhí)行器中,MyBatis將緩存和對緩存相關(guān)的操作封裝成了Cache接口中。SqlSession、Executor、Cache之間的關(guān)系如下列類圖所示:

如上述的類圖所示,Executor接口的實現(xiàn)類BaseExecutor中擁有一個Cache接口的實現(xiàn)類PerpetualCache,則對于BaseExecutor對象而言,它將使用PerpetualCache對象維護緩存。
由于Session級別的一級緩存實際上就是使用PerpetualCache維護的,那么PerpetualCache是怎樣實現(xiàn)的呢?
PerpetualCache實現(xiàn)原理其實很簡單,其內(nèi)部就是通過一個簡單的HashMap<k,v> 來實現(xiàn)的,沒有其他的任何限制。如下是PerpetualCache的實現(xiàn)代碼:
public class PerpetualCache implements Cache {
private String id;
private Map<Object, Object> cache = new HashMap<Object, Object>();
public PerpetualCache(String id) {
this.id = id;
}
public String getId() {
return id;
}
public int getSize() {
return cache.size();
}
public void putObject(Object key, Object value) {
cache.put(key, value);
}
public Object getObject(Object key) {
return cache.get(key);
}
public Object removeObject(Object key) {
return cache.remove(key);
}
public void clear() {
cache.clear();
}
public ReadWriteLock getReadWriteLock() {
return null;
}
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;
return getId().equals(otherCache.getId());
}
public int hashCode() {
if (getId() == null) throw new CacheException("Cache instances require an ID.");
return getId().hashCode();
}
}
一級緩存的生命周期有多長?
a. MyBatis在開啟一個數(shù)據(jù)庫會話時,會 創(chuàng)建一個新的SqlSession對象,SqlSession對象中會有一個新的Executor對象,Executor對象中持有一個新的PerpetualCache對象;當會話結(jié)束時,SqlSession對象及其內(nèi)部的Executor對象還有PerpetualCache對象也一并釋放掉。
b. 如果SqlSession調(diào)用了close()方法,會釋放掉一級緩存PerpetualCache對象,一級緩存將不可用;
c. 如果SqlSession調(diào)用了clearCache(),會清空PerpetualCache對象中的數(shù)據(jù),但是該對象仍可使用;
d.SqlSession中執(zhí)行了任何一個update操作(update()、delete()、insert()) ,都會清空PerpetualCache對象的數(shù)據(jù),但是該對象可以繼續(xù)使用;

CacheKey的定義
我們知道,Cache最核心的實現(xiàn)其實就是一個Map,將本次查詢使用的特征值作為key,將查詢結(jié)果作為value存儲到Map中。
現(xiàn)在最核心的問題出現(xiàn)了:怎樣來確定一次查詢的特征值?
換句話說就是:怎樣判斷某兩次查詢是完全相同的查詢?
也可以這樣說:如何確定Cache中的key值?
MyBatis認為,對于兩次查詢,如果以下條件都完全一樣,那么就認為它們是完全相同的兩次查詢:
傳入的 statementId
查詢時要求的結(jié)果集中的結(jié)果范圍 (結(jié)果的范圍通過rowBounds.offset和rowBounds.limit表示);
這次查詢所產(chǎn)生的最終要傳遞給JDBC java.sql.Preparedstatement的Sql語句字符串(boundSql.getSql() )
傳遞給java.sql.Statement要設(shè)置的參數(shù)值
綜上所述,CacheKey由以下條件決定:statementId + rowBounds + 傳遞給JDBC的SQL + 傳遞給JDBC的參數(shù)值