1. 程式人生 > >MyBatis-23MyBatis快取配置【二級快取】

MyBatis-23MyBatis快取配置【二級快取】

概述

這裡我們來看下工作中最常用的二級快取。

MyBaits的二級快取可以理解為存在於SqlSessionFactory的生命週期中。

目前還沒接觸過同時存在多個SqlSessionFactory的情況,但可以知道當存在多個SqlSessionFactory時,他們的快取物件都是繫結在各自物件上的,快取資料在一般情況下是不相通的。 只有在使用如redis這樣的快取資料庫時,才可以共享快取。

二級快取的配置

全域性開關cacheEnabled

在MyBatis的全域性配置settings中有一個引數 cacheEnabled , 這個引數是二級快取的全域性開關,預設為true

,初始狀態為啟用狀態。 如果設定為false ,即使後面的二級快取配置,也不會生效。 預設為true,所以不用配置。

MyBatis的二級快取是和名稱空間繫結的,即二級快取需要配置在Mapper.xml對映檔案中或者配置在Mapper.java介面中。 在對映檔案中,名稱空間就是XML根節點mapper的namespace屬性。 在Mapper介面中,名稱空間就是介面的全限定名稱。

Mapper.xml中配置二級快取

在保證二級快取全域性配置開啟的情況下,如果想要給PrivilegeMapper.xml開啟二級快取只需要在PrivilegeMapper.xml中新增 <cache/>

元素即可。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
                    "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >

<!-- 當Mapper介面和XML檔案關聯的時候, namespace的值就需要配置成介面的全限定名稱 -->
<mapper namespace="com.artisan.mybatis.xml.mapper.PrivilegeMapper"
>
<cache/> <!-- 其他配置 --> </mapper>

預設的二級快取功能如下:

  • 對映語句檔案中所有的select語句將會被快取

  • 對映語句檔案中所有的insert update delete 語句會重新整理快取

  • 快取會使用(Least Flush Interval,LRU最近最少使用的)演算法來收回

  • 根據時間表(如 no Flush Interval,沒有重新整理間隔),快取不會以任何時間順序來重新整理

  • 快取會儲存集合或物件(無論查詢方法返回什麼型別的值)的1024個引用

  • 快取會被視為read/wriete(可讀/可寫)的,意味著物件檢索不是共享的,而且可以安全的被呼叫者修改,而不干擾其他呼叫者或者執行緒所做的潛在修改。

所有的這些屬性都是可以通過快取元素的屬性來修改,比如

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

這個更高階的配置建立了一個FIFP快取,每隔60S重新整理一次,儲存集合或物件的512個引用,而且返回的物件被認為是隻讀的,因而在不同執行緒中的呼叫者之間修改它們會導致衝突。

cache可以配置的屬性如下:
- eviction(收回策略)

  • LRU 最近最少使用的,移除最長時間不被使用的物件,這是預設值
  • FIFO 先進先出,按物件進入快取的順序來移除它們
  • SOFT 軟引用,移除基於垃圾回收器狀態和軟引用規則的物件
  • WEAK 弱引用,更積極的移除基於垃圾收集器狀態和弱引用規則的物件
  • flushInterval(重新整理間隔)可以被設定為任意的正整數,而且它們代表一個合理的毫秒形式的時間段。 預設情況不設定,即沒有重新整理間隔,快取僅僅在呼叫語句時重新整理

  • size(引用數目)可以被設定為任意的正整數,要記住快取的物件數目和執行環境的可用記憶體資源數目,預設1024

  • readOnly(只讀)屬性可以被設定為true後者false。 只讀的快取會給所有呼叫者返回快取物件的相同例項,因此這些物件不能被修改,這提供了很重要的效能優勢。 可讀寫的快取會通過序列化返回快取物件的拷貝,這種方式會慢一些,但很安全,因此預設為false

Mapper介面中配置二級快取

