作者:陳浩翔(Java架構沉思錄進行部分修正)
MyBatis自帶的緩存有一級緩存和二級緩存。
一級緩存
Mybatis的一級緩存是指Session緩存。一級緩存的作用域預設是一個SqlSession。Mybatis預設開啟一級緩存。 也就是在同一個SqlSession中,執行相同的查詢SQL,第一次會去資料庫進行查詢,并寫到緩存中; 第二次以後是直接去緩存中取。 當執行SQL查詢中間發生了增删改的操作,MyBatis會把SqlSession的緩存清空。
一級緩存的範圍有SESSION和STATEMENT兩種,預設是SESSION,如果不想使用一級緩存,可以把一級緩存的範圍指定為STATEMENT,這樣每次執行完一個Mapper中的語句後都會将一級緩存清除。
如果需要更改一級緩存的範圍,可以在Mybatis的配置檔案中,通過localCacheScope指定。
<setting name="localCacheScope" value="STATEMENT"/>
建議不要修改。
需要注意的是
當Mybatis整合Spring後,直接通過Spring注入Mapper的形式,如果不是在同一個事務中每個Mapper的每次查詢操作都對應一個全新的SqlSession執行個體,這個時候就不會有一級緩存的命中,但是在同一個事務中時共用的是同一個SqlSession。
如有需要可以啟用二級緩存。
二級緩存
Mybatis的二級緩存是指mapper映射檔案。二級緩存的作用域是同一個namespace下的mapper映射檔案内容,多個SqlSession共享。Mybatis需要手動設定啟動二級緩存。
二級緩存是預設啟用的(要生效需要對每個Mapper進行配置),如想取消,則可以通過Mybatis配置檔案中的元素下的子元素來指定cacheEnabled為false。
<settings> <setting name="cacheEnabled" value="false" /> </settings>
cacheEnabled預設是啟用的,隻有在該值為true的時候,底層使用的Executor才是支援二級緩存的CachingExecutor。具體可參考Mybatis的核心配置類org.apache.ibatis.session.Configuration的newExecutor方法實作。
可以通過源碼看看
要使用二級緩存除了上面一個配置外,我們還需要在我們每個DAO對應的Mapper.xml檔案中定義需要使用的cache。
<mapper namespace="...UserMapper"> <cache/><!-- 加上該句即可,使用預設配置、還有另外一種方式,在後面寫出 --> </mapper>
具體可以看org.apache.ibatis.executor.CachingExecutor類的以下實作
其中使用的cache就是我們在對應的Mapper.xml中定義的cache。
還有一個條件就是需要目前的查詢語句是配置了使用cache的,即上面源碼的useCache()是傳回true的,預設情況下所有select語句的useCache都是true,如果我們在啟用了二級緩存後,有某個查詢語句是我們不想緩存的,則可以通過指定其useCache為false來達到對應的效果。
如果我們不想該語句緩存,可使用useCache="false"。
<select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.String" useCache="false"> select <include refid="Base_Column_List"/> from tuser where id = #{id,jdbcType=VARCHAR} </select>
上面說了要想使用二級緩存,需要在每個DAO對應的Mapper.xml檔案中定義其中的查詢語句需要使用cache來緩存資料的。
這有兩種方式可以定義,一種是通過cache元素定義,一種是通過cache-ref元素來定義。
需要注意的是
對于同一個Mapper來講,隻能使用一個Cache,當同時使用了cache和cache-ref時,cache定義的優先級更高(後面的代碼會給出原因)。
Mapper使用的Cache是與我們的Mapper對應的namespace綁定的,一個namespace最多隻會有一個Cache與其綁定。
使用cache元素來定義使用的Cache時,最簡單的做法是直接在對應的Mapper.xml檔案中指定一個空的元素(看前面的代碼),這個時候Mybatis會按照預設配置建立一個Cache對象,準備的說是PerpetualCache對象,更準确的說是LruCache對象(底層用了裝飾器模式)。
具體的可看org.apache.ibatis.builder.xml.XMLMapperBuilder中的cacheElement()方法解析cache元素的邏輯。
空cache元素定義會生成一個采用最近最少使用算法最多隻能存儲1024個元素的緩存,而且是可讀寫的緩存,即該緩存是全局共享的,任何一個線程在拿到緩存結果後對資料的修改都将影響其它線程擷取的緩存結果,因為它們是共享的,同一個對象。
cache元素可指定如下屬性,每種屬性的指定都是針對都是針對底層Cache的一種裝飾,采用的是裝飾器的模式。
blocking:預設為false,當指定為true時将采用BlockingCache進行封裝,blocking,阻塞的意思,使用BlockingCache會在查詢緩存時鎖住對應的Key,如果緩存命中了則會釋放對應的鎖,否則會在查詢資料庫以後再釋放鎖,這樣可以阻止并發情況下多個線程同時查詢資料,詳情可參考BlockingCache的源碼。 簡單了解,也就是設定true時,在進行增删改之後的并發查詢,隻會有一條去資料庫查詢,而不會并發
eviction:eviction,驅逐的意思。也就是元素驅逐算法,預設是LRU,對應的就是LruCache,其預設隻儲存1024個Key,超出時按照最近最少使用算法進行驅逐,詳情請參考LruCache的源碼。如果想使用自己的算法,則可以将該值指定為自己的驅逐算法實作類,隻需要自己的類實作Mybatis的Cache接口即可。除了LRU以外,系統還提供了FIFO(先進先出,對應FifoCache)、SOFT(采用軟引用存儲Value,便于垃圾回收,對應SoftCache)和WEAK(采用弱引用存儲Value,便于垃圾回收,對應WeakCache)這三種政策。 這裡,根據個人需求選擇了,沒什麼要求的話,預設的LRU即可。
flushInterval:清空緩存的時間間隔,機關是毫秒,預設是不會清空的。當指定了該值時會再用ScheduleCache包裝一次,其會在每次對緩存進行操作時判斷距離最近一次清空緩存的時間是否超過了flushInterval指定的時間,如果超出了,則清空目前的緩存,詳情可參考ScheduleCache的實作。
readOnly:是否隻讀 ,預設為false。當指定為false時,底層會用SerializedCache包裝一次,其會在寫緩存的時候将緩存對象進行序列化,然後在讀緩存的時候進行反序列化,這樣每次讀到的都将是一個新的對象,即使你更改了讀取到的結果,也不會影響原來緩存的對象,即非隻讀,你每次拿到這個緩存結果都可以進行修改,而不會影響原來的緩存結果; 當指定為true時那就是每次擷取的都是同一個引用,對其修改會影響後續的緩存資料擷取,這種情況下是不建議對擷取到的緩存結果進行更改,意為隻讀(不建議設定為true)。 這是Mybatis二級緩存讀寫和隻讀的定義,可能與我們通常情況下的隻讀和讀寫意義有點不同。每次都進行序列化和反序列化無疑會影響性能,但是這樣的緩存結果更安全,不會被随意更改,具體可根據實際情況進行選擇。詳情可參考SerializedCache的源碼。
size:用來指定緩存中最多儲存的Key的數量。其是針對LruCache而言的,LruCache預設隻存儲最多1024個Key,可通過該屬性來改變預設值,當然,如果你通過eviction指定了自己的驅逐算法,同時自己的實作裡面也有setSize方法,那麼也可以通過cache的size屬性給自定義的驅逐算法裡面的size指派。
type:type屬性用來指定目前底層緩存實作類,預設是PerpetualCache,如果我們想使用自定義的Cache,則可以通過該屬性來指定,對應的值是我們自定義的Cache的全路徑名稱。
cache-ref元素可以用來指定其它Mapper.xml中定義的Cache,有的時候可能我們多個不同的Mapper需要共享同一個緩存。如果希望在MapperA中緩存的内容在MapperB中可以直接命中,這個時候我們就可以考慮使用cache-ref,這種場景隻需要保證它們的緩存的Key是一緻的即可命中,二級緩存的Key是通過Executor接口的createCacheKey()方法生成的,其實作基本都是BaseExecutor,源碼如下。
打個比方我想在MenuMapper.xml中的查詢都使用在UserMapper.xml中定義的Cache,則可以通過cache-ref元素的namespace屬性指定需要引用的Cache所在的namespace,即UserMapper.xml中的定義的namespace,假設在UserMapper.xml中定義的namespace是cn.chenhaoxiang.dao.UserMapper,則在MenuMapper.xml的cache-ref應該定義如下。
<cache-ref namespace="cn.chenhaoxiang.dao.UserMapper"/>
這樣這兩個Mapper就共享同一個緩存了。
自定義cache就不介紹了。

