天天看點

mybatis 緩存的使用, 看這篇就夠了

@

目錄

  • 1 一級緩存
    • 1.1 同一個 SqlSession
    • 1.2 不同的 SqlSession
    • 1.3 重新整理緩存
    • 1.4 總結
  • 2 二級緩存
    • 2.1 配置二級緩存
      • 2.1.1 全局開關
      • 2.1.2 分開關
      • 2.1.3 entity 實作序列化接口
    • 2.2 使用二級緩存
    • 2.3 配置詳解
      • 2.3.1 type
      • 2.3.2 eviction
      • 2.3.3 flushInterval
      • 2.3.4 size
      • 2.3.5 readOnly
      • 2.3.6 blocking
    • 2.4 注意事項

緩存的重要性是不言而喻的。 使用緩存, 我們可以避免頻繁的與資料庫進行互動, 尤其是在查詢越多、緩存命中率越高的情況下, 使用緩存對性能的提高更明顯。

mybatis 也提供了對緩存的支援, 分為一級緩存和二級緩存。 但是在預設的情況下, 隻開啟一級緩存(一級緩存是對同一個 SqlSession 而言的)。

以下的項目是在mybatis 初步使用(IDEA的Maven項目, 超詳細)的基礎上進行。

對以下的代碼, 你也可以從我的GitHub中擷取相應的項目。

同一個

SqlSession

對象, 在參數和 SQL 完全一樣的情況先, 隻執行一次 SQL 語句(如果緩存沒有過期)

也就是隻有在參數和 SQL 完全一樣的情況下, 才會有這種情況。