使用註解的方式時,如果想對註解方式啟用二級快取,還需要在Mapper介面中進行配置,如果Mapper介面也存在對應的XML對映檔案,兩者同時開啟快取時,還需要特殊配置。

只使用註解方式配置二級快取

當只使用註解方式配置二級快取時,比如RoleMapper介面中,則需要增加如下配置

@CacheNamespace
public interface RoleMapper {
    ....
}

只需要增加@CacheNamespace(org.apache.ibatis.annotations.CacheNamespace),該註解同樣可以配置各項屬性

@CacheNamespace{
    eviction = FifoCache.class,
    flushInterval = 60000,
    size = 512,
    readWrite = true 
}

這裡的readWrite屬性和XML中的readOnly屬性一樣,用於配置快取是否為只讀型別,在這裡true為讀寫,false為只讀,預設為true。

同時使用註解方式和XML對映檔案時

當同時使用註解方式和XML對映檔案時,如果同時配置了上述的二級快取(使用xml 以及介面上標註了@CacheNamespace),會丟擲如下異常

Cache collection already contains value for **

這是因為Mapper介面和對應的XML檔案是相同的名稱空間,想使用二級快取,兩者必須同時配置(如果介面不存在使用註解方式的方法,可以只在XML中配置),因此按照上面的方式進行配置就出錯,這個時候應該使用參照快取,

在Mapper介面中,參照快取配置如下

@CacheNamespaceRef(RoleMapper.class)
public interface RoleMapper{

}

MyBatis很少會同時使用Mapper介面註解方式和XML對映檔案,所以參照快取並不是為了解決這個問題而設計的,參照快取主要是為了解決髒讀

二級快取的使用

前提:實體類實現Serializable介面

由於MyBatis配置的是可讀寫的快取,而MyBatis使用SerializedCache序列化快取來實現可讀寫快取類,並通過序列化和反序列化來保證通過快取獲取資料時,得到的是一個新的示例。 因此如果配置為只讀快取,MyBatis會使用Map來儲存快取值,這種情況下,從快取中獲取的物件就是同一個例項

因為使用可讀寫快取,可以使用SerializedCache序列化快取,這個快取類要求所有被序列化的物件必須實現java.io.Serializable介面,所以需要修改SysPrivilege實體類

public class SysPrivilege implements Serializable {

    private static final long serialVersionUID = 6315662516417216377L;
    // 其他保持不變
}

示例

實體類SysPrivilege實現Serializable介面

public class SysPrivilege implements Serializable {

    private static final long serialVersionUID = 6315662516417216377L;
    // 其他保持不變
}

PrivilegeMapper介面類增加介面方法

/**
     * 
     * 
     * @Title: selectPrivilegeByIdWithCache
     * 
     * @Description: 二級快取測試方法 ,實體類SysPrivilege必須要實現Serializable
     * 
     * @param id
     * @return
     * 
     * @return: SysPrivilege
     */
    SysPrivilege selectPrivilegeByIdWithCache(Long id);

PrivilegeMapper.xml中配置對單表操作的SQL

<select id="selectPrivilegeByIdWithCache"  resultType="com.artisan.mybatis.xml.domain.SysPrivilege">
        SELECT
            id,
            privilege_name privilegeName,
            privilege_url privilegeUrl
        FROM
            sys_privilege
        WHERE
            id = #{id}
    </select>

單元測試

