聊聊MyBatis二級緩存機制

導語

看着這篇文章的你一定是程序員了吧,哈哈,這麼快來添加小編的微信,帶你進入Java技術交流羣;備註csdn,羣裏的大佬,等你來聊;
小編微信:372787553

Mybatis 自定義二級緩存

Mybatis 在日常的Java開發,應該非常廣泛,這裏我就不過多介紹,今天我們聊聊Mybatis 的二級緩存,在日常開發中,如果經常訪問數據庫,開銷 和速度都是個問題,Mybatis爲我們提供了二級緩存,但是Mybatis自帶的二級緩存,存在一些缺陷,因爲他是本地,這樣我們部署多個實例就會發生一些髒讀/幻讀的問題(因爲是多機器部署,緩存無法統一處理而造成的,如果您的服務是單機部署,不會產生這樣的問題)

1.自帶緩存

MyBatis 內置了一個強大的事務性查詢緩存機制,它可以非常方便地配置和定製。 爲了使它更加強大而且易於配置,我們對 MyBatis 3 中的緩存實現進行了許多改進。

默認情況下,只啓用了本地的會話緩存,它僅僅對一個會話中的數據進行緩存。 要啓用全局的二級緩存,只需要在你的 SQL 映射文件中添加一行:

<cache/>

基本上就是這樣。這個簡單語句的效果如下:

  • 映射語句文件中的所有 select 語句的結果將會被緩存。
  • 映射語句文件中的所有 insert、update 和 delete 語句會刷新緩存。
  • 緩存會使用最近最少使用算法(LRU, Least Recently Used)算法來清除不需要的緩存。
  • 緩存不會定時進行刷新(也就是說,沒有刷新間隔)。
  • 緩存會保存列表或對象(無論查詢方法返回哪種)的 1024 個引用。
  • 緩存會被視爲讀/寫緩存,這意味着獲取到的對象並不是共享的,可以安全地被調用者修改,而不干擾其他調用者或線程所做的潛在修改。

提示 緩存只作用於 cache 標籤所在的映射文件中的語句。如果你混合使用 Java API 和 XML 映射文件,在共用接口中的語句將不會被默認緩存。你需要使用 @CacheNamespaceRef 註解指定緩存作用域。

這些屬性可以通過 cache 元素的屬性來修改。比如:

<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="true"/>

這個更高級的配置創建了一個 FIFO 緩存,每隔 60 秒刷新,最多可以存儲結果對象或列表的 512 個引用,而且返回的對象被認爲是隻讀的,因此對它們進行修改可能會在不同線程中的調用者產生衝突。

可用的清除策略有:

  • LRU – 最近最少使用:移除最長時間不被使用的對象。
  • FIFO – 先進先出:按對象進入緩存的順序來移除它們。
  • SOFT – 軟引用:基於垃圾回收器狀態和軟引用規則移除對象。
  • WEAK – 弱引用:更積極地基於垃圾收集器狀態和弱引用規則移除對象。

默認的清除策略是 LRU。

flushInterval(刷新間隔)屬性可以被設置爲任意的正整數,設置的值應該是一個以毫秒爲單位的合理時間量。 默認情況是不設置,也就是沒有刷新間隔,緩存僅僅會在調用語句時刷新。

size(引用數目)屬性可以被設置爲任意正整數,要注意欲緩存對象的大小和運行環境中可用的內存資源。默認值是 1024。

readOnly(只讀)屬性可以被設置爲 true 或 false。只讀的緩存會給所有調用者返回緩存對象的相同實例。 因此這些對象不能被修改。這就提供了可觀的性能提升。而可讀寫的緩存會(通過序列化)返回緩存對象的拷貝。 速度上會慢一些,但是更安全,因此默認值是 false。

提示 二級緩存是事務性的。這意味着,當 SqlSession 完成並提交時,或是完成並回滾,但沒有執行 flushCache=true 的 insert/delete/update 語句時,緩存會獲得更新。

2. 自定義緩存

這裏的自定義緩存我們採用Redis來進行持久化,這是我們需要更改配置:

 <cache type="com.javayh.mybatis.cache.RedisCache"/>

這個示例展示瞭如何使用一個自定義的緩存實現。type 屬性指定的類必須實現 org.apache.ibatis.cache.Cache 接口,且提供一個接受 String 參數作爲 id 的構造器。 這個接口是 MyBatis 框架中許多複雜的接口之一,但是行爲卻非常簡單。
PerpetualCache 類時Mybatis爲我們提供的自帶的二級緩存實現

2.1 Redis版本實現
public class RedisCache implements Cache {