@Test
public void oneSqlSession() {
    SqlSession sqlSession = null;
    try {
        sqlSession = sqlSessionFactory.openSession();

        StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
        // 執行第一次查詢
        List<Student> students = studentMapper.selectAll();
        for (int i = 0; i < students.size(); i++) {
            System.out.println(students.get(i));
        }
        System.out.println("=============開始同一個 Sqlsession 的第二次查詢============");
        // 同一個 sqlSession 進行第二次查詢
        List<Student> stus = studentMapper.selectAll();
        Assert.assertEquals(students, stus);
        for (int i = 0; i < stus.size(); i++) {
            System.out.println("stus:" + stus.get(i));
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (sqlSession != null) {
            sqlSession.close();
        }
    }
}
           

在以上的代碼中, 進行了兩次查詢, 使用相同的

SqlSession

, 結果如下

在日志和輸出中:

第一次查詢發送了 SQL 語句, 後傳回了結果;

第二次查詢沒有發送 SQL 語句, 直接從記憶體中擷取了結果。

而且兩次結果輸入一緻, 同時斷言兩個對象相同也通過。

@Test
public void differSqlSession() {
    SqlSession sqlSession = null;
    SqlSession sqlSession2 = null;
    try {
        sqlSession = sqlSessionFactory.openSession();

        StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
        // 執行第一次查詢
        List<Student> students = studentMapper.selectAll();
        for (int i = 0; i < students.size(); i++) {
            System.out.println(students.get(i));
        }
        System.out.println("=============開始不同 Sqlsession 的第二次查詢============");
        // 從新建立一個 sqlSession2 進行第二次查詢
        sqlSession2 = sqlSessionFactory.openSession();
        StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
        List<Student> stus = studentMapper2.selectAll();
        // 不相等
        Assert.assertNotEquals(students, stus);
        for (int i = 0; i < stus.size(); i++) {
            System.out.println("stus:" + stus.get(i));
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (sqlSession != null) {
            sqlSession.close();
        }
        if (sqlSession2 != null) {
            sqlSession2.close();
        }
    }
}
           

在代碼中, 分别使用 sqlSession 和 sqlSession2 進行了相同的查詢。

其結果如下

從日志中可以看到兩次查詢都分别從資料庫中取出了資料。 雖然結果相同, 但兩個是不同的對象。

重新整理緩存是清空這個 SqlSession 的所有緩存, 不單單是某個鍵。

@Test
public void sameSqlSessionNoCache() {
    SqlSession sqlSession = null;
    try {
        sqlSession = sqlSessionFactory.openSession();

        StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
        // 執行第一次查詢
        Student student = studentMapper.selectByPrimaryKey(1);
        System.out.println("=============開始同一個 Sqlsession 的第二次查詢============");
        // 同一個 sqlSession 進行第二次查詢
        Student stu = studentMapper.selectByPrimaryKey(1);
        Assert.assertEquals(student, stu);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (sqlSession != null) {
            sqlSession.close();
        }
    }
}
           

如果是以上, 沒什麼不同, 結果還是第二個不發 SQL 語句。

在此, 做一些修改, 在 StudentMapper.xml 中, 添加

flushCache="true"

修改後的配置檔案如下:

<select id="selectByPrimaryKey" flushCache="true" parameterType="java.lang.Integer" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List" />
    from student
    where student_id=#{id, jdbcType=INTEGER}
</select>
           

結果如下:

第一次, 第二次都發送了 SQL 語句, 同時, 斷言兩個對象相同出錯。

  1. 在同一個

    SqlSession

    中, Mybatis 會把執行的方法和參數通過算法生成緩存的鍵值, 将鍵值和結果存放在一個 Map 中, 如果後續的鍵值一樣, 則直接從 Map 中擷取資料;
  2. 不同的

    SqlSession

    之間的緩存是互相隔離的;
  3. 用一個

    SqlSession

    , 可以通過配置使得在查詢前清空緩存;
  4. 任何的 UPDATE, INSERT, DELETE 語句都會清空緩存。

二級緩存存在于 SqlSessionFactory 生命周期中。

在 mybatis 中, 二級緩存有全局開關和分開關, 全局開關, 在 mybatis-config.xml 中如下配置:

<settings>
  <!--全局地開啟或關閉配置檔案中的所有映射器已經配置的任何緩存。 -->
  <setting name="cacheEnabled" value="true"/>
</settings>
           

預設是為 true, 即預設開啟總開關。

分開關就是說在 *Mapper.xml 中開啟或關閉二級緩存, 預設是不開啟的。

public class Student implements Serializable {

    private static final long serialVersionUID = -4852658907724408209L;
    
    ...
    
}
           

@Test
public void secendLevelCacheTest() {

    // 擷取 SqlSession 對象
    SqlSession sqlSession = sqlSessionFactory.openSession();
    //  擷取 Mapper 對象
    StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
    // 使用 Mapper 接口的對應方法,查詢 id=2 的對象
    Student student = studentMapper.selectByPrimaryKey(2);
    // 更新對象的名稱
    student.setName("奶茶");
    // 再次使用相同的 SqlSession 查詢id=2 的對象
    Student student1 = studentMapper.selectByPrimaryKey(2);
    Assert.assertEquals("奶茶", student1.getName());
    // 同一個 SqlSession , 此時是一級緩存在作用, 兩個對象相同
    Assert.assertEquals(student, student1);

    sqlSession.close();

    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    StudentMapper studentMapper1 = sqlSession1.getMapper(StudentMapper.class);
    Student student2 = studentMapper1.selectByPrimaryKey(2);
    Student student3 = studentMapper1.selectByPrimaryKey(2);
    // 由于我們配置的 readOnly="true", 是以後續同一個 SqlSession 的對象都不一樣
    Assert.assertEquals("奶茶", student2.getName());
    Assert.assertNotEquals(student3, student2);

    sqlSession1.close();
}

           
2018-09-29 23:14:26,889 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Created connection 242282810.
2018-09-29 23:14:26,889 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@e70f13a]
2018-09-29 23:14:26,897 [main] DEBUG [com.homejim.mybatis.mapper.StudentMapper.selectByPrimaryKey] - ==>  Preparing: select student_id, name, phone, email, sex, locked, gmt_created, gmt_modified from student where student_id=? 
2018-09-29 23:14:26,999 [main] DEBUG [com.homejim.mybatis.mapper.StudentMapper.selectByPrimaryKey] - ==> Parameters: 2(Integer)
2018-09-29 23:14:27,085 [main] TRACE [com.homejim.mybatis.mapper.StudentMapper.selectByPrimaryKey] - <==    Columns: student_id, name, phone, email, sex, locked, gmt_created, gmt_modified
2018-09-29 23:14:27,085 [main] TRACE [com.homejim.mybatis.mapper.StudentMapper.selectByPrimaryKey] - <==        Row: 2, 小麗, 13821378271, [email protected], 0, 0, 2018-09-04 18:27:42.0, 2018-09-04 18:27:42.0
2018-09-29 23:14:27,093 [main] DEBUG [com.homejim.mybatis.mapper.StudentMapper.selectByPrimaryKey] - <==      Total: 1
2018-09-29 23:14:27,093 [main] DEBUG [com.homejim.mybatis.mapper.StudentMapper] - Cache Hit Ratio [com.homejim.mybatis.mapper.StudentMapper]: 0.0
2018-09-29 23:14:27,108 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@e70f13a]
2018-09-29 23:14:27,116 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@e70f13a]
2018-09-29 23:14:27,116 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Returned connection 242282810 to pool.
2018-09-29 23:14:27,124 [main] DEBUG [com.homejim.mybatis.mapper.StudentMapper] - Cache Hit Ratio [com.homejim.mybatis.mapper.StudentMapper]: 0.3333333333333333
2018-09-29 23:14:27,124 [main] DEBUG [com.homejim.mybatis.mapper.StudentMapper] - Cache Hit Ratio [com.homejim.mybatis.mapper.StudentMapper]: 0.5
           

