Mybatis緩存機制

Mybatis緩存機制深刻解析
正如大多數持久層框架同樣,MyBatis 一樣提供了一級緩存和二級緩存的支持;
一級緩存基於 PerpetualCache 的 HashMap 本地緩存,其存儲做用域爲 Session,當 Session flush 或 close 以後,該Session中的全部 Cache 就將清空。
二級緩存與一級緩存其機制相同,默認也是採用 PerpetualCache,HashMap存儲,不一樣在於其存儲做用域爲 Mapper(Namespace),而且可自定義存儲源,如 Ehcache、Hazelcast等。
對於緩存數據更新機制,當某一個做用域(一級緩存Session/二級緩存Namespaces)的進行了 C/U/D 操做後,默認該做用域下全部 select 中的緩存將被clear。
MyBatis 的緩存採用了delegate機制 及 裝飾器模式設計,當put、get、remove時,其中會通過多層 delegate cache 處理,其Cache類別有:BaseCache(基礎緩 存)、EvictionCache(排除算法緩存) 、DecoratorCache(裝飾器緩存)、BaseCache:爲緩存數據最終存儲的處理類,默認爲 PerpetualCache,基於Map存儲;可自定義存儲處理,如基於EhCache、Memcached等;
EvictionCache :當緩存數量達到必定大小後,將經過算法對緩存數據進行清除。默認採用 Lru 算法(LruCache),提供有 fifo 算法(FifoCache)等;
DecoratorCache:緩存put/get處理先後的裝飾器,如使用 LoggingCache 輸出緩存命中日誌信息、使用 SerializedCache 對 Cache的數據 put或get 進行序列化 及反序列化處理、當設置flushInterval(默認1/h)後,則使用 ScheduledCache 對緩存數據進行定時刷新等。
通常緩存框架的數據結構基本上都是 Key-Value 方式存儲,MyBatis 對於其 Key 的生成採起規則爲:[hashcode : checksum : mappedStementId : offset : limit : executeSql : queryParams]。
對於併發 Read/Write 時緩存數據的同步問題,MyBatis 默認基於 JDK/concurrent中的ReadWriteLock,使用ReentrantReadWriteLock 的實現,從而經過 Lock 機制防止在併發 Write Cache 過程當中線程安全問題。
源碼剖解 :java

接下來將結合 MyBatis 序列圖進行源碼分析。在分析其Cache前,先看看其整個處理過程。

執行過程: 算法

① 一般狀況下,咱們須要在 Service 層調用 Mapper Interface 中的方法實現對數據庫的操做,上述根據產品 ID 獲取 Product 對象。
② 當調用 ProductMapper 時中的方法時,其實這裏所調用的是 MapperProxy 中的方法,而且 MapperProxy已經將將全部方法攔截,其具體原理及分析,參考 MyBatis+Spring基於接口編程的原理分析,其 invoke 方法代碼爲:
[Java] 純文本查看 複製代碼
?sql

//當調用 Mapper 全部的方法時,將都交由Proxy 中的 invoke 處理:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {數據庫

try {  
  if (!OBJECT_METHODS.contains(method.getName())) {  
    final Class declaringInterface = findDeclaringInterface(proxy, method);  
    // 最終交由 MapperMethod 類處理數據庫操做,初始化 MapperMethod 對象  
    final MapperMethod mapperMethod = new MapperMethod(declaringInterface, method, sqlSession);  
    // 執行 mapper method,返回執行結果   
    final Object result = mapperMethod.execute(args);  
    ....  
    return result;  
  }  
} catch (SQLException e) {  
  e.printStackTrace();  
}  
return null;

}apache

③其中的 mapperMethod 中的 execute 方法代碼以下:
[Java] 純文本查看 複製代碼
?編程

public Object execute(Object[] args) throws SQLException {緩存

Object result;  
// 根據不一樣的操做類別,調用 DefaultSqlSession 中的執行處理  
if (SqlCommandType.INSERT == type) {  
  Object param = getParam(args);  
  result = sqlSession.insert(commandName, param);  
} else if (SqlCommandType.UPDATE == type) {  
  Object param = getParam(args);  
  result = sqlSession.update(commandName, param);  
} else if (SqlCommandType.DELETE == type) {  
  Object param = getParam(args);  
  result = sqlSession.delete(commandName, param);  
} else if (SqlCommandType.SELECT == type) {  
  if (returnsList) {  
    result = executeForList(args);  
  } else {  
    Object param = getParam(args);  
    result = sqlSession.selectOne(commandName, param);  
  }  
} else {  
  throw new BindingException("Unkown execution method for: " + commandName);  
}  
return result;

}安全