查詢測試
/** * Created with IntelliJ IDEA. * User: 陳浩翔. * Date: 2018/1/10. * Time: 下午 10:15. * Explain: */@RunWith(SpringJUnit4ClassRunner.class)//配置了@ContextConfiguration注解并使用該注解的locations屬性指明spring和配置檔案之後@ContextConfiguration(locations = {"classpath:spring.xml","classpath:spring-mybatis.xml"})public class MyBatisTestBySpringTestFramework { //注入userService @Autowired private UserService userService; @Test public void testGetUserId(){ String userId = "4e07f3963337488e81716cfdd8a0fe04"; User user = userService.getUserById(userId); System.out.println(user); //前面說到spring和MyBatis整合 User user2 = userService.getUserById(userId); System.out.println("user2:"+user2); } }
接下來我們把Mapper中的cache元素删除,不使用二級緩存
再運作測試
對二級緩存進行了以下測試,擷取兩個不同的SqlSession(前面有說,Spring和MyBatis內建,每次都是不同的SqlSession)執行兩條相同的SQL,在未指定Cache時Mybatis将查詢兩次資料庫,在指定了Cache時Mybatis隻查詢了一次資料庫,第二次是從緩存中拿的。
Cache Hit Ratio 表示緩存命中率。
開啟二級緩存後,每執行一次查詢,系統都會計算一次二級緩存的命中率。
第一次查詢也是先從緩存中查詢,隻不過緩存中一定是沒有的。
是以會再從DB中查詢。由于二級緩存中不存在該資料,是以命中率為0.但第二次查詢是從二級緩存中讀取的,是以這一次的命中率為1/2=0.5。
當然,若有第三次查詢,則命中率為1/3=0.66 。
0.5這個值可以從上面開啟cache的圖看出來,0.0的值未截取到~漏掉了~
注意:
增删改操作,無論是否進行送出sqlSession.commit(),均會清空一級、二級緩存,使查詢再次從DB中select。
隻能在一個命名空間下使用二級緩存
由于二級緩存中的資料是基于namespace的,即不同namespace中的資料互不幹擾。在多個namespace中若均存在對同一個表的操作,那麼這多個namespace中的資料可能就會出現不一緻現象。
在單表上使用二級緩存
如果一個表與其它表有關聯關系,那麼久非常有可能存在多個namespace對同一資料的操作。而不同namespace中的資料互補幹擾,是以就有可能出現多個namespace中的資料不一緻現象。
查詢多于修改時使用二級緩存
在查詢操作遠遠多于增删改操作的情況下可以使用二級緩存。因為任何增删改操作都将重新整理二級緩存,對二級緩存的頻繁重新整理将降低系統性能。