以上結果, 分幾個過程解釋:

第一階段:

  1. 在第一個

    SqlSession

    中, 查詢出

    student

    對象, 此時發送了 SQL 語句;
  2. student

    更改了

    name

    屬性;
  3. SqlSession

    再次查詢出

    student1

    對象, 此時不發送 SQL 語句, 日志中列印了 「Cache Hit Ratio」, 代表二級緩存使用了, 但是沒有命中。 因為一級緩存先作用了。
  4. 由于是一級緩存, 是以, 此時兩個對象是相同的。
  5. 調用了

    sqlSession.close()

    , 此時将資料序列化并保持到二級緩存中。

第二階段:

  1. 新建立一個

    sqlSession.close()

    對象;
  2. 查詢出

    student2

    對象,直接從二級緩存中拿了資料, 是以沒有發送 SQL 語句, 此時查了 3 個對象,但隻有一個命中, 是以 命中率 1/3=0.333333;
  3. student3

    對象,直接從二級緩存中拿了資料, 是以沒有發送 SQL 語句, 此時查了 4 個對象,但隻有一個命中, 是以 命中率 2/4=0.5;
  4. 由于

    readOnly="true"

    , 是以

    student2

    student3

    都是反序列化得到的, 為不同的執行個體。

檢視 dtd 檔案, 可以看到如下限制:

<!ELEMENT cache (property*)>
<!ATTLIST cache
type CDATA #IMPLIED
eviction CDATA #IMPLIED
flushInterval CDATA #IMPLIED
size CDATA #IMPLIED
readOnly CDATA #IMPLIED
blocking CDATA #IMPLIED
>
           

從中可以看出:

  1. cache

    中可以出現任意多個

    property

    子元素;
  2. cache

    有一些可選的屬性

    type

    ,

    eviction

    flushInterval

    size

    readOnly

    blocking

    .

type

用于指定緩存的實作類型, 預設是

PERPETUAL

, 對應的是 mybatis 本身的緩存實作類

org.apache.ibatis.cache.impl.PerpetualCache

後續如果我們要實作自己的緩存或者使用第三方的緩存, 都需要更改此處。

eviction

對應的是回收政策, 預設為 LRU。

  1. LRU: 最近最少使用, 移除最長時間不被使用的對象。
  2. FIFO: 先進先出, 按對象進入緩存的順序來移除對象。
  3. SOFT: 軟引用, 移除基于垃圾回收器狀态和軟引用規則的對象。
  4. WEAK: 弱引用, 移除基于垃圾回收器狀态和弱引用規則的對象。

flushInterval

對應重新整理間隔, 機關毫秒, 預設值不設定, 即沒有重新整理間隔, 緩存僅僅在重新整理語句時重新整理。

如果設定了之後, 到了對應時間會過期, 再次查詢需要從資料庫中取資料。

size

對應為引用的數量,即最多的緩存對象資料, 預設為 1024。

readOnly

為隻讀屬性, 預設為 false

  1. false: 可讀寫, 在建立對象時, 會通過反序列化得到緩存對象的拷貝。 是以在速度上會相對慢一點, 但重在安全。
  2. true: 隻讀, 隻讀的緩存會給所有調用者傳回緩存對象的相同執行個體。 是以性能很好, 但如果修改了對象, 有可能會導緻程式出問題。

blocking

為阻塞, 預設值為 false。 當指定為 true 時将采用

BlockingCache

進行封裝。

使用

BlockingCache

會在查詢緩存時鎖住對應的 Key,如果緩存命中了則會釋放對應的鎖,否則會在查詢資料庫以後再釋放鎖,這樣可以阻止并發情況下多個線程同時查詢資料。

  1. 由于在更新時會重新整理緩存, 是以需要注意使用場合:查詢頻率很高, 更新頻率很低時使用, 即經常使用 select, 相對較少使用delete, insert, update。
  2. 緩存是以 namespace 為機關的,不同 namespace 下的操作互不影響。但重新整理緩存是重新整理整個 namespace 的緩存, 也就是你 update 了一個, 則整個緩存都重新整理了。
  3. 最好在 「隻有單表操作」 的表的 namespace 使用緩存, 而且對該表的操作都在這個 namespace 中。 否則可能會出現資料不一緻的情況。

一起學 mybatis

你想不想來學習 mybatis? 學習其使用和源碼呢?那麼, 在部落格園關注我吧!!

我自己打算把這個源碼系列更新完畢, 同時會更新相應的注釋。快去 star 吧!!

mybatis最新源碼和注釋

mybatis 緩存的使用, 看這篇就夠了

作者:阿進的寫字台

出處:https://www.cnblogs.com/homejim/

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。