1. 程式人生 > >Mybatis的緩存機制詳解

Mybatis的緩存機制詳解

Mybatis 持久層框架 一級緩存 二級緩存 自定義緩存

一級緩存

MyBatis 包含了一個非常強大的查詢緩存特性,它可以非常方便地配置和定制。MyBatis 3 中的緩存實現的很多改進都已經實現了,使得它更加強大而且易於配置。mybatis默認情況下只會開啟一級緩存,也就是局部的 session 會話緩存。

首先我們要知道什麽是查詢緩存?查詢緩存又有什麽作用?

  • 功能:mybatis提供查詢緩存,用於減輕數據壓力,提高數據庫性能。

如下圖,每一個 session 會話都會有各自的緩存,這緩存是局部的,也就是所謂的一級緩存:
技術分享圖片

一級緩存是SqlSession級別的緩存。我們都知道在操作數據庫時需要構造 sqlSession對象,而在sqlSession對象中有一個數據結構(HashMap)用於存儲緩存數據。

如下圖:
技術分享圖片

從圖上,我們可以看出,一級緩存區域是根據SqlSession為單位劃分的。每次查詢都會先從緩存區域找,如果找不到就會從數據庫查詢數據,然後將查詢到的數據寫入一級緩存中。Mybatis內部存儲緩存使用的是一個HashMap對象,key為 hashCode + sqlId + sql 語句。而value值就是從查詢出來映射生成的java對象。而為了保證緩存裏面的數據肯定是準確數據避免臟讀,每次我們進行數據修改後(增、刪、改操作)就會執行commit操作,清空緩存區域。

我們可以來寫一個測試用例,測試一下同一個數據的第二次查詢是否沒有訪問數據庫,代碼如下:

package org.zero01.test;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.*;
import org.json.JSONObject;
import org.junit.Test;
import org.zero01.dao.StudentMapper;
import org.zero01.pojo.Student;

import java.io.IOException;
import java.io.InputStream;

public class TestMybatisCache {

    @Test
    public void testMybatisCache() throws IOException {
        String confPath = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(confPath);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);

        // 進行第一次查詢
        Student student1 = studentMapper.selectByPrimaryKey(1);
        System.out.println("sqlSession1 第一次查詢:" + new JSONObject(student1));

        // 進行第二次查詢
        Student student2 = studentMapper.selectByPrimaryKey(1);
        System.out.println("sqlSession1 第二次查詢:" + new JSONObject(student2));

        sqlSession.close();
    }
}

控制臺打印結果:
技術分享圖片

如上,可以看到只有第一次查詢訪問了數據庫。第二次查詢則沒有訪問數據庫,是從內存中直接讀取出來的數據。

我們上面也提到了,如果進行了增、刪、改的sql操作並進行了事務的commit提交操作後,SqlSession中的一級緩存就會被清空,不會導致臟數據的出現。同樣的,我們可以使用測試用例來演示這一點,修改測試代碼如下:

@Test
public void testMybatisCache() throws IOException {
    String confPath = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(confPath);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession sqlSession = sqlSessionFactory.openSession();
    StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);

    // 進行第一次查詢
    Student student1 = studentMapper.selectByPrimaryKey(2);
    System.out.println("sqlSession1 第一次查詢:" + new JSONObject(student1));

    Student stuUpdate = new Student();
    stuUpdate.setSid(2);
    stuUpdate.setSname("渣渣輝");
    stuUpdate.setAge(21);
    int rowCount = studentMapper.updateByPrimaryKeySelective(stuUpdate);
    if (rowCount > 0) {
        sqlSession.commit();
        System.out.println("更新student數據成功");
    }

    // 進行第二次查詢
    Student student2 = studentMapper.selectByPrimaryKey(2);
    System.out.println("sqlSession1 第二次查詢:" + new JSONObject(student2));

    sqlSession.close();
}

控制臺打印結果:
技術分享圖片

如上,可以看到當數據更新成功並commit後,會清空SqlSession中的一級緩存,第二次查詢就會訪問數據庫查詢最新的數據了。

不同的sqlSession之間的緩存數據區域(HashMap)是互相不影響的。所以在這種情況下,是不能實現跨表的session共享的。有一點值得註意的是,由於不同的sqlSession之間的緩存數據區域不共享,如果使用多個SqlSession對數據庫進行操作時,就會出現臟數據。我們可以修改之前的測試用例來演示這個現象,修改測試代碼如下:

