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

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

mybatis 也提供了對緩存的支持, 分爲一級緩存和二級緩存。 但是在默認的情況下, 只開啓一級緩存(一級緩存是對同一個 SqlSession 而言的)。

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

對以下的代碼, 你也可以從我的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 [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”, 因此 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 中。 否則可能會出現數據不一致的情況。

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