    private String id;

    /** 讀寫鎖*/
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);

    public RedisCache() {
    }

    public RedisCache(final String id) {
        if (id == null) {
            throw new IllegalArgumentException("Cache instances require an ID");
        }
        this.id = id;
    }
    @Autowired
    private RedisUtil redisUtil;
    private static RedisCache redisCache ;

    @PostConstruct
    public void init() {
        redisCache = this;
        redisCache.redisUtil = this.redisUtil;
    }
    @Override
    public String getId() {
        return this.id;
    }

    @Override
    public void putObject(Object key, Object value) {
        if (value != null) {
            //向Redis中添加數據,有效時間是12小時
            redisCache.redisUtil.setObj(key.toString(),value,43200);
            log.debug(value.toString());
        }
    }

    @Override
    public Object getObject(Object key) {
        try {
            if (key != null) {
                return redisCache.redisUtil.get(key.toString());
            }
        } catch (Exception e) {
            Log.error("Mybatis Get Cache",e.getStackTrace());
        }
        return null;
    }

    @Override
    public Object removeObject(Object key) {
        try {
            if (!ObjectUtils.isEmpty(key)) {
                redisCache.redisUtil.del(key.toString());
            }
        } catch (Exception e) {
            Log.error("Mybatis Del Cache",e.getStackTrace());
        }
        return null;
    }

    @Override
    public void clear() {
        try {
            Set<String> keys = redisCache.redisUtil.keys(this.id);
            if (!CollectionUtils.isEmpty(keys)) {
                redisCache.redisUtil.del(keys);
            }
        } catch (Exception e) {
            Log.error("Mybatis Clear Cache",e.getStackTrace());
        }
    }

    @Override
    public int getSize() {
        return redisCache.redisUtil.execute();
    }

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

2.2 測試驗證

第一次查詢:
在這裏插入圖片描述

第二次查詢:

在這裏插入圖片描述
這是我們發現,第二次查詢已經命中緩存,並且查詢的速度大大提升了,進而達到了對服務優化;並且也避免了多機部署帶來的問題;

這時還有一個問題,細心的朋友也許已經發現,雖然我們對緩存進行過期時間的設置,但是這期間我們對數據進行增刪改的操作,還是查詢緩存,問題好像更大;其實不然,我們對數據進行刪除時,Mybatis會自動刪除緩存;

刪除一條數據

在這裏插入圖片描述
再次查詢

在這裏插入圖片描述
如上圖我們發現,當對數據進行Update時,會進行緩存的銷燬

提示 上一節中對緩存的配置(如清除策略、可讀或可讀寫等),不能應用於自定義緩存。

請注意,緩存的配置和緩存實例會被綁定到 SQL 映射文件的命名空間中。 因此,同一命名空間中的所有語句和緩存將通過命名空間綁定在一起。 每條語句可以自定義與緩存交互的方式,或將它們完全排除於緩存之外,這可以通過在每條語句上使用兩個簡單屬性來達成。 默認情況下,語句會這樣來配置:

<select ... flushCache="false" useCache="true"/>
<insert ... flushCache="true"/>
<update ... flushCache="true"/>
<delete ... flushCache="true"/>

鑑於這是默認行爲,顯然你永遠不應該以這樣的方式顯式配置一條語句。但如果你想改變默認的行爲,只需要設置 flushCache 和 useCache 屬性。比如,某些情況下你可能希望特定 select 語句的結果排除於緩存之外,或希望一條 select 語句清空緩存。類似地,你可能希望某些 update 語句執行時不要刷新緩存。

2.3 cache-ref

回想一下上一節的內容,對某一命名空間的語句,只會使用該命名空間的緩存進行緩存或刷新。 但你可能會想要在多個命名空間中共享相同的緩存配置和實例。要實現這種需求,你可以使用 cache-ref 元素來引用另一個緩存。

<cache-ref namespace="com.someone.application.data.SomeMapper"/>

項目源代碼:github源代碼地址
演示源代碼:github源代碼地址

常見面試題總結

1.Mybatis 一級緩存/二級緩存命中原則
  1. sql id
  2. 查詢參數
  3. 分頁參數
  4. sql語句
  5. 環境
    一級緩存前期:SqlSession內
    二級緩存前提:SqlSessionactory內
2.Mybatis 一級緩存生命週期

產生:調用查詢語句時
銷燬:Session關閉,Conmit提交,Rolback回滾,Update,ClearCahche

3.Mybatis 二級緩存生命週期

產生:調用查詢語句,並執行了sqlsession.close();
銷燬:Update

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章