@Test
public void testMybatisCache() throws IOException {
    String confPath = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(confPath);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();

    // 使用sqlSession1進行第一次查詢
    StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
    Student student = studentMapper.selectByPrimaryKey(1);
    System.out.println("sqlSession1 第一次查詢:" + new JSONObject(student));

    // 使用sqlSession2進行數據的更新
    StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
    Student student2 = new Student();
    student2.setSid(1);
    student2.setSname("渣渣輝");
    student2.setAge(21);
    int rowCount = studentMapper2.updateByPrimaryKeySelective(student2);
    if (rowCount > 0) {
        sqlSession2.commit();
        System.out.println("sqlSession2 更新student數據成功");
    }

    // 使用sqlSession1進行第二次查詢
    student = studentMapper.selectByPrimaryKey(1);
    System.out.println("sqlSession1 第二次查詢:" + new JSONObject(student));

    sqlSession1.close();
    sqlSession2.close();
}

控制臺打印結果:

sqlSession1 第一次查詢:{"address":"湖南","sname":"小明","sex":"男","age":16,"sid":1,"cid":1}
sqlSession2 更新student數據成功
sqlSession1 第二次查詢:{"address":"湖南","sname":"小明","sex":"男","age":16,"sid":1,"cid":1}

具體的日誌細節如下圖:
技術分享圖片

由此可見,Mybatis的一級緩存只存在於SqlSession中,可以提高我們的查詢性能,降低數據庫壓力,但是不能實現多sql的session共享,所以使用多個SqlSession操作數據庫會產生臟數據。


二級緩存

二級緩存是mapper級別的緩存,多個SqlSession去操作同一個Mapper的sql語句,多個SqlSession可以共用二級緩存,二級緩存是可以橫跨跨SqlSession的。

示意圖:
技術分享圖片

二級緩存區域是根據mapper的namespace劃分的,相同namespace的mapper查詢數據放在同一個區域,如果使用mapper代理方法每個mapper的namespace都不同,此時可以理解為二級緩存區域是根據mapper劃分,也就是根據命名空間來劃分的,如果兩個mapper文件的命名空間一樣,那樣,不同的SqlSession之間就可以共享一個mapper緩存。

示意圖:
技術分享圖片

在默認情況下是沒有開啟二級緩存的,除了局部的 session 緩存。而在一級緩存中我們也介紹了,不同的SqlSession之間的一級緩存是不共享的,所以如果我們用兩個SqlSession去查詢同一個數據,都會往數據庫發送sql。這一點,我們也可以通過測試用例進行測試,測試代碼如下:

@Test
public void testMybatisCache2() throws IOException {
    String confPath = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(confPath);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();

    // 使用sqlSession1進行第一次查詢
    StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
    Student student = studentMapper.selectByPrimaryKey(1);
    System.out.println("sqlSession1 第一次查詢:" + new JSONObject(student));

    sqlSession1.close();

    // 使用sqlSession2進行第一次查詢
    StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
    Student student2 = studentMapper2.selectByPrimaryKey(1);
    System.out.println("sqlSession2 第一次查詢:" + new JSONObject(student2));

    sqlSession2.close();
}

控制臺輸出結果:
技術分享圖片

如果想要開啟二級緩存,你需要在你的mybatis主配置文件裏加入:

<settings>
    <!-- 對在此配置文件下的所有cache進行全局性的開/關設置。默認值:true -->
    <setting name="cacheEnabled" value="true"/>
</settings>

然後在需要被緩存的 SQL 映射文件中添加一行cache配置即可:

...
<mapper namespace="org.zero01.dao.StudentMapper">
    ...
    <cache/>
    ...
</mapper>

字面上看就是這樣。這個簡單語句的效果如下:

  • 映射語句文件中的所有 select 語句將會被緩存。
  • 映射語句文件中的所有 insert,update 和 delete 語句會刷新緩存。
  • 緩存會使用 Least Recently Used(LRU,最近最少使用的)算法來收回。
  • 根據時間表(比如 no Flush Interval,沒有刷新間隔), 緩存不會以任何時間順序 來刷新。
  • 緩存會存儲列表集合或對象(無論查詢方法返回什麽)的 1024 個引用。
  • 緩存會被視為是 read/write(可讀/可寫)的緩存,意味著對象檢索不是共享的,而 且可以安全地被調用者修改,而不幹擾其他調用者或線程所做的潛在修改。

