爲了提高應用程序性能,一種比較通用的方法是使用緩存技術來減少與數據庫之間的交互。緩存技術是一種“以空間換時間”的設計理念,利用內存空間資源來提高數據檢索速度的有效手段之一。
iBATIS以一種簡單、易用、靈活的方式實現了數據緩存。下面,首先看一下iBATIS關於緩存部分的核心類圖:
關於這些類的用途,在註釋中做了比較概括性的說明,下面就來仔細的講一下這些類的用途以及它們是如何工作的。
在iBATIS中,可以配置多個緩存,每個cacheModel的配置對應一個CacheModel類的一個對象。其中包括id等配置信息。iBATIS通過這些配置信息來定義緩存管理的行爲。
緩存的目的是爲了能夠實現數據的高速檢索。在程序中,數據是用對象表示的;爲了能夠檢索到以緩存的數據對象,每個數據對象必須擁有一個唯一標識,在iBATIS中,這個唯一標識用CacheKey來表示。
那麼,緩存的數據保存到什麼地方了呢?如何實現數據的快速檢索呢?答案在CacheController的實現類中。每個CacheController中都有一個Map類型的屬性cache來保存被緩存的數據,其中key爲CacheKey類型,value爲Object類型;需要關注的是CacheKey對象的hashCode的生成算法,每次調用CacheKey對象的update方法時,都會更新它的hashCode值,關於hashCode值的計算方法後續在給出詳細說明。
在擁有了數據緩存區後,就可以向其中存放數據和檢索數據了。在iBATIS中,有多種的緩存管理策略,也可以自定義緩存管理策略。
關於緩存的功能,主要有兩種類型:一種是對外提供的功能:數據存儲和數據檢索;另外一種是內部管理的功能:緩存對象標識的生成,緩存區刷新,數據檢索算法等。下面就逐一介紹這些功能的代碼實現。
1. 數據存儲
首先看一下CacheModel中的putObject方法是如何實現的
- public void putObject(CacheKey key, Object value) {
- if (null == value) value = NULL_OBJECT;
- //關於緩存的操作,需要互斥
- synchronized ( this ) {
- if (serialize && !readOnly && value != NULL_OBJECT) {
- //需要序列化,並且非只讀,則需要將緩存對象序列化到內存,以供後續檢索使用
- //readOnly爲false時,不能直接將對象引用直接返回個客戶程序
- try {
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
- ObjectOutputStream oos = new ObjectOutputStream(bos);
- oos.writeObject(value);
- oos.flush();
- oos.close();
- value = bos.toByteArray();
- } catch (IOException e) {
- throw new RuntimeException("Error caching serializable object. Cause: " + e, e);
- }
- }
- //如果執行了內存序列化,則保存的是它的字節數組
- controller.putObject(this, key, value);
- if ( log.isDebugEnabled() ) {
- log("stored object", true, value);
- }
- }
- }
因爲真正緩存數據對象的地方是在CacheController中,所以CacheModel的putObject方法中會調用CacheController的putObject方法執行真正的數據存儲。由於不同的CacheController實現的緩存管理方式不同,所以putObject實現也各不相同。下面分別介紹不同的CacheController實現的putObject方法
1) FifoCacheController
- public void putObject(CacheModel cacheModel, Object key, Object value) {
- //保存到Map中
- cache.put(key, value);
- //保存key到keyList
- keyList.add(key);
- //如果當前key的數量大於緩存容量時,移除keyList和cache中的第一個元素,達到先進先出的目的
- if (keyList.size() > cacheSize) {
- try {
- Object oldestKey = keyList.remove(0);
- cache.remove(oldestKey);
- } catch (IndexOutOfBoundsException e) {
- //ignore
- }
- }
- }
2)LruCacheController
- public void putObject(CacheModel cacheModel, Object key, Object value) {
- cache.put(key, value);
- keyList.add(key);
- if (keyList.size() > cacheSize) {
- try {
- //取得keyList中的第一個元素作爲最近最少用的key,爲什麼呢?
- //這個問題等到講解它的getObject方法時別會知曉
- Object oldestKey = keyList.remove(0);
- cache.remove(oldestKey);
- } catch (IndexOutOfBoundsException e) {
- //ignore
- }
- }
- }
3)MemoryCacheController
- public void putObject(CacheModel cacheModel, Object key, Object value) {
- Object reference = null;
- //根據配置創建響應的引用類型,此種緩存管理方式完全交給jvm的垃圾回收器來管理
- //創建好引用後,將數據對象放入到引用中
- if (referenceType.equals(MemoryCacheLevel.WEAK)) {
- reference = new WeakReference(value);
- } else if (referenceType.equals(MemoryCacheLevel.SOFT)) {
- reference = new SoftReference(value);
- } else if (referenceType.equals(MemoryCacheLevel.STRONG)) {
- reference = new StrongReference(value);
- }
- //在緩存中保存引用
- cache.put(key, reference);
- }
4)OSCacheController
這個緩存管理使用了OSCache來管理緩存,這裏就不做仔細的介紹了。
2. 數據檢索
在數據被放置到緩存區中以後,程序需要根據一定的條件進行數據檢索。首先看一下CacheModel類的getObject方法是如何檢索數據的
- public Object getObject(CacheKey key) {
- Object value = null;
- //互斥訪問緩衝區
- synchronized (this) {
- if (flushInterval != NO_FLUSH_INTERVAL
- && System.currentTimeMillis() - lastFlush > flushInterval) {
- //如果到了定期刷新緩衝區時,則執行刷新
- flush();
- }
- //根據key來從CacheController中取得數據對象
- value = controller.getObject(this, key);
- if (serialize && !readOnly &&
- (value != NULL_OBJECT && value != null)) {
- //如果需要序列化,並且非只讀,則從內存中序列化出一個數據對象的副本
- try {
- ByteArrayInputStream bis = new ByteArrayInputStream((byte[]) value);
- ObjectInputStream ois = new ObjectInputStream(bis);
- value = ois.readObject();
- ois.close();
- } catch (Exception e) {
- throw new RuntimeException("Error caching serializable object. Be sure you're not attempting to use " +
- "a serialized cache for an object that may be taking advantage of lazy loading. Cause: " + e, e);
- }
- }
- //下面的兩個操作是用來計算緩存區數據檢索的命中率的
- //對於緩衝區的數據檢索請求加一操作
- requests++;
- //如果檢索到數據,則命中數加一
- if (value != null) {
- hits++;
- }
- if ( log.isDebugEnabled() ) {
- if ( value != null ) {
- log("retrieved object", true, value);
- }
- else {
- log("cache miss", false, null);
- }
- }
- }
- return value;
- }
真正的數據檢索操作是在CacheController的實現類中進行的,下面就分別來看一下各個實現類是如何檢索數據的。
1) FifoCacheController
- public Object getObject(CacheModel cacheModel, Object key) {
- //直接從Map中取得
- return cache.get(key);
- }
2) LruCacheController
- public Object getObject(CacheModel cacheModel, Object key) {
- Object result = cache.get(key);
- //因爲這個key被使用了,如果檢索到了數據,則將其移除並重新放置到隊尾
- //這樣的目的就是保持最近使用的key放在隊尾,而對頭爲最近未使用的
- //如果沒有檢索到對象,則直接將該key移除
- keyList.remove(key);
- if (result != null) {
- keyList.add(key);
- }
- return result;
- }
3) MemoryCacheController
- public Object getObject(CacheModel cacheModel, Object key) {
- Object value = null;
- //取得引用對象
- Object ref = cache.get(key);
- if (ref != null) {
- //從引用對象中取得數據對象
- if (ref instanceof StrongReference) {
- value = ((StrongReference) ref).get();
- } else if (ref instanceof SoftReference) {
- value = ((SoftReference) ref).get();
- } else if (ref instanceof WeakReference) {
- value = ((WeakReference) ref).get();
- }
- }
- return value;
- }
3 唯一標識的生成
在iBATIS中,用CacheKey來標識一個緩存對象,而CacheKey通常是作爲Map中的key存在,所以CacheKey的hashCode的計算方法異常重要。影響hashCode的值有很多方面的因素,對每一個影響hashCode的元素,都需要調用CacheKey的update方法來重新計算hashCode值。下面我們就來看一下CacheKey的創建以及計算的相關過程。
首先CacheKey是在BaseDataExchange類的getCacheKey方法中被創建的。
- public CacheKey getCacheKey(StatementScope statementScope, ParameterMap parameterMap, Object parameterObject) {
- CacheKey key = new CacheKey();
- //取得parameterObject中的數據,這個parameterObject就是客戶端傳遞過來的參數對象
- Object[] data = getData(statementScope, parameterMap, parameterObject);
- //根據parameterObject中的數據去重計算hashCode
- for (int i = 0; i < data.length; i++) {
- if (data[i] != null) {
- key.update(data[i]);
- }
- }
- return key;
- }
這個方法被MappedStatement中的getCacheKey調用
- public CacheKey getCacheKey(StatementScope statementScope, Object parameterObject) {
- Sql sql = statementScope.getSql();
- ParameterMap pmap = sql.getParameterMap(statementScope, parameterObject);
- CacheKey cacheKey = pmap.getCacheKey(statementScope, parameterObject);
- //statement id對hashCode有影響
- cacheKey.update(id);
- cacheKey.update(baseCacheKey);
- //sql語句對hashCode有影響
- cacheKey.update(sql.getSql(statementScope, parameterObject)); //Fixes bug 953001
- return cacheKey;
- }
真正需要CacheKey對象的地方是在CacheStatement類中
- public CacheKey getCacheKey(StatementScope statementScope, Object parameterObject) {
- CacheKey key = statement.getCacheKey(statementScope, parameterObject);
- //如果不可讀並且不被序列化,那麼當前的SessionScope也對hashCode有影響
- //而真正起作用的是SessionScope的id屬性
- //也就是說這個緩存與調用線程的會話有關,當前線程所存儲的數據不能被其他線程使用
- if (!cacheModel.isReadOnly() && !cacheModel.isSerialize()) {
- key.update(statementScope.getSession());
- }
- return key;
- }
經過上述一系列的getCacheKey調用,將對CacheKey有影響的因素施加給了hashCode。其中對CacheKey的hashCode起影響作用的因素主要有:baseCacheKey,sql語句,參數值,statement id。可能產生影響的因素是session id。
現在我們知道了決定CacheKey的相關因素,也就知道了iBATIS是如何唯一的確定一個緩存對象。
經過以上的代碼分析,可以掌握iBatis如何生成CacheKey對象和計算其hashCode值,以及存儲和檢索數據對象。這些正是iBATIS緩存的基礎,掌握了這些實現原理,有助於我們更高效的使用iBATIS緩存功能,或者是開發自己的緩存系統。