因爲這裏是根據 ID 進行查詢,因此最終調用爲 sqlSession.selectOne函數。也就是接下來的的 DefaultSqlSession.selectOne 執行;
④ ⑤ 能夠在 DefaultSqlSession 看到,其 selectOne 調用了 selectList 方法:
[Java] 純文本查看 複製代碼
?數據結構

public Object selectOne(String statement, Object parameter) {mybatis

List list = selectList(statement, parameter);  
if (list.size() == 1) {  
  return list.get(0);  
}   
...

}

public List selectList(String statement, Object parameter, RowBounds rowBounds) {

try {  
  MappedStatement ms = configuration.getMappedStatement(statement);  
  // 若是啓動用了Cache 才調用 CachingExecutor.query,反之則使用 BaseExcutor.query 進行數據庫查詢   
  return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);  
} catch (Exception e) {  
  throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);  
} finally {  
  ErrorContext.instance().reset();  
}

}

⑥到這裏,已經執行到具體數據查詢的流程,在分析 CachingExcutor.query 前,先看看 MyBatis 中 Executor 的結構及構建過程。

執行器(Executor):
Executor: 執行器接口。也是最終執行數據獲取及更新的實例。其類結構以下:

BaseExecutor: 基礎執行器抽象類。實現一些通用方法,如createCacheKey 之類。而且採用 模板模式 將具體的數據庫操做邏輯(doUpdate、doQuery)交由子類實現。另外,能夠看到變量 localCache: PerpetualCache,在該類採用 PerpetualCache 實現基於 Map 存儲的一級緩存,其 query 方法以下:
[Java] 純文本查看 複製代碼
?

public List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {

ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());  
// 執行器已關閉  
if (closed) throw new ExecutorException("Executor was closed.");  
List list;  
try {  
  queryStack++;   
  // 建立緩存Key  
  CacheKey key = createCacheKey(ms, parameter, rowBounds);   
  // 從本地緩存在中獲取該 key 所對應 的結果集  
  final List cachedList = (List) localCache.getObject(key);   
  // 在緩存中找到數據  
  if (cachedList != null) {   
    list = cachedList;  
  } else { // 未從本地緩存中找到數據,開始調用數據庫查詢  
    //爲該 key 添加一個佔位標記  
    localCache.putObject(key, EXECUTION_PLACEHOLDER);   
    try {  
      // 執行子類所實現的數據庫查詢 操做  
      list = doQuery(ms, parameter, rowBounds, resultHandler);   
    } finally {  
      // 刪除該 key 的佔位標記  
      localCache.removeObject(key);  
    }  
    // 將db中的數據添加至本地緩存中  
    localCache.putObject(key, list);  
  }  
} finally {  
  queryStack--;  
}  
// 刷新當前隊列中的全部 DeferredLoad實例,更新 MateObject  
if (queryStack == 0) {   
  for (DeferredLoad deferredLoad : deferredLoads) {  
    deferredLoad.load();  
  }  
}  
return list;

}

BatchExcutor、ReuseExcutor、SimpleExcutor: 這幾個就沒什麼好說的了,繼承了 BaseExcutor 的實現其 doQuery、doUpdate 等方法,一樣都是採用 JDBC 對數據庫進行操做;三者區別在於,批量執行、重用 Statement 執行、普通方式執行。具體應用及場景在Mybatis 的文檔上都有詳細說明。

CachingExecutor: 二級緩存執行器。我的以爲這裏設計的不錯,靈活地使用 delegate機制。其委託執行的類是 BaseExcutor。 當沒法從二級緩存獲取數據時,一樣須要從 DB 中進行查詢,因而在這裏能夠直接委託給 BaseExcutor 進行查詢。其大概流程爲:

流程爲: 從二級緩存中進行查詢 -> [若是緩存中沒有,委託給 BaseExecutor] -> 進入一級緩存中查詢 -> [若是也沒有] -> 則執行 JDBC 查詢,其 query 代碼以下:
[Java] 純文本查看 複製代碼
?

