1. 程式人生 > >mybatis 快取的使用, 看這篇就夠了

mybatis 快取的使用, 看這篇就夠了

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

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

對以下的程式碼, 你也可以從我的GitHub中獲取相應的專案。

1 一級快取

同一個 SqlSession 物件, 在引數和 SQL 完全一樣的情況先, 只執行一次 SQL 語句(如果快取沒有過期)

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

1.1 同一個 SqlSession

@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 語句, 直接從記憶體中獲取了結果。

而且兩次結果輸入一致, 同時斷言兩個物件相同也通過。

1.2 不同的 SqlSession

 @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();
        }
    }
}

在程式碼中, 分別使用 sqlSessionsqlSession2 進行了相同的查詢。

其結果如下

不同SqlSession執行結果

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

1.3 重新整理快取

重新整理快取是清空這個 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.4 總結

  1. 在同一個 SqlSession 中, Mybatis 會把執行的方法和引數通過演算法生成快取的鍵值, 將鍵值和結果存放在一個 Map 中, 如果後續的鍵值一樣, 則直接從 Map 中獲取資料;

  2. 不同的 SqlSession 之間的快取是相互隔離的;

  3. 用一個 SqlSession, 可以通過配置使得在查詢前清空快取;

  4. 任何的 UPDATE, INSERT, DELETE 語句都會清空快取。

2 二級快取

二級快取存在於 SqlSessionFactory 生命週期中。

2.1 配置二級快取

2.1.1 全域性開關

在 mybatis 中, 二級快取有全域性開關和分開關, 全域性開關, 在 mybatis-config.xml 中如下配置:

<settings>
  <!--全域性地開啟或關閉配置檔案中的所有對映器已經配置的任何快取。 -->
  <setting name="cacheEnabled" value="true"/>
</settings>

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

2.1.2 分開關

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

2.1.3 entity 實現序列化介面

public class Student implements Serializable {

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

2.2 使用二級快取

@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 [[email protected]]
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 [[email protected]]
2018-09-29 23:14:27,116 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Closing JDBC Connection [[email protected]]
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”, 因此 student2student3 都是反序列化得到的, 為不同的例項。

2.3 配置詳解

檢視 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.

2.3.1 type

type 用於指定快取的實現型別, 預設是PERPETUAL, 對應的是 mybatis 本身的快取實現類 org.apache.ibatis.cache.impl.PerpetualCache

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

2.3.2 eviction

eviction 對應的是回收策略, 預設為 LRU

  1. LRU: 最近最少使用, 移除最長時間不被使用的物件。

  2. FIFO: 先進先出, 按物件進入快取的順序來移除物件。

  3. SOFT: 軟引用, 移除基於垃圾回收器狀態和軟引用規則的物件。

  4. WEAK: 弱引用, 移除基於垃圾回收器狀態和弱引用規則的物件。

2.3.3 flushInterval

flushInterval 對應重新整理間隔, 單位毫秒, 預設值不設定, 即沒有重新整理間隔, 快取僅僅在重新整理語句時重新整理。

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

2.3.4 size

size 對應為引用的數量,即最多的快取物件資料, 預設為 1024

2.3.5 readOnly

readOnly 為只讀屬性, 預設為 false

  1. false: 可讀寫, 在建立物件時, 會通過反序列化得到快取物件的拷貝。 因此在速度上會相對慢一點, 但重在安全。

  2. true: 只讀, 只讀的快取會給所有呼叫者返回快取物件的相同例項。 因此效能很好, 但如果修改了物件, 有可能會導致程式出問題。

2.3.6 blocking

blocking 為阻塞, 預設值為 false。 當指定為 true 時將採用 BlockingCache 進行封裝。

使用 BlockingCache 會在查詢快取時鎖住對應的 Key,如果快取命中了則會釋放對應的鎖,否則會在查詢資料庫以後再釋放鎖,這樣可以阻止併發情況下多個執行緒同時查詢資料。
blocking

2.4 注意事項

  1. 由於在更新時會重新整理快取, 因此需要注意使用場合:查詢頻率很高, 更新頻率很低時使用, 即經常使用 select, 相對較少使用delete, insert, update

  2. 快取是以 namespace 為單位的,不同 namespace 下的操作互不影響。但重新整理快取是重新整理整個 namespace 的快取, 也就是你 update 了一個, 則整個快取都重新整理了。

  3. 最好在 「只有單表操作」 的表的 namespace 使用快取, 而且對該表的操作都在這個 namespace 中。 否則可能會出現資料不一致的情況。