使用Redis做MyBatis的二級緩存


使用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前,先看看其整個處理過程。

  1. 通常我們在service層最終都會調用Mapper的接口方法,實現對數據庫的操作,本例中是通過id查詢product對象。

  2. 我 們知道Mapper是一個接口,接口是沒有對象的,更不能調用方法了,而我們調用的其實是mybatis框架的mapper動態代理對象 MapperProxy,而MapperProxy中有封裝了配置信息的DefaultSqlSession中的Configuration。動態代理的 具體實現請戳這里。調用mapper方法的具體代碼如下。


  3. 在執行mapperMethod的execute的時候,不僅傳遞了方法參數,還傳遞了sqlSession。在執行 execute,其實是通過判斷配置文件的操作類型,來調用sqlSession的對應方法的。本例中,由於是select,而返回值不是list,所以 下一步執行的是sqlSession的selectOne方法具體代碼如下:

  4. selectOne其實調用了selectList,只不過是取了第一個。具體代碼如下:

  5. selectList經過層層調用,最終交給執行器執行。具體執行器的結構待會我們會分析。注意這里的ms參數,其實就是從Configration中得到的一些配置信息,包括mapper文件里的sql語句。具體代碼如下:

  6. 這里的執行器execute,其實是spring注入的。excute是一個接口,而到時候具體是哪個execute執行,是看配置文件的。而我們的一級緩存和二級緩存其實都是execute中的一種。接下來,我們遍分析一下執行器(Executor)。

2.2 執行器(Executor)

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

  1. BaseExecutor:基礎執行器抽象類。實現一些通用方法,如createCacheKey之類的。並采用模板模式將具體的數據庫操作邏輯交由子類實現。另外,可以看到變量localCache:PerpetualCache,在該類采用perpetualCache實現基於map存儲的一級緩存,其query方法如下:

    一級緩存和二級緩存很相似,都是實現Cache緩存接口,然后等待調用。其中的一級緩存具體實現其實使用了Map存儲,原理非常簡單。PerPetualCache具體結構如下:

  2. BatchExcutor、 ReuseExcutor、SimpleExcutor:這三個就是簡單的繼承了BaseExcutor,實現了doQuery、doUpdate等發 放,同樣都是采用了JDBC對數據庫進行操作;三者的區別在於,批量執行、重用Statement執行、普通方法執行。具體應用及長江在mybatis文 檔上都有詳細的說明。

  3. 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關於緩存的這一部分分為三個部分:

  1. 解析器:結合mybatis-spring框架,讀取spring關於mybatis的配置文件。具體看是否開啟緩存(這里指二級緩存),如果開啟,生成的執行器為CachingExecutor。

  2. 動態代理:實現調用mapper接口的時候執行mybatis邏輯

  3. 執行器:執行緩存處理邏輯。在這里二級緩存和一級緩存有所區別。

三、具體實現

3.1 配置文件

  1. 開啟緩存:修改spring中關於mybatis的配置文件,將cacheEnabled設置為true。

  2. 添加實現Cache接口的實現類。重寫方法會在查詢數據庫前后調用,查詢、更新、刪除、創建緩存需要在這幾個方法中實現。值得注意的是,getObject方法,當返回的是null時,就會接着查詢。如果不為null,則返回,不再查詢了。

  3. /**

    * 使用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;

    }

  4.  

    @Override

    public int getSize() {

    return Integer.valueOf(redisClient.dbSize().toString());

    }

  5.  

    @Override

    public void putObject(Object key, Object value) {

    logger.debug("==================pubObject:" + key + "=" + value);

    redisClient.set(SerializeUtil.serialize(key),SerializeUtil.serialize(value));

    }

  6.  

    @Override

    public Object getObject(Object key) {

    Object value = SerializeUtil.unserialize(redisClient.get(SerializeUtil.serialize(key.toString())));

    logger.debug("==================getObjec:" + key + "=" + value);

    return value;

    }

  7.  

    @Override

    public Object removeObject(Object key) {

    return redisClient.expire(SerializeUtil.serialize(key.toString()), 0);

    }

  8.  

    @Override

    public void clear() {

    redisClient.flushDB();

    }

  9.  

    @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錯誤");

    }

    }

  10. 在相應的Mapper文件中,加入Cache節點,將自定義的cache實現類加進去。

四、總結

 MyBatis的整體思路其實很簡單,但是跟着源碼發現,一個好的框架需要考慮的問題很多,從可擴展性、功能維護等角度考慮,如果沒有一個好的設計思路會把代碼設計的很亂很亂。MyBatis充分利用了動態代理、建造模式、裝飾器模式,似的他們結合在一起,讓整個框架變得簡單易用,其實是很難得的

 這就好比讀一本書,需要先讀后再度薄一樣,框架的設計最開始需要考慮到各種各樣的問題,然后把一個簡單的思路變得復雜。然后通過合理的設計,將復雜的問題簡單的設計出來,使得代碼很整潔,易於維護和讀,這才是一個好的框架應該有的樣子

 真的很感謝能有這么一個機會研究一下mybatis,並從中學到了許多。希望有朝一日,也能寫出想mybatis一樣的框架。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM