MyBatis的Cache機制解析
1. MyBatis快取機制的核心構件
1.1 Cache介面
介面簡單明瞭,Cache的基本操作;put/get/remove/clear。
public interface Cache {
String getId();//分組ID
void putObject(Object key, Object value);//put
Object getObject(Object key);//get
Object removeObject(Object key);//remove
void clear();//clear
int getSize();//可選 core不再使用
ReadWriteLock getReadWriteLock();//3.2.6後已廢棄
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
1.2 Cache的基本實現:PerpetualCache
通過HashMap實現快取管理。
public class PerpetualCache implements Cache {
private String id;//ID進行分組
private Map<Object, Object> cache = new HashMap<Object, Object>();//資料快取
//...實現介面,就是對HashMap的操作
}
- 1
- 2
- 3
- 4
- 5
1.3 CacheKey的構成
核心功能,可以動態更新Key的hash值。因為底層快取資料是基於HashMap實現的,在比對value|key是否存在時,會呼叫hashCode方法和equals。
CacheKey覆寫了hashcode和equals。
比較順序:hashCode–>checksum–>count–>updateList,只要有一個不等則說明不是相同的Key。
public class CacheKey implements Cloneable, Serializable {
//...
private static final int DEFAULT_MULTIPLYER = 37;
private static final int DEFAULT_HASHCODE = 17;
private 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>();
}
//...
//hashcode的計算方法
private void doUpdate(Object object) {
int baseHashCode = object == null ? 1 : object.hashCode();
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;//hashcode的演算法
updateList.add(object);
}
//...
//比較2個CacheKey是否相等
@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 (thisObject == null) {
if (thatObject != null) {
return false;
}
} else {
if (!thisObject.equals(thatObject)) {
return false;
}
}
}
return true;
}
//...
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
1.4 MyBatis中CacheKey的建立
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
//...
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
//...
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
//...
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
Key的構成:cacheKey=ID + offset + limit + sql + parameterValues + environmentId
2. MyBatis的一級快取
一級快取,即本地快取,永久快取,Mybatis自行控制快取的讀寫刪,在執行器(BaseExecutor)中使用,執行器是在SqlSessionFactory.openSession()後建立的SqlSession中執行SQL語句時建立的,所以一級快取的生命週期等同於SqlSession,也就是說是Session級的快取。
2.1 一級快取的建立、清空和銷燬
MyBatis在commit和rollback時都會清空一級快取,在SqlSession關閉時也會主動清空快取,可以被GC掉。
protected BaseExecutor(Configuration configuration, Transaction transaction) {
//...
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
//...
}
@Override
public void close(boolean forceRollback) {
try {
//...
} finally {
//...
localCache = null;
localOutputParameterCache = null;
//...
}
}
@Override
public void commit(boolean required) throws SQLException {
//...
clearLocalCache();
//...
if (required) {
transaction.commit();
}
}
@Override
public void rollback(boolean required) throws SQLException {
if (!closed) {
try {
clearLocalCache();
//...
} finally {
if (required) {
transaction.rollback();
}
}
}
}
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
2.2 一級快取的重新整理
快取必然是針對Select查詢操作提高查詢效率設計的,在Mapper XML的配置檔案中可以設定 flushCache=”ture”(預設為false)來重新整理一級快取(也包括二級快取),任何時候只要語句被呼叫,都會清空一級快取和二級快取。
2.3 一級快取的配置
mybatis-config.xml
<settings>
//...
<setting name="localCacheScope" value="SESSION | STATEMENT"/>
//...
</settings>
- 1
- 2
- 3
- 4
- 5
localCacheScope預設為SESSION,快取一個會話中執行的所有的查詢結果,如果設定為STATEMENT,則在同一個會話中的不同的呼叫將不會共享資料,在呼叫完畢後會清理一級快取。
//...
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
//...
- 1
- 2
- 3
- 4
- 5
- 6
2.4 巢狀查詢的優化
MyBatis利用一級快取機制加速重複巢狀查詢。
private static class DeferredLoad {
//...
public boolean canLoad() {
return localCache.getObject(key) != null && localCache.getObject(key) != EXECUTION_PLACEHOLDER;
}
public void load() {
//...
List<Object> list = (List<Object>) localCache.getObject(key);
//...
}
}
//...
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
3. MyBatis的二級快取
實現 org.mybatis.cache.Cache 介面都可以作為MyBatis的二級快取;二級快取的Cache物件由Configuration進行管理,而
每次構建SqlSessionFactory物件時都會建立新的Configuration物件,因此,二級快取的生命週期與SqlSessionFactory是相同的。基於Mapper XML 配置,在建立每個MapperedStatement物件時,都會根據其所屬的namespace名稱空間,給其分配Cache快取例項。
二級快取由CachingExecutor負責管理維護,開啟了二級快取後,Executor將使用CacheExecutor,基於裝飾器模式對配置的Executor進行包裝,擴充套件了快取功能。
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
//...
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
//...
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
3.1 二級快取的配置
mybatis-config.xml配置,false則不啟用二級快取,ture則自動使用CachingExecutor。
<settings>
//...
<setting name="cacheEnabled" value="true|false" />
//...
</settings>
- 1
- 2
- 3
- 4
- 5
Mapper XML配置
xxx.mapper.xml
<cache
type="PERPETUAL"//
eviction="LRU"//演算法
flushInterval="60000"//重新整理間隔,間隔60秒清空快取,被動觸發非定時器輪詢
size="512"//大小
readOnly="false"//true:返回cache結果的克隆物件
blocking="false"
/>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
根據快取器配置建立Cache例項,以namespace為CacheId,一個namespace對應一個Cache例項。
可以通過實現 org.mybatis.cache.Cache 介面建立自定義的快取器,比如基於Redis或者Memcached實現分散式快取。
3.2 MyBatis內建的二級快取演算法
Mybatis的所有Cache演算法都是基於裝飾器模式對PerpetualCache擴充套件增加功能。
FIFO:先入先出,基於LinkedList實現;
LRU:最近最少使用,基於LinkedHashMap實現,在put的時候,自動移除最少使用快取物件;
SOFT:對Cache的value進行SoftReference包裝;當快取物件是Soft reference可達時,gc會向系統申請更多記憶體,而不是直接回收它,當記憶體不足的時候才回收它;
WEAK:對Cache的value進行WeakReference包裝;WeakReference不會強制物件儲存在記憶體中。它擁有比較短暫的生命週期,允許你使用垃圾回收器的能力去權衡一個物件的可達性。在垃圾回收器掃描它所管轄的記憶體區域過程中,一旦gc發現物件是weakReference可達,就會把它放到ReferenceQueue中,等下次gc時回收它。
Mybatis Cache引數
- flushInterval(重新整理間隔)可以被設定為任意的正整數,而且它們代表一個合理的毫秒形式的時間段。預設情況是不設定,也就是沒有重新整理間隔,快取僅僅呼叫語句時重新整理。
- size(引用數目)可以被設定為任意正整數,要記住你快取的物件數目和你執行環境的可用記憶體資源數目。預設值是1024。
- readOnly(只讀)屬性可以被設定為true或false。只讀的快取會給所有呼叫者返回快取物件的相同例項。因此這些物件不能被修改。這提供了很重要的效能優勢。可讀寫的快取會返回快取物件的拷貝(通過序列化)。這會慢一些,但是安全,因此預設是false。
如下例子:
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
- 1
這個更高階的配置建立了一個 FIFO 快取,並每隔 60 秒重新整理,存數結果物件或列表的 512 個引用,而且返回的物件被認為是隻讀的,因此在不同執行緒中的呼叫者之間修改它們會導致衝突。
可用的收回策略有, 預設的是 LRU:
1.LRU – 最近最少使用的:移除最長時間不被使用的物件。
2.FIFO – 先進先出:按物件進入快取的順序來移除它們。
3.SOFT – 軟引用:移除基於垃圾回收器狀態和軟引用規則的物件。
4.WEAK – 弱引用:更積極地移除基於垃圾收集器狀態和弱引用規則的物件。
- 1
- 2
- 3
- 4