public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {

if (ms != null) {  
  // 獲取二級緩存實例  
  Cache cache = ms.getCache();  
  if (cache != null) {  
    flushCacheIfRequired(ms);  
    // 獲取 讀鎖( Read鎖可由多個Read線程同時保持)  
    cache.getReadWriteLock().readLock().lock();  
    try {  
      // 當前 Statement 是否啓用了二級緩存  
      if (ms.isUseCache()) {  
        // 將建立 cache key 委託給 BaseExecutor 建立  
        CacheKey key = createCacheKey(ms, parameterObject, rowBounds);  
        final List cachedList = (List) cache.getObject(key);  
        // 從二級緩存中找到緩存數據  
        if (cachedList != null) {  
          return cachedList;  
        } else {  
          // 未找到緩存,很委託給 BaseExecutor 執行查詢  
          List list = delegate.query(ms, parameterObject, rowBounds, resultHandler);  
          tcm.putObject(cache, key, list);  
          return list;  
        }  
      } else { // 沒有啓動用二級緩存,直接委託給 BaseExecutor 執行查詢   
        return delegate.query(ms, parameterObject, rowBounds, resultHandler);  
      }  
    } finally {  
      // 當前線程釋放 Read 鎖  
      cache.getReadWriteLock().readLock().unlock();  
    }  
  }  
}  
return delegate.query(ms, parameterObject, rowBounds, resultHandler);

}

至此,已經完完了整個緩存執行器的整個流程分析,接下來是對緩存的 緩存數據管理實例進行分析,也就是其 Cache 接口,用於對緩存數據 put 、get及remove的實例對象。

Cache 委託鏈構建:
正如最開始的緩存概述所描述道,其緩存類的設計採用 裝飾模式,基於委託的調用機制。
緩存實例構建:
緩存實例的構建 ,Mybatis 在解析其 Mapper 配置文件時就已經將該實現初始化,在 org.apache.ibatis.builder.xml.XMLMapperBuilder 類中能夠看到:
[Java] 純文本查看 複製代碼
?

private void cacheElement(XNode context) throws Exception {

if (context != null) {  
  // 基礎緩存類型  
  String type = context.getStringAttribute("type", "PERPETUAL");  
  Class typeClass = typeAliasRegistry.resolveAlias(type);  
  // 排除算法緩存類型  
  String eviction = context.getStringAttribute("eviction", "LRU");  
  Class evictionClass = typeAliasRegistry.resolveAlias(eviction);  
  // 緩存自動刷新時間  
  Long flushInterval = context.getLongAttribute("flushInterval");  
  // 緩存存儲實例引用的大小  
  Integer size = context.getIntAttribute("size");  
  // 是不是隻讀緩存  
  boolean readWrite = !context.getBooleanAttribute("readOnly", false);  
  Properties props = context.getChildrenAsProperties();  
  // 初始化緩存實現  
  builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, props);  
}

}

如下是 useNewCache 方法實現:
[Java] 純文本查看 複製代碼
?

public Cache useNewCache(Class typeClass,

Class evictionClass,  
                       Long flushInterval,  
                       Integer size,  
                       boolean readWrite,  
                       Properties props) {  
typeClass = valueOrDefault(typeClass, PerpetualCache.class);  
evictionClass = valueOrDefault(evictionClass, LruCache.class);  
// 這裏構建 Cache 實例採用 Builder 模式,每個 Namespace 生成一個  Cache 實例  
Cache cache = new CacheBuilder(currentNamespace)  
    // Builder 前設置一些從XML中解析過來的參數  
    .implementation(typeClass)  
    .addDecorator(evictionClass)  
    .clearInterval(flushInterval)  
    .size(size)  
    .readWrite(readWrite)  
    .properties(props)  
    // 再看下面的 build 方法實現  
    .build();  
configuration.addCache(cache);  
currentCache = cache;  
return cache;

}

public Cache build() {

setDefaultImplementations();  
// 建立基礎緩存實例  
Cache cache = newBaseCacheInstance(implementation, id);  
setCacheProperties(cache);  
// 緩存排除算法初始化,並將其委託至基礎緩存中  
for (Class<? extends Cache> decorator : decorators) {  
  cache = newCacheDecoratorInstance(decorator, cache);  
  setCacheProperties(cache);  
}  
// 標準裝飾器緩存設置,如LoggingCache之類,一樣將其委託至基礎緩存中  
cache = setStandardDecorators(cache);  
// 返回最終緩存的責任鏈對象  
return cache;

}

最終生成後的緩存實例對象結構:

可見,全部構建的緩存實例已經經過責任鏈方式將其串連在一塊兒,各 Cache 各負其責、依次調用,直到緩存數據被 Put 至 基礎緩存實例中存儲。

