使用Redis做MyBatis的二級緩存
通常為了減輕數據庫的壓力,我們會引入緩存。在Dao查詢數據庫之前,先去緩存中找是否有要找的數據,如果有則用緩存中的數據即可,就不用查詢數據庫了。 如果沒有才去數據庫中查找。這樣就能分擔一下數據庫的壓力。另外,為了讓緩存中的數據與數據庫同步,我們應該在該數據發生變化的地方加入更新緩存的邏輯代 碼。這樣無形之中增加了工作量,同時也是一種對原有代碼的入侵。這對於有着代碼潔癖的程序員來說,無疑是一種傷害。
MyBatis框架早就考慮到了這些問題,因此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 過程中線程安全問題
二、源碼剖析
2.1 執行流程分析
接下來將結合 MyBatis 序列圖進行源碼分析。在分析其Cache前,先看看其整個處理過程。
-
通常我們在service層最終都會調用Mapper的接口方法,實現對數據庫的操作,本例中是通過id查詢product對象。
-
我 們知道Mapper是一個接口,接口是沒有對象的,更不能調用方法了,而我們調用的其實是mybatis框架的mapper動態代理對象 MapperProxy,而MapperProxy中有封裝了配置信息的DefaultSqlSession中的Configuration。動態代理的 具體實現請戳這里。調用mapper方法的具體代碼如下。
-
在執行mapperMethod的execute的時候,不僅傳遞了方法參數,還傳遞了sqlSession。在執行 execute,其實是通過判斷配置文件的操作類型,來調用sqlSession的對應方法的。本例中,由於是select,而返回值不是list,所以 下一步執行的是sqlSession的selectOne方法具體代碼如下:
-
selectList經過層層調用,最終交給執行器執行。具體執行器的結構待會我們會分析。注意這里的ms參數,其實就是從Configration中得到的一些配置信息,包括mapper文件里的sql語句。具體代碼如下:
-
這里的執行器execute,其實是spring注入的。excute是一個接口,而到時候具體是哪個execute執行,是看配置文件的。而我們的一級緩存和二級緩存其實都是execute中的一種。接下來,我們遍分析一下執行器(Executor)。
2.2 執行器(Executor)
Executor:執行器接口。也是最終執行數據獲取及更新的實例。其結構如下:
-
BaseExecutor:基礎執行器抽象類。實現一些通用方法,如createCacheKey之類的。並采用模板模式將具體的數據庫操作邏輯交由子類實現。另外,可以看到變量localCache:PerpetualCache,在該類采用perpetualCache實現基於map存儲的一級緩存,其query方法如下:
一級緩存和二級緩存很相似,都是實現Cache緩存接口,然后等待調用。其中的一級緩存具體實現其實使用了Map存儲,原理非常簡單。PerPetualCache具體結構如下: -
BatchExcutor、 ReuseExcutor、SimpleExcutor:這三個就是簡單的繼承了BaseExcutor,實現了doQuery、doUpdate等發 放,同樣都是采用了JDBC對數據庫進行操作;三者的區別在於,批量執行、重用Statement執行、普通方法執行。具體應用及長江在mybatis文 檔上都有詳細的說明。
-
CachingExecutor:二級緩存執行器。其中使用了靜態代理模式,當二級緩存中沒有數據的時候,就使用BaseExecutor做代理,進行下一步執行。具體代碼如下:
2.3 Cache的設計
像 之前所說,Cache是一個緩存接口,運行時用到的其實是在解析mapper文件的時候根據配置文件生成的對應Cahce實現類。另外這個實現類的構造過 程使用了建造者(Builder)模式。在build的過程中,將所有設計到的cache放入基礎緩存中,並使用裝飾器模式將cache進行裝飾。具體設 計如下:
1. 從配置文件中獲取節點,將配置信息提取出來初始化生成Cache
2. useNewCache方法中使用了建造者(Builder)模式,將從配置文件中讀取出來的各個元素組裝起來。其中最主要的是build方法。
3. 在build方法中,值得注意的是使用了裝飾器模式,將幾個基本的Cache裝飾了一下。因為我們的Cache只是加入了自定義的緩存功能和邏輯,日志功能、同步功能等其實並沒有。所以需要裝飾一下,具體代碼如下:
4. 最終的緩存實例對象結構:
2.4 總結
總體上看,我們可以把MyBatis關於緩存的這一部分分為三個部分:
-
解析器:結合mybatis-spring框架,讀取spring關於mybatis的配置文件。具體看是否開啟緩存(這里指二級緩存),如果開啟,生成的執行器為CachingExecutor。
-
動態代理:實現調用mapper接口的時候執行mybatis邏輯
-
執行器:執行緩存處理邏輯。在這里二級緩存和一級緩存有所區別。
三、具體實現
3.1 配置文件
-
添加實現Cache接口的實現類。重寫方法會在查詢數據庫前后調用,查詢、更新、刪除、創建緩存需要在這幾個方法中實現。值得注意的是,getObject方法,當返回的是null時,就會接着查詢。如果不為null,則返回,不再查詢了。
-
/**
* 使用redis做mybatis二級緩存
* @Description
* @file_name MyBatisRedisCache.java
* @time 2016-07-26 下午4:49:13
* @author muxiaocao
*/
public class MyBatisRedisCache implements Cache{
@Value("#{config['redis.ip']}")
protected String redisIp;
@Value("#{config['redis.port']}")
protected Integer redisPort;
private static Log logger = LogFactory.getLog(MyBatisRedisCache.class);
private Jedis redisClient = createClient();
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private String id;
public MyBatisRedisCache(final String id) {
if (id == null) {
throw new IllegalArgumentException("緩存沒有初始化id");
}
logger.debug("==================MyBatisRedisCache:id=" + id);
this.id = id;
}
@Override
public String getId() {
return this.id;
}
-
@Override
public int getSize() {
return Integer.valueOf(redisClient.dbSize().toString());
}
-
@Override
public void putObject(Object key, Object value) {
logger.debug("==================pubObject:" + key + "=" + value);
redisClient.set(SerializeUtil.serialize(key),SerializeUtil.serialize(value));
}
-
@Override
public Object getObject(Object key) {
Object value = SerializeUtil.unserialize(redisClient.get(SerializeUtil.serialize(key.toString())));
logger.debug("==================getObjec:" + key + "=" + value);
return value;
}
-
@Override
public Object removeObject(Object key) {
return redisClient.expire(SerializeUtil.serialize(key.toString()), 0);
}
-
@Override
public void clear() {
redisClient.flushDB();
}
-
@Override
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
}
private Jedis createClient() {
try {
RedisUtil.initRedis(redisIp);
return RedisUtil.getRedis();
} catch (Exception e) {
e.printStackTrace();
}
throw new RuntimeException("初始化redis錯誤");
}
}
四、總結
MyBatis的整體思路其實很簡單,但是跟着源碼發現,一個好的框架需要考慮的問題很多,從可擴展性、功能維護等角度考慮,如果沒有一個好的設計思路會把代碼設計的很亂很亂。MyBatis充分利用了動態代理、建造模式、裝飾器模式,似的他們結合在一起,讓整個框架變得簡單易用,其實是很難得的。
這就好比讀一本書,需要先讀后再度薄一樣,框架的設計最開始需要考慮到各種各樣的問題,然后把一個簡單的思路變得復雜。然后通過合理的設計,將復雜的問題簡單的設計出來,使得代碼很整潔,易於維護和讀,這才是一個好的框架應該有的樣子。
真的很感謝能有這么一個機會研究一下mybatis,並從中學到了許多。希望有朝一日,也能寫出想mybatis一樣的框架。