註:緩存只適用於緩存標記所在的映射文件中聲明的語句。如果你使用的是java的API和XML映射文件一起,默認情況下不會緩存接口中聲明的語句。你需要把緩存區使用@CacheNamespaceRef註解進行聲明。

假如說,已開啟二級緩存的Mapper中有個statement要求禁用怎麽辦,那也不難,只需要在statement中設置useCache="false"就可以禁用當前select語句的二級緩存,也就是每次都會生成sql去查詢,ps:默認情況下默認是true,也就是默認使用二級緩存。如下示例:

<select id="findAll" resultMap="BaseResultMap" useCache="false">
    select
    <include refid="Base_Column_List"/>
    from
    student
</select>

除此之外,還有個flushCache屬性,該屬性用於刷新緩存,將其設置為 true時,任何時候只要語句被調用,都會導致一級緩存和二級緩存都會被清空,默認值:false。在mapper的同一個namespace中,如果有其他insert、update、delete操作後都需要執行刷新緩存操作,來避免臟讀。這時我們只需要設置statement配置中的flushCache="true"屬性,就會默認刷新緩存,相反如果是false就不會了。當然,不管開不開緩存刷新功能,你要是手動更改數據庫表,那都肯定不能避免臟讀的發生,那就屬於手賤了。如下示例:

<select id="findAll" resultMap="BaseResultMap" flushCache="true">
    ...
</select>

那既然能夠刷新緩存,能定時刷新嗎?也就是設置時間間隔來刷新緩存,答案是肯定的。我們在mapper映射文件中添加&lt;cache/&gt;來表示開啟緩存,所以我們就可以通過&lt;cache/&gt;元素的屬性來進行配置。比如:

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

這個更高級的配置創建了一個 FIFO 緩存,並每隔 60 秒刷新,存數結果對象或列表的 512 個引用,而且返回的對象被認為是只讀的,因此在不同線程中的調用者之間修改它們會導致沖突。

  • flushInterval(刷新間隔) 可以被設置為任意的正整數,而且它們代表一個合理的毫秒 形式的時間段。默認情況是不設置,也就是沒有刷新間隔,緩存僅僅調用語句時刷新。
  • size(引用數目) 可以被設置為任意正整數,要記住你緩存的對象數目和你運行環境的 可用內存資源數目。默認值是 1024。
  • readOnly(只讀) 屬性可以被設置為 true 或 false。只讀的緩存會給所有調用者返回緩 存對象的相同實例。因此這些對象不能被修改。這提供了很重要的性能優勢。可讀寫的緩存 會返回緩存對象的拷貝(通過序列化) 。這會慢一些,但是安全,因此默認是 false。

可用的收回策略有:

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

開啟了二級緩存之後,我們再來進行測試,但是在運行測試用例之前,我們需要給pojo類加上實現序列化接口的代碼,不然在關閉SqlSession的時候就會報錯,代碼如下:

package org.zero01.pojo;

import java.io.Serializable;

public class Student implements Serializable {
    ...
}

測試代碼不變,運行後,控制臺輸出結果如下:
技術分享圖片

可以看到,開啟二級緩存後,SqlSession之間的數據就可以通過二級緩存共享了,和一級緩存一樣,當執行了insert、update、delete等操作並commit提交後就會清空二級緩存區域。當一級緩存和二級緩存同時存在時,會先訪問二級緩存,再去訪問各自的一級緩存,如果都沒有需要的數據,才會往數據庫發送sql進行查詢。這一點,我們也可以通過測試用例來進行測試,測試代碼如下:

@Test
public void testMybatisCache() throws IOException {
    String confPath = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(confPath);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();

    // 使用sqlSession1進行第一次查詢
    StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
    Student student = studentMapper.selectByPrimaryKey(1);
    System.out.println("sqlSession1 第一次查詢:" + new JSONObject(student));

    // 使用sqlSession2進行數據的更新
    StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
    Student student2 = new Student();
    student2.setSid(1);
    student2.setSname("小明");
    student2.setAge(16);
    int rowCount = studentMapper2.updateByPrimaryKeySelective(student2);
    if (rowCount > 0) {
        sqlSession2.commit();
        System.out.println("sqlSession2 更新student數據成功");
    }

    // 使用sqlSession1進行第二次查詢
    student = studentMapper.selectByPrimaryKey(1);
    System.out.println("sqlSession1 第二次查詢:" + new JSONObject(student));

    // 使用sqlSession2進行第一次查詢
    student2 = studentMapper2.selectByPrimaryKey(1);
    System.out.println("sqlSession2 第一次查詢:" + new JSONObject(student2));

    // 關閉會話
    sqlSession1.close();
    sqlSession2.close();
}

運行測試代碼後,控制臺輸出結果如下:
技術分享圖片

通過此測試用例可以看出兩點:

  • 1.Mybatis的二級緩存是跨Session的,每個Mapper享有同一個二級緩存域,同樣,每次執行commit操作之後,會清空二級緩存區域。
  • 2.如果數據存在一級緩存的話,依舊會去一級緩存中讀取數據,這樣會發生臟讀現象,不過我們可以在相應的statement中,設置flushCache="true",這樣每次都會清除緩存,並向數據發送sql來進行查詢。

或者全局關閉本地、二級緩存:

<settings>
    <setting name="cacheEnabled" value="false"/>
    <setting name="localCacheScope" value="STATEMENT"/>
</settings>

但是在使用多個sqlSession操作數據庫的時候,還有一個需要註意的問題,那就是事務隔離級別,mysql的默認事務隔離級別是REPEATABLE-READ(可重復讀)。這樣當多個sqlsession操作同一個數據的時候,可能會導致兩個不同的事務查詢出來的數據不一致,例如,sqlsession1 在同一個事務中讀取了兩次數據,而 sqlsession2 在 sqlsession1 第一次查詢之後就更新了數據,那麽由於可重復讀的原因,sqlsession1 第二次查詢到的依舊是之前的數據。

我們可以使用測試用例來測試一下,首先得關閉本地緩存或者在相應的statement中設置flushCache屬性值為true,測試用例代碼如下:

@Test
public void testMybatisCache() throws IOException {
    String confPath = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(confPath);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();

    // 使用sqlSession1進行第一次查詢
    StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
    Student student = studentMapper.selectByPrimaryKey(1);
    System.out.println("sqlSession1 第一次查詢:" + new JSONObject(student));

    // 使用sqlSession2進行數據的更新
    StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
    Student student2 = new Student();
    student2.setSid(1);
    student2.setSname("小明");
    student2.setAge(16);
    int rowCount = studentMapper2.updateByPrimaryKeySelective(student2);
    if (rowCount > 0) {
        sqlSession2.commit();
        System.out.println("sqlSession2 更新student數據成功");
    }

    // 使用sqlSession1進行第二次查詢
    student = studentMapper.selectByPrimaryKey(1);
    System.out.println("sqlSession1 第二次查詢:" + new JSONObject(student));

    // 使用sqlSession2進行第一次查詢
    student2 = studentMapper2.selectByPrimaryKey(1);
    System.out.println("sqlSession2 第一次查詢:" + new JSONObject(student2));

    // 關閉會話
    sqlSession1.close();
    sqlSession2.close();
}

控制臺輸出結果:
技術分享圖片

這就是mysql默認事務隔離級別REPEATABLE-READ(可重復讀)導致的現象,這種隔離級別能夠保證同一個事務的生命周期內,讀取的數據是一致的,但是兩個不同的事務之間讀取出來的數據就可能不一致。

不過,如果你希望在不同的事務的生命周期內讀取的數據一致的話,就需要把事務隔離級別改成READ-COMMITTED(讀已提交),該級別會導致不可重復讀,也就是說在同一個事務的生命周期內讀取到的數據可能是不一致的,而在兩個不同的事務之間讀取的數據則是一致的。同樣的我們可以使用測試用例進行測試,修改測試代碼如下:

@Test
public void testMybatisCache() throws IOException {
    String confPath = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(confPath);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    // 設置事務隔離級別為讀已提交
    SqlSession sqlSession1 = sqlSessionFactory.openSession(TransactionIsolationLevel.READ_COMMITTED);
    SqlSession sqlSession2 = sqlSessionFactory.openSession(TransactionIsolationLevel.READ_COMMITTED);

    // 使用sqlSession1進行第一次查詢
    StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
    Student student = studentMapper.selectByPrimaryKey(1);
    System.out.println("sqlSession1 第一次查詢:" + new JSONObject(student));

    // 使用sqlSession2進行數據的更新
    StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
    Student student2 = new Student();
    student2.setSid(1);
    student2.setSname("阿基米德");
    student2.setAge(22);
    int rowCount = studentMapper2.updateByPrimaryKeySelective(student2);
    if (rowCount > 0) {
        sqlSession2.commit();
        System.out.println("sqlSession2 更新student數據成功");
    }

    // 使用sqlSession1進行第二次查詢
    student = studentMapper.selectByPrimaryKey(1);
    System.out.println("sqlSession1 第二次查詢:" + new JSONObject(student));

    // 使用sqlSession2進行第一次查詢
    student2 = studentMapper2.selectByPrimaryKey(1);
    System.out.println("sqlSession2 第一次查詢:" + new JSONObject(student2));

    // 關閉會話
    sqlSession1.close();
    sqlSession2.close();
}

控制臺輸出結果:
技術分享圖片

可以看到,設置成讀已提交後,兩個事務在數據更新後查詢出來的數據是一致的了。至於是使用可重復讀還是讀已提交,就取決於實際的業務需求了,如果希望同一個事務的生命周期內,讀取的數據是一致的,就使用可重復讀級別。如果希望兩個不同的事務之間查詢出來的數據是一致的,那麽就使用讀已提交級別。


使用自定義緩存

mybatis自身的緩存做的並不完美,不過除了使用mybatis自帶的二級緩存, 你也可以使用你自己實現的緩存或者其他第三方的緩存方案創建適配器來完全覆蓋緩存行為。所以它提供了使用自定義緩存的機會,我們可以選擇使用我們喜歡的自定義緩存,下面將介紹一下,使用ehcache作為mybatis的自定義緩存的具體步驟。

首先,要想使用mybatis自定義緩存,就必須讓自定義緩存類實現mybatis提供的Cache 接口(org.apache.ibatis.cache.Cache):

public interface Cache {
  // 獲取緩存編號
  String getId();

  // 獲取緩存對象的大小
  int getSize();

  // 保存key值緩存對象
  void putObject(Object key, Object value);

  // 通過kEY獲取值
  Object getObject(Object key);

  // 緩存中是否有某個key
  boolean hasKey(Object key);

  // 獲取緩存的讀寫鎖
  ReadWriteLock getReadWriteLock();

  // 通過key刪除緩存對象
  Object removeObject(Object key);

  // 清空緩存
  void clear();
}

我們要使用ehcache做自定義緩存,就應該完成這個自定義緩存類,但mybatis的git上提供了相對於的適配包,我們只需要下載即可,下面是適配包的maven依賴:

<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-ehcache</artifactId>
    <version>1.1.0</version>
</dependency>

接著在相應的 mapper xml文件中配置相應的緩存實現類:

<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>

實現Cache接口的是EhcacheCache的父類AbstractEhcacheCache,我們可以看一下它的源碼:

package org.mybatis.caches.ehcache;

import java.util.concurrent.locks.ReadWriteLock;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Ehcache;
import net.sf.ehcache.Element;
import org.apache.ibatis.cache.Cache;

public abstract class AbstractEhcacheCache implements Cache {
    protected static CacheManager CACHE_MANAGER = CacheManager.create();
    protected final String id;
    protected Ehcache cache;

    public AbstractEhcacheCache(String id) {
        if (id == null) {
            throw new IllegalArgumentException("Cache instances require an ID");
        } else {
            this.id = id;
        }
    }

    public void clear() {
        this.cache.removeAll();
    }

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

    public Object getObject(Object key) {
        Element cachedElement = this.cache.get(key);
        return cachedElement == null ? null : cachedElement.getObjectValue();
    }

    public int getSize() {
        return this.cache.getSize();
    }

    public void putObject(Object key, Object value) {
        this.cache.put(new Element(key, value));
    }

    public Object removeObject(Object key) {
        Object obj = this.getObject(key);
        this.cache.remove(key);
        return obj;
    }