@Test
    public void selectPrivilegeByIdWithCacheTest() {
        logger.info("selectPrivilegeByIdWithCacheTest");
        SqlSession sqlSession =  getSqlSession();
        SysPrivilege sysPrivilege = null;
        try {
            // 獲取介面
            PrivilegeMapper privilegeMapper = sqlSession.getMapper(PrivilegeMapper.class);
            // 呼叫介面方法
            sysPrivilege = privilegeMapper.selectPrivilegeByIdWithCache(1L);
            sysPrivilege.setPrivilegeName("New Priv");
            // 再次呼叫相同的介面方法,查詢相同的使用者
            logger.info("再次呼叫相同的介面方法,查詢相同的使用者 Begin");
            SysPrivilege sysPrivilege2 = privilegeMapper.selectPrivilegeByIdWithCache(1L);
            logger.info("再次呼叫相同的介面方法,查詢相同的使用者 End");
            // 一級快取在同一個sqlSession中,雖然沒有更新資料庫,但是會使用一級快取
            Assert.assertEquals("New Priv", sysPrivilege2.getPrivilegeName());
            // sysPrivilege 和 sysPrivilege2 是同一個例項
            Assert.assertEquals(sysPrivilege, sysPrivilege2);
        } finally {
            // sqlSession關閉後,在二級快取開啟的前提下,會寫入二級快取
            sqlSession.close();
        }

        logger.info("重新獲取一個SqlSession");
        sqlSession = getSqlSession();
        try {
            // 獲取介面
            PrivilegeMapper privilegeMapper = sqlSession.getMapper(PrivilegeMapper.class);
            // 呼叫介面方法
            SysPrivilege sysPrivilege2 = privilegeMapper.selectPrivilegeByIdWithCache(1L);
            sysPrivilege.setPrivilegeName("New Priv");
            // 第二個session獲取的許可權名為 New Priv
            Assert.assertEquals("New Priv", sysPrivilege2.getPrivilegeName());
            // 這裡的sysPrivilege2 和 前一個session中的sysPrivilege不是同一個例項
            Assert.assertNotEquals(sysPrivilege, sysPrivilege2);

            // 獲取sysPrivilege3
            SysPrivilege sysPrivilege3 = privilegeMapper.selectPrivilegeByIdWithCache(1L);
            // 這裡的sysPrivilege2 和sysPrivilege3是兩個不同的例項
            Assert.assertNotEquals(sysPrivilege2, sysPrivilege3);
        } finally {
            // sqlSession關閉後,在二級快取開啟的前提下,會寫入二級快取
            sqlSession.close();
        }

    }

日誌

2018-05-07 14:21:22,949  INFO [main] (BaseMapperTest.java:26) - sessionFactory bulit successfully
2018-05-07 14:21:22,949  INFO [main] (BaseMapperTest.java:29) - reader close successfully
2018-05-07 14:21:22,960  INFO [main] (PrivilegeMapperTest.java:38) - selectPrivilegeByIdWithCacheTest
2018-05-07 14:21:23,010 DEBUG [main] (LoggingCache.java:62) - Cache Hit Ratio [com.artisan.mybatis.xml.mapper.PrivilegeMapper]: 0.0
2018-05-07 14:21:23,080 DEBUG [main] (BaseJdbcLogger.java:145) - ==>  Preparing: SELECT id, privilege_name privilegeName, privilege_url privilegeUrl FROM sys_privilege WHERE id = ? 
2018-05-07 14:21:23,270 DEBUG [main] (BaseJdbcLogger.java:145) - ==> Parameters: 1(Long)
2018-05-07 14:21:23,320 TRACE [main] (BaseJdbcLogger.java:151) - <==    Columns: id, privilegeName, privilegeUrl
2018-05-07 14:21:23,320 TRACE [main] (BaseJdbcLogger.java:151) - <==        Row: 1, 使用者管理, /users
2018-05-07 14:21:23,340 DEBUG [main] (BaseJdbcLogger.java:145) - <==      Total: 1
2018-05-07 14:21:23,340  INFO [main] (PrivilegeMapperTest.java:48) - 再次呼叫相同的介面方法,查詢相同的使用者 Begin
2018-05-07 14:21:23,340 DEBUG [main] (LoggingCache.java:62) - Cache Hit Ratio [com.artisan.mybatis.xml.mapper.PrivilegeMapper]: 0.0
2018-05-07 14:21:23,340  INFO [main] (PrivilegeMapperTest.java:50) - 再次呼叫相同的介面方法,查詢相同的使用者 End
2018-05-07 14:21:23,350  INFO [main] (PrivilegeMapperTest.java:60) - 重新獲取一個SqlSession
2018-05-07 14:21:23,360 DEBUG [main] (LoggingCache.java:62) - Cache Hit Ratio [com.artisan.mybatis.xml.mapper.PrivilegeMapper]: 0.3333333333333333
2018-05-07 14:21:23,360 DEBUG [main] (LoggingCache.java:62) - Cache Hit Ratio [com.artisan.mybatis.xml.mapper.PrivilegeMapper]: 0.5