Cache 實例解剖:
實例類:SynchronizedCache
說 明:用於控制 ReadWriteLock,避免併發時所產生的線程安全問題。
解 剖:
對於 Lock 機制來講,其分爲 Read 和 Write 鎖,其 Read 鎖容許多個線程同時持有,而 Write 鎖,一次能被一個線程持有,若是當 Write 鎖沒有釋放,其它須要 Write 的線程只能等待其釋放才能去持有。
其代碼實現:
[Java] 純文本查看 複製代碼
?

public void putObject(Object key, Object object) {

acquireWriteLock();  // 獲取 Write 鎖  
try {  
  delegate.putObject(key, object); // 委託給下一個 Cache 執行 put 操做  
} finally {  
  releaseWriteLock(); // 釋放 Write 鎖  
}

}

對於 Read 數據來講,也是如此,不一樣的是 Read 鎖容許多線程同時持有 :
[Java] 純文本查看 複製代碼
?

public Object getObject(Object key) {

acquireReadLock();  
try {  
  return delegate.getObject(key);  
} finally {  
  releaseReadLock();  
}

}

其具體原理能夠看看 jdk concurrent 中的 ReadWriteLock 實現。

實例類:LoggingCache
說 明:用於日誌記錄處理,主要輸出緩存命中率信息。
解 剖:
說到緩存命中信息的統計,只有在 get 的時候才須要統計命中率:
[Java] 純文本查看 複製代碼
?

public Object getObject(Object key) {

requests++; // 每調用一次該方法,則獲取次數+1  
final Object value = delegate.getObject(key);  
if (value != null) {  // 命中! 命中+1  
  hits++;  
}  
if (log.isDebugEnabled()) {  
  // 輸出命中率。計算方法爲: hits / requets 則爲命中率  
  log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());  
}  
return value;

}

實例類:SerializedCache
說 明:向緩存中 put 或 get 數據時的序列化及反序列化處理。
解 剖:
序列化在Java裏面已是最基礎的東西了,這裏也沒有什麼特殊之處:
[Java] 純文本查看 複製代碼
?

public void putObject(Object key, Object object) {

// PO 類須要實現 Serializable 接口  
if (object == null || object instanceof Serializable) {  
  delegate.putObject(key, serialize((Serializable) object));   
} else {  
  throw new CacheException("SharedCache failed to make a copy of a non-serializable object: " + object);  
}

}

public Object getObject(Object key) {

Object object = delegate.getObject(key);  
// 獲取數據時對 二進制數據進行反序列化  
return object == null ? null : deserialize((byte[]) object);

}

其 serialize 及 deserialize 代碼就沒必要要貼了。

實例類:LruCache
說 明:最近最少使用的:移除最長時間不被使用的對象,基於LRU算法。
解 剖:
這裏的 LRU 算法基於 LinkedHashMap 覆蓋其 removeEldestEntry 方法實現。好象以前看過 XMemcached 的 LRU 算法也是這樣實現的。
初始化 LinkedHashMap,默認爲大小爲 1024 個元素:
[Java] 純文本查看 複製代碼
?

public LruCache(Cache delegate) {

this.delegate = delegate;  
setSize(1024); // 設置 map 默認大小

}
public void setSize(final int size) {

// 設置其 capacity 爲size, 其 factor 爲.75F  
keyMap = new LinkedHashMap(size, .75F, true) {  
  // 覆蓋該方法,當每次往該map 中put 時數據時,如該方法返回 True,便移除該map中使用最少的Entry  
  // 其參數  eldest 爲當前最老的  Entry  
  protected boolean removeEldestEntry(Map.Entry eldest) {  
    boolean tooBig = size() > size;  
    if (tooBig) {  
      eldestKey = eldest.getKey(); //記錄當前最老的緩存數據的 Key 值,由於要委託給下一個 Cache 實現刪除  
    }  
    return tooBig;  
  }  
};

}

public void putObject(Object key, Object value) {

delegate.putObject(key, value);  
cycleKeyList(key);  // 每次 put 後,調用移除最老的 key

}
// 看看當前實現是否有 eldestKey, 有的話就調用 removeObject ,將該key從cache中移除
private void cycleKeyList(Object key) {

keyMap.put(key, key); // 存儲當前 put 到cache中的 key 值  
if (eldestKey != null) {  
  delegate.removeObject(eldestKey);  
  eldestKey = null;  
}

}