    public void unlock(Object key) {
    }

    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        } else if (obj == null) {
            return false;
        } else if (!(obj instanceof Cache)) {
            return false;
        } else {
            Cache otherCache = (Cache)obj;
            return this.id.equals(otherCache.getId());
        }
    }

    public int hashCode() {
        return this.id.hashCode();
    }

    public ReadWriteLock getReadWriteLock() {
        return null;
    }

    public String toString() {
        return "EHCache {" + this.id + "}";
    }

    public void setTimeToIdleSeconds(long timeToIdleSeconds) {
        this.cache.getCacheConfiguration().setTimeToIdleSeconds(timeToIdleSeconds);
    }

    public void setTimeToLiveSeconds(long timeToLiveSeconds) {
        this.cache.getCacheConfiguration().setTimeToLiveSeconds(timeToLiveSeconds);
    }

    public void setMaxEntriesLocalHeap(long maxEntriesLocalHeap) {
        this.cache.getCacheConfiguration().setMaxEntriesLocalHeap(maxEntriesLocalHeap);
    }

    public void setMaxEntriesLocalDisk(long maxEntriesLocalDisk) {
        this.cache.getCacheConfiguration().setMaxEntriesLocalDisk(maxEntriesLocalDisk);
    }

    public void setMemoryStoreEvictionPolicy(String memoryStoreEvictionPolicy) {
        this.cache.getCacheConfiguration().setMemoryStoreEvictionPolicy(memoryStoreEvictionPolicy);
    }
}

接著我們還需要在resources目錄下,創建ehcache的配置文件:ehcache.xml,文件內容如下:

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">

    <diskStore path="java.io.tmpdir"/>

    <!--
    Mandatory Default Cache configuration. These settings will be applied to caches
    created programmtically using CacheManager.add(String cacheName)
    -->
    <!--
       name:緩存名稱。
       maxElementsInMemory:緩存最大個數。
       eternal:對象是否永久有效,一但設置了,timeout將不起作用。
       timeToIdleSeconds:設置對象在失效前的允許閑置時間(單位:秒)。僅當eternal=false對象不是永久有效時使用,可選屬性,默認值是0,也就是可閑置時間無窮大。
       timeToLiveSeconds:設置對象在失效前允許存活時間(單位:秒)。最大時間介於創建時間和失效時間之間。僅當eternal=false對象不是永久有效時使用,默認是0.,也就是對象存活時間無窮大。
       overflowToDisk:當內存中對象數量達到maxElementsInMemory時,Ehcache將會對象寫到磁盤中。
       diskSpoolBufferSizeMB:這個參數設置DiskStore(磁盤緩存)的緩存區大小。默認是30MB。每個Cache都應該有自己的一個緩沖區。
       maxElementsOnDisk:硬盤最大緩存個數。
       diskPersistent:是否緩存虛擬機重啟期數據 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
       diskExpiryThreadIntervalSeconds:磁盤失效線程運行時間間隔,默認是120秒。
       memoryStoreEvictionPolicy:當達到maxElementsInMemory限制時,Ehcache將會根據指定的策略去清理內存。默認策略是LRU(最近最少使用)。你可以設置為FIFO(先進先出)或是LFU(較少使用)。
       clearOnFlush:內存數量最大時是否清除。
    -->
    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="5"
            timeToLiveSeconds="5"
            overflowToDisk="true"
            maxElementsOnDisk="10000000"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU"
    />
</ehcache>

以上就完成了自定義緩存的配置,接下來我們測試一下緩存是否生效,測試代碼如下:

@Test
public void testMybatisCache2() throws IOException {
    String confPath = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(confPath);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();

    // 使用sqlSession1進行第一次查詢
    StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
    Student student = studentMapper.selectByPrimaryKey(1);
    System.out.println("sqlSession1 第一次查詢:" + new JSONObject(student));

    sqlSession1.close();

    // 使用sqlSession2進行第一次查詢
    StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
    Student student2 = studentMapper2.selectByPrimaryKey(1);
    System.out.println("sqlSession2 第一次查詢:" + new JSONObject(student2));

    sqlSession2.close();
}

控制臺輸出結果:
技術分享圖片

可以看到,sqlsession2 查詢數據的時候緩存命中率為0.5,並且也沒有向數據庫發送sql語句,那麽就代表我們配置的自定義緩存生效並可以成功緩存數據了。

Mybatis的緩存機制詳解