MyBatis緩存你了解多少?會(huì)用嗎?
本文轉(zhuǎn)載自微信公眾號(hào)「三不猴子」,作者sanbuhouzi。轉(zhuǎn)載本文請(qǐng)聯(lián)系三不猴子公眾號(hào)。
MyBatis緩存你了解多少?會(huì)用嗎?
MyBatis緩存介紹
正如大多數(shù)持久層框架一樣,MyBatis 同樣提供了一級(jí)緩存和二級(jí)緩存的支持
- 一級(jí)緩存: 基于PerpetualCache 的 HashMap本地緩存,其存儲(chǔ)作用域?yàn)?Session,當(dāng) Session flush 或 close 之后,該Session中的所有 Cache 就將清空。
- 二級(jí)緩存與一級(jí)緩存其機(jī)制相同,默認(rèn)也是采用 PerpetualCache,HashMap存儲(chǔ),不同在于其存儲(chǔ)作用域?yàn)? Mapper(Namespace),并且可自定義存儲(chǔ)源,如 Ehcache。
- 對(duì)于緩存數(shù)據(jù)更新機(jī)制,當(dāng)某一個(gè)作用域(一級(jí)緩存Session/二級(jí)緩存Namespaces)的進(jìn)行了 C/U/D 操作后,默認(rèn)該作用域下所有 select 中的緩存將被clear。
mybatis的相關(guān)概念
- SqlSession : 代表和數(shù)據(jù)庫的一次會(huì)話,向用戶提供了操作數(shù)據(jù)庫的方法。
- MappedStatement: 代表要發(fā)往數(shù)據(jù)庫執(zhí)行的指令,可以理解為是Sql的抽象表示。
- Executor: 具體用來和數(shù)據(jù)庫交互的執(zhí)行器,接受MappedStatement作為參數(shù)。
- 映射接口: 在接口中會(huì)要執(zhí)行的Sql用一個(gè)方法來表示,具體的Sql寫在映射文件中。
- 映射文件: 可以理解為是Mybatis編寫Sql的地方,通常來說每一張單表都會(huì)對(duì)應(yīng)著一個(gè)映射文件,在該文件中會(huì)定義Sql語句入?yún)⒑统鰠⒌男问健?/li>
一級(jí)緩存
- MyBatis的一級(jí)查詢緩存(也叫作本地緩存)是基于org.apache.ibatis.cache.impl.PerpetualCache 類的 HashMap本地緩存,其作用域是SqlSession
- 在同一個(gè)SqlSession中兩次執(zhí)行相同的 sql 查詢語句,第一次執(zhí)行完畢后,會(huì)將查詢結(jié)果寫入到緩存中,第二次會(huì)從緩存中直接獲取數(shù)據(jù),而不再到數(shù)據(jù)庫中進(jìn)行查詢,這樣就減少了數(shù)據(jù)庫的訪問,從而提高查詢效率。
- 當(dāng)一個(gè) SqlSession 結(jié)束后,該 SqlSession 中的一級(jí)查詢緩存也就不存在了。
- myBatis 默認(rèn)一級(jí)查詢緩存是開啟狀態(tài),且不能關(guān)閉。
- 增刪改會(huì)清空緩存,無論是否commit 當(dāng)SqlSession關(guān)閉和提交時(shí),會(huì)清空一級(jí)緩存
可能你會(huì)有疑惑,我的mybatis bean是由spring 來管理的,已經(jīng)屏蔽了sqlSession這個(gè)東西了?那怎么的一次操作才算是一次sqlSession呢?
- spring整合mybatis后,非事務(wù)環(huán)境下,每次操作數(shù)據(jù)庫都使用新的sqlSession對(duì)象。因此mybatis的一級(jí)緩存無法使用(一級(jí)緩存針對(duì)同一個(gè)sqlsession有效)
- 在開啟事物的情況之下,spring使用threadLocal獲取當(dāng)前資源綁定同一個(gè)sqlSession,因此此時(shí)一級(jí)緩存是有效的 在開啟以及緩存的時(shí)候查詢得到的對(duì)象是同一個(gè)對(duì)象。這種情況下會(huì)出現(xiàn)一個(gè)問題。我們先看一下代碼。
- public void listMybatisModel() {
- List<MybatisModel> mybatisModels = mapper.listMybatisModel();
- List<MybatisModel> mybatisModelsOther = mapper.listMybatisModel();
- System.out.println(mybatisModels == mybatisModelsOther);
- System.out.println("list count: " + mybatisModels.size());
- }
- System.out.println(mybatisModels == mybatisModelsOther);
輸出結(jié)果竟然是true,這樣說來是同一個(gè)對(duì)象。會(huì)出現(xiàn)這種場景,第一次查出來的對(duì)象然后修改了,第二次查出來的就是修改后的對(duì)象。
一級(jí)緩存實(shí)現(xiàn)
對(duì)SqlSession的操作mybatis內(nèi)部都是通過Executor來執(zhí)行的。Executor的生命周期和SqlSession是一致的。Mybatis在Executor中創(chuàng)建了一級(jí)緩存,基于PerpetualCache 類的 HashMap
二級(jí)緩存
- MyBatis的二級(jí)緩存是mapper范圍級(jí)別的
- SqlSession關(guān)閉后才會(huì)將數(shù)據(jù)寫到二級(jí)緩存區(qū)域
- 增刪改操作,無論是否進(jìn)行提交commit(),均會(huì)清空一級(jí)、二級(jí)緩存
- 二級(jí)緩存是默認(rèn)開啟的。(想開啟就不必做任何配置)
- 二級(jí)緩存會(huì)使用 Least Recently Used (LRU,最近最少使用的)算法來收回。
- 根據(jù)時(shí)間表(如 no Flush Interval ,沒有刷新間隔),緩存不會(huì)以任何時(shí)間順序來刷新 。
- 緩存會(huì)存儲(chǔ)集合或?qū)ο?無論查詢方法返回什么類型的值)的 1024 個(gè)引用。
- 緩存會(huì)被視為 read/write (可讀/可寫)的,意味著對(duì)象檢索不是共享的,而且可以安全地被調(diào)用者修改,而不干擾其他調(diào)用者或線程所做的潛在修改 。
用下面這張圖描述一級(jí)緩存和二級(jí)緩存的關(guān)系。
配置二級(jí)緩存
在保證二級(jí)緩存的全局配置開啟的情況下,給某個(gè)xml開啟二級(jí)緩存只需要在xml中添加即可
- // mybatis-config.xml 中配置
- <settings>
- 默認(rèn)值為 true。即二級(jí)緩存默認(rèn)是開啟的
- <setting name="cacheEnabled" value="true"/>
- </settings>
- // 具體mapper.xml 中配置
- <mapper namespace="cn.itcast.mybatis.mapper.UserMapper">
- <!-- 開啟本mapper的namespace下的二級(jí)緩存
- type:指定cache接口的實(shí)現(xiàn)類的類型,mybatis默認(rèn)使用PerpetualCache
- 要和ehcache整合,需要配置type為ehcache實(shí)現(xiàn)cache接口的類型-->
- <cache />
- </mapper>
如果想要設(shè)置增刪改操作的時(shí)候不清空二級(jí)緩存的話,可以在其insert或delete或update中添加屬性flushCache=”false”,默認(rèn)為 true。
- <delete id="deleteStudent" flushCache="false">
- DELETE FROM user where id=#{id}
- </delete>
通過以下一些配置可以修改一些緩存參數(shù)。
- <cache
- eviction= "FIFO"
- flushlnterval="600000"
- size="512"
- readOnly="true" />
配置創(chuàng)建了一個(gè)FIFO緩存,并每隔 60 秒刷新一次,存儲(chǔ)集合或?qū)ο蟮?12個(gè)引用, 而且返回的對(duì)象被認(rèn)為是只讀的,因此在不同線程中的調(diào)用者之間修改它們會(huì)導(dǎo)致沖突。cache可以配置的屬性如下。eviction (收回策略)
LRU (最近最少使用的: 移除最長時(shí)間不被使用的對(duì)象,這是默認(rèn)值 。FIFO (先進(jìn)先出〉 :按對(duì)象進(jìn)入緩存的順序來移除它們 。SOFT (軟引用) :移除基于垃圾回收器狀態(tài)和軟引用規(guī)則的對(duì)象 。WEAK (弱引用) :更積極地移除基于垃圾收集器狀態(tài)和弱引用規(guī)則的對(duì)象
- flushinterval(刷新間隔)。可以被設(shè)置為任意的正整數(shù),而且它們代表一個(gè)合理 的毫秒形式的時(shí)間段。默認(rèn)情況不設(shè)置,即沒有刷新間隔,緩存僅僅在調(diào)用語句時(shí)刷新。
- size (引用數(shù)目)。可以被設(shè)置為任意正整數(shù),要記住緩存的對(duì)象數(shù)目和運(yùn)行環(huán)境的可用內(nèi)存資源數(shù)目。默認(rèn)值是 1024 。
- readOnly (只讀)。屬性可以被設(shè)置為 true 或 false 。只讀的緩存會(huì)給所有調(diào)用者 返回緩存對(duì)象的相同實(shí)例,因此這些對(duì)象不能被修改,這提供了很重要的性能優(yōu)勢。可讀寫的緩存會(huì)通過序列化返回緩存對(duì)象的拷貝,這種方式會(huì)慢一些,但是安全,因此默認(rèn)是false。
當(dāng)只使用注解方式配置二級(jí)緩存時(shí),如果在Mapper接口中,則需要增加如下配置 。
- @CacheNamespace (
- eviction = FifoCache.class ,
- flushinterval = 60000 ,
- size = 512 ,
- readWrite = true)
- public interface Mapper {
- }
括號(hào)內(nèi)的內(nèi)容是配置緩存屬性。
Mapper 接口和對(duì)應(yīng)的 XML 文件是相同的命名空間,想使用二級(jí)緩存,兩者必須同時(shí)配置(如果接口不存在使用注解方式的方法,可以只在 XML 中配置〉,因此按照上面的方 式進(jìn)行配置就會(huì)出錯(cuò) , 這個(gè)時(shí)候應(yīng)該使用參照緩存。在 Mapper 接口中,參照緩存配置如下 。
- @CacheNarnespaceRef(RoleMapper.class)
- public interface RoleMapper {
因?yàn)橄胱?RoleMapper 接口中的注解方法和 XML中的方法使用相同的緩存,因此使用參照緩存配置RoleMapper.class,這樣就會(huì)使用命名空間為xx.xxx.xxx.xxx.RoleMapper的緩存配置,即RoleMapper.xml 中配置的緩存 。Mapper 接口可以通過注解引用XML 映射文件或者其他接口的緩存,在 XML 中也可以配置參照緩存,如可以在 RoleMapper.xml 中進(jìn)行如下修改 。
- <cache-ref narnespace="xxx.xxx.xxx.xxx.RoleMapper"/>
這樣配置后XML 就會(huì)引用 Mapper 接口中配置的二級(jí)緩存,同樣可以避免同時(shí)配置二級(jí)緩存導(dǎo)致的沖突。MyBatis 中很少會(huì)同時(shí)使用 Mapper 接口注解方式和XML映射文件,所以參照緩存并不是為了解決這個(gè)問題而設(shè)計(jì)的。參照緩存除了能夠通過引用其他緩存減少配置外,主要的作用是解決臟讀。
MyBatis使用SerializedCache(org.apache.ibaits.cache.decorators.SerializedCache)序列化緩存來實(shí)現(xiàn)可讀寫緩存類,井通過序列化和反序列化來保證通過緩存獲取數(shù)據(jù)時(shí),得到的是一個(gè)新的實(shí)例。因此,如果配置為只讀緩存,MyBatis就會(huì)使用Map來存儲(chǔ)緩存值,這種情況下,從緩存中獲取的對(duì)象就是同一個(gè)實(shí)例。因?yàn)槭褂每勺x寫緩存,可以使用SerializedCache序列化緩存。這個(gè)緩存類要求所有被序列化的對(duì)象必須實(shí)現(xiàn) Serializable (java.io.Serializable)接口 雖然使用序列化得到的對(duì)象都是不一樣的對(duì)象修改時(shí)都是互不影響,但是還是不安全的。
臟讀的產(chǎn)生
Mybatis的二級(jí)緩存是和命名空間綁定的,所以通常情況下每一個(gè)Mapper映射文件都有自己的二級(jí)緩存,不同的mapper的二級(jí)緩存互不影響。
- 引起臟讀的操作通常發(fā)生在多表關(guān)聯(lián)操作中,比如在兩個(gè)不同的mapper中都涉及到同一個(gè)表的增刪改查操作,當(dāng)其中一個(gè)mapper對(duì)這張表進(jìn)行查詢操作,此時(shí)另一個(gè)mapper進(jìn)行了更新操作刷新緩存,然后第一個(gè)mapper又查詢了一次,那么這次查詢出的數(shù)據(jù)是臟數(shù)據(jù)。出現(xiàn)臟讀的原因是他們的操作的緩存并不是同一個(gè)。
臟讀的避免
- mapper中的操作以單表操作為主,避免在關(guān)聯(lián)操作中使用mapper
- 使用參照緩存
集成EhCache緩存
- 緩存數(shù)據(jù)有內(nèi)存和磁盤兩級(jí),無須擔(dān)心容量問題。
- 緩存數(shù)據(jù)會(huì)在虛擬機(jī)重啟的過程中寫入磁盤。可以通過RMI、可插入API等方式進(jìn)行分布式緩存。
- 具有緩存和緩存管理器的偵昕接口。
支持多緩存管理器實(shí)例以及一個(gè)實(shí)例的多個(gè)緩存區(qū)域。
1. 添加項(xiàng)目依賴
- <dependency>
- <groupId>org.mybatis.caches</groupId>
- <artifactId>mybatis-ehcache</artifactId>
- <version>1.0.3</version>
- </dependency>
2. 配置 EhCache
在 src/main/resources 目錄下新增 ehcache.xml 文件。
- <?xml version="1.0" encoding="UTF-8"?>
- <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:noNamespaceSchemaLocation="ehcache.xsd"
- updateCheck="false" monitoring="autodetect"
- dynamicConfig="true">
- <diskStore path="D:/cache" />
- <defaultCache
- maxElementsInMemory="3000"
- eternal="false"
- copyOnRead="true"
- copyOnWrite="true"
- timeToIdleSeconds="3600"
- timeToLiveSeconds="3600"
- overflowToDisk="true"
- diskPersistent="true"/>
- </ehcache>
有關(guān)EhCache的詳細(xì)配置可以參考地址 http://www.ehcache.org/ehcache.xml 中的內(nèi)容。
- copyOnRead 的含義是,判斷從緩存中讀取數(shù)據(jù)時(shí)是返回對(duì)象的引用還是復(fù)制一個(gè)對(duì)象返回。默認(rèn)情況下是false,即返回?cái)?shù)據(jù)的引用,這種情況下返回的都是相同的對(duì)象,和MyBatis默認(rèn)緩存中的只讀對(duì)象是相同的。如果設(shè)置為 true ,那就是可讀寫緩存,每次讀取緩存時(shí)都會(huì)復(fù)制一個(gè)新的實(shí)例 。
- copyOnWrite 的含義是 ,判斷寫入緩存時(shí)是直接緩存對(duì)象的引用還是復(fù)制一個(gè)對(duì)象然后緩存,默認(rèn)也是false。如果想使用可讀寫緩存,就需要將這兩個(gè)屬性配置為true,如果使用只讀緩存,可以不配置這兩個(gè)屬性,使用默認(rèn)值 false 即可 。
- 修改Mapper.xml中的緩存配置 ehcache-cache 提供了如下 2 個(gè)可選的緩存實(shí)現(xiàn)。
- org.mybatis.caches.ehcache.EhcacheCache
- org.mybatis.caches.ehcache.LoggingEhcache 這個(gè)是帶日志的緩存。
在xml中添加
- <cache type ="org.mybatis.caches.ehcache.EhcacheCache" />
只通過設(shè)置 type 屬性就可 以使用 EhCache 緩存了,這時(shí)cache的其他屬性都不會(huì)起到任何作用,針對(duì)緩存的配置都在ehcache.xml中進(jìn)行。在ehcache.xml配置文件中,只有一個(gè)默認(rèn)的緩存配置,所以配置使用EhCache緩存的Mapper映射文件都會(huì)有一個(gè)以映射文件命名空間命名的緩存。如果想針對(duì)某一個(gè)命名空間進(jìn)行配置,需要在 ehcache.xml 中添加一個(gè)和映射文件命名空間一致的緩存配置,例如針對(duì)RoleMapper可以進(jìn)行如下配置。
- <cache
- name="tk.mybatis.simple.mapper.RoleMapper"
- maxElementsInMemory="3000"
- eternal="false"
- copyOnRead="true"
- copyOnWrite="true"
- timeToIdleSeconds="3600"
- timeToLiveSeconds="3600"
- overflowToDisk="true"
- diskPersistent="true"/>
集成Redis緩存
添加依賴,目前只有bata版本。
- <dependency>
- <groupId>org.mybatis.caches</groupId>
- <artifactId>mybatis-redis</artifactId>
- <version>1.0.0-beta2</version>
- </dependency>
配置Redis 使用 Redis 前,必須有一個(gè) Redis 服務(wù),有關(guān)Redis安裝啟動(dòng)的相關(guān)內(nèi)容,可參考如下地址中的官方文檔:https://redis.io/topics/quickstart。Redis服務(wù)啟動(dòng)后,在src/main/resources 目錄下新增 redis.properties 文件 。
- host=localhost
- port=6379
- connectionTimeout=SOOO
- soTimeout=SOOO
- password=
- database=O
- clientName=
修改mapper.xml中的配置。
- <mapper namespace = ” tk.mybat 工 s.s 工 mple.mapper.RoleMapper ” 〉
- <cache type= "org.mybatis.caches.redis.RedisCache" />
- 〈 !一其他自己直一 〉
- </mapper>
配置依然很簡單, RedisCache 在保存緩存數(shù)據(jù)和獲取緩存數(shù)據(jù)時(shí),使用了Java的序列化和反序列化,因此還需要保證被緩存的對(duì)象必須實(shí)現(xiàn)Serializable接口。改為RedisCache緩存配置后, testL2Cache 測試第一次執(zhí)行時(shí)會(huì)全部成功,但是如果再次執(zhí)行,就會(huì)出錯(cuò)。這是因?yàn)镽edis作為緩存服務(wù)器,它緩存的數(shù)據(jù)和程序(或測試)的啟動(dòng)無關(guān),Redis 的緩存并不會(huì)因?yàn)閼?yīng)用的關(guān)閉而失效。所以再次執(zhí)行時(shí)沒有進(jìn)行一次數(shù)據(jù)庫查詢,所有查詢都使用緩存,測試的第一部分代碼中的rolel和role2都是直接從二級(jí)緩存中獲取數(shù)據(jù),因?yàn)槭强勺x寫緩存,所以不是相同的對(duì)象。當(dāng)需要分布式部署應(yīng)用時(shí),如果使用MyBatis自帶緩存或基礎(chǔ)的EhCahca緩存,分布式應(yīng)用會(huì)各自擁有自己的緩存,它們之間不會(huì)共享緩存 ,這種方式會(huì)消耗更多的服務(wù)器資源。如果使用類似 Redis 的緩存服務(wù),就可以將分布式應(yīng)用連接到同一個(gè)緩存服務(wù)器,實(shí)現(xiàn)分布式應(yīng)用間的緩存共享 。