public Object getObject(Object key) {

keyMap.get(key); // 便於 該 Map 統計 get該key的次數  
return delegate.getObject(key);

}

實例類:PerpetualCache
說 明:這個比較簡單,直接經過一個 HashMap 來存儲緩存數據。因此沒什麼說的,直接看下面的 MemcachedCache 吧。

自定義二級緩存/Memcached
其自定義二級緩存也較爲簡單,它自己默認提供了對 Ehcache 及 Hazelcast 的緩存支持:Mybatis-Cache,我這裏參考它們的實現,自定義了針對 Memcached 的緩存支持,其代碼以下:
[Java] 純文本查看 複製代碼
?

package com.xx.core.plugin.mybatis;

import java.util.LinkedList;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.apache.ibatis.cache.Cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.xx.core.memcached.JMemcachedClientAdapter;
import com.xx.core.memcached.service.CacheService;
import com.xx.core.memcached.service.MemcachedService;

/**

  • Cache adapter for Memcached.
  • @author denger

*/
public class MemcachedCache implements Cache {

// Sf4j logger reference  
private static Logger logger = LoggerFactory.getLogger(MemcachedCache.class);  

/** The cache service reference. */ 
protected static final CacheService CACHE_SERVICE = createMemcachedService();  

/** The ReadWriteLock. */ 
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();  

private String id;  
private LinkedList<String> cacheKeys = new LinkedList<String>();  

public MemcachedCache(String id) {  
    this.id = id;  
}  
// 建立緩存服務類,基於java-memcached-client  
protected static CacheService createMemcachedService() {  
    JMemcachedClientAdapter memcachedAdapter;  

    try {  
        memcachedAdapter = new JMemcachedClientAdapter();  
    } catch (Exception e) {  
        String msg = "Initial the JMmemcachedClientAdapter Error.";  
        logger.error(msg, e);  
        throw new RuntimeException(msg);  
    }  
    return new MemcachedService(memcachedAdapter);  
}  

@Override 
public String getId() {  
    return this.id;  
}  

// 根據 key 從緩存中獲取數據  
@Override 
public Object getObject(Object key) {  
    String cacheKey = String.valueOf(key.hashCode());  
    Object value = CACHE_SERVICE.get(cacheKey);  
    if (!cacheKeys.contains(cacheKey)){  
        cacheKeys.add(cacheKey);  
    }  
    return value;  
}  

@Override 
public ReadWriteLock getReadWriteLock() {  
    return this.readWriteLock;  
}  

// 設置數據至緩存中  
@Override 
public void putObject(Object key, Object value) {  
    String cacheKey = String.valueOf(key.hashCode());  

    if (!cacheKeys.contains(cacheKey)){  
        cacheKeys.add(cacheKey);  
    }  
    CACHE_SERVICE.put(cacheKey, value);  
}  
// 從緩存中刪除指定 key 數據  
@Override 
public Object removeObject(Object key) {  
    String cacheKey = String.valueOf(key.hashCode());  

    cacheKeys.remove(cacheKey);  
    return CACHE_SERVICE.delete(cacheKey);  
}  
//清空當前 Cache 實例中的全部緩存數據  
@Override 
public void clear() {  
    for (int i = 0; i < cacheKeys.size(); i++){  
        String cacheKey = cacheKeys.get(i);  
        CACHE_SERVICE.delete(cacheKey);  
    }  
    cacheKeys.clear();  
}  

@Override 
public int getSize() {  
    return cacheKeys.size();  
}

}

在 ProductMapper 中增長配置:
[XML] 純文本查看 複製代碼
?
1
<cache eviction="LRU" type="com.xx.core.plugin.mybatis.MemcachedCache" />

啓動Memcached:
[Shell] 純文本查看 複製代碼
?
1
memcached -c 2000 -p 11211 -vv -U 0 -l 192.168.1.2 -v

執行Mapper 中的查詢、修改等操做,Test:
[Java] 純文本查看 複製代碼
?

@Test

public void testSelectById() {  
    Long pid = 100L;  

    Product dbProduct = productMapper.selectByID(pid);  
    Assert.assertNotNull(dbProduct);  

    Product cacheProduct = productMapper.selectByID(pid);  
    Assert.assertNotNull(cacheProduct);  

    productMapper.updateName("IPad", pid);  

    Product product = productMapper.selectByID(pid);  
    Assert.assertEquals(product.getName(), "IPad");  
}
相關文章
相關標籤/搜索