日誌分析:

第一部分第一次和第二次查詢的sysPrivilege 和 sysPrivilege2 是完全相同的例項,這是使用的是預設的一級快取,所以返回同一個例項。

當呼叫close方法關閉sqlSession後,sqlSession才回儲存查詢資料到二級快取中。 在這之後二級快取才有了快取資料。 所以第一次裡看到的兩次查詢時,命中率都是0 。

重新開一個sqlSession,再次獲取sysPrivilege 時,因為快取中有了資料,沒有查詢資料庫,而是輸出了命中率,這是的命中率為 0.3333333333333333 , 查詢3次,命中1次,因此是1/3

緊接著第4次查詢,加上上次的命中,2/4 ,命中率為0.5

注意: 我們為了測試,在獲取到資料後,呼叫了setPrivilegeName方法,實際中並不需要,避免人為產生髒資料。

注意事項(重要)

MyBatis二級快取的使用場景

  1. 只能在【只有單表操作】的表上使用快取,不只是要保證這個表在整個系統中只有單表操作,而且和該表有關的全部操作必須全部在一個namespace下

  2. 在可以保證查詢遠遠大於insert,update,delete操作的情況下使用快取,這一點需要保證在1的前提下才可以

避免使用二級快取

二級快取帶來的好處遠遠比不上他所隱藏的危害。

二級快取
- 快取是以namespace為單位的,不同namespace下的操作互不影響。
- insert,update,delete操作會清空所在namespace下的全部快取。
- 通常使用MyBatis Generator生成的程式碼中,都是各個表獨立的,每個表都有自己的namespace。

為什麼避免使用二級快取

在符合【MyBatis二級快取的使用場景】的要求時,並沒有什麼危害。

其他情況就會有很多危害了。

針對一個表的某些操作不在他獨立的namespace下進行。
例如在UserMapper.xml中有大多數針對user表的操作。但是在一個XXXMapper.xml中,還有針對user單表的操作。

這會導致user在兩個名稱空間下的資料不一致。如果在UserMapper.xml中做了重新整理快取的操作,在XXXMapper.xml中快取仍然有效,如果有針對user的單表查詢,使用快取的結果可能會不正確。

更危險的情況是在XXXMapper.xml做了insert,update,delete操作時,會導致UserMapper.xml中的各種操作充滿未知和風險。

有關這樣單表的操作可能不常見.

多表操作一定不能使用快取

首先不管多表操作寫到那個namespace下,都會存在某個表不在這個namespace下的情況。

例如兩個表:role和user_role,如果想查詢出某個使用者的全部角色role,就一定會涉及到多表的操作。

<select id="selectUserRoles" resultType="UserRoleVO">
    select * from user_role a,role b where a.roleid = b.roleid and a.userid = #{userid}
</select>

像上面這個查詢,你會寫到那個xml中呢??

不管是寫到RoleMapper.xml還是UserRoleMapper.xml,或者是一個獨立的XxxMapper.xml中。如果使用了二級快取,都會導致上面這個查詢結果可能不正確。

如果你正好修改了這個使用者的角色,上面這個查詢使用快取的時候結果就是錯的。

這點應該很容易理解。

在我看來,就以MyBatis目前的快取方式來看是無解的。多表操作根本不能快取。

如果你讓他們都使用同一個namespace(通過<cache-ref>)來避免髒資料,那就失去了快取的意義。

實際上就是說,二級快取不能用

如何挽救二級快取

想更高效率的使用二級快取是解決不了的

建議放棄二級快取,在業務層使用可控制的快取代替更好。