- 正如大多數持久層框架一樣,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 過程中線程安全問題。
源碼剖解
接下來將結合 MyBatis 序列圖進行源碼分析。在分析其Cache前,先看看其整個處理過程。
執行過程:
① 通常情況下,我們需要在 Service 層調用 Mapper Interface 中的方法實現對數據庫的操作,上述根據產品 ID 獲取 Product 對象。
② 當調用 ProductMapper 時中的方法時,其實這裏所調用的是 MapperProxy 中的方法,並且 MapperProxy已經將將所有方法攔截,其具體原理及分析,參考 MyBatis+Spring基於接口編程的原理分析,其 invoke 方法代碼爲:
- //當調用 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;
- }
③其中的 mapperMethod 中的 execute 方法代碼如下:
- 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;
- }
④ ⑤ 可以在 DefaultSqlSession 看到,其 selectOne 調用了 selectList 方法:
- public Object selectOne(String statement, Object parameter) {
- 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();
- }
- }
執行器(Executor):
Executor: 執行器接口。也是最終執行數據獲取及更新的實例。其類結構如下:
BaseExecutor: 基礎執行器抽象類。實現一些通用方法,如createCacheKey 之類。並且採用 模板模式 將具體的數據庫操作邏輯(doUpdate、doQuery)交由子類實現。另外,可以看到變量 localCache: PerpetualCache,在該類採用 PerpetualCache 實現基於 Map 存儲的一級緩存,其 query 方法如下:
- 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;
- }
CachingExecutor: 二級緩存執行器。個人覺得這裏設計的不錯,靈活地使用 delegate機制。其委託執行的類是 BaseExcutor。 當無法從二級緩存獲取數據時,同樣需要從 DB 中進行查詢,於是在這裏可以直接委託給 BaseExcutor 進行查詢。其大概流程爲:
流程爲: 從二級緩存中進行查詢 -> [如果緩存中沒有,委託給 BaseExecutor] -> 進入一級緩存中查詢 -> [如果也沒有] -> 則執行 JDBC 查詢,其 query 代碼如下:
- 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 委託鏈構建:
正如最開始的緩存概述所描述道,其緩存類的設計採用 裝飾模式,基於委託的調用機制。
緩存實例構建:
緩存實例的構建 ,Mybatis 在解析其 Mapper 配置文件時就已經將該實現初始化,在 org.apache.ibatis.builder.xml.XMLMapperBuilder 類中可以看到:
- 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);
- }
- }
- 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 的線程只能等待其釋放才能去持有。
其代碼實現:
- public void putObject(Object key, Object object) {
- acquireWriteLock(); // 獲取 Write 鎖
- try {
- delegate.putObject(key, object); // 委託給下一個 Cache 執行 put 操作
- } finally {
- releaseWriteLock(); // 釋放 Write 鎖
- }
- }
- public Object getObject(Object key) {
- acquireReadLock();
- try {
- return delegate.getObject(key);
- } finally {
- releaseReadLock();
- }
- }
實例類:LoggingCache
說 明:用於日誌記錄處理,主要輸出緩存命中率信息。
解 剖:
說到緩存命中信息的統計,只有在 get 的時候才需要統計命中率:
- 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裏面已經是最基礎的東西了,這裏也沒有什麼特殊之處:
- 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);
- // 獲取數據時對 byte數據進行反序列化
- return object == null ? null : deserialize((byte[]) object);
- }
實例類:LruCache
說 明:最近最少使用的:移除最長時間不被使用的對象,基於LRU算法。
解 剖:
這裏的 LRU 算法基於 LinkedHashMap 覆蓋其 removeEldestEntry 方法實現。好象之前看過 XMemcached 的 LRU 算法也是這樣實現的。
初始化 LinkedHashMap,默認爲大小爲 1024 個元素:
- 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 的緩存支持,其代碼如下:
- 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 中增加配置:
- <cache eviction="LRU" type="com.xx.core.plugin.mybatis.MemcachedCache" />
啓動Memcached:
- memcached -c 2000 -p 11211 -vv -U 0 -l 192.168.1.2 -v
執行Mapper 中的查詢、修改等操作,Test:
- @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");
- }
Memcached Loging:
看上去沒什麼問題~ OK了。