Mybatis之一二級(jí)緩存和攔截器

1 Mybatis緩存

1.1 一級(jí)緩存

一級(jí)緩存Mybatis的一級(jí)緩存是指SQLSession,一級(jí)緩存的作用域是SQLSession, Mabits默認(rèn)開(kāi)啟一級(jí)緩存意述。
在同一個(gè)SqlSession中幸冻,執(zhí)行相同的SQL查詢(xún)時(shí)务傲;第一次會(huì)去查詢(xún)數(shù)據(jù)庫(kù)舔株,并寫(xiě)在緩存中首启,第二次會(huì)直接從緩存中取房铭。
當(dāng)執(zhí)行SQL時(shí)候兩次查詢(xún)中間發(fā)生了增刪改的操作驻龟,則SQLSession的緩存會(huì)被清空。
每次查詢(xún)會(huì)先去緩存中找缸匪,如果找不到翁狐,再去數(shù)據(jù)庫(kù)查詢(xún),然后把結(jié)果寫(xiě)到緩存中凌蔬。
Mybatis的內(nèi)部緩存使用一個(gè)HashMap露懒,keyhashcode+statementId+sql語(yǔ)句闯冷。Value為查詢(xún)出來(lái)的結(jié)果集映射成的java對(duì)象
SqlSession執(zhí)行insertupdate隐锭、delete等操作commit后會(huì)清空該SQLSession緩存

一級(jí)緩存只是相對(duì)于同一個(gè)SqlSession而言窃躲。所以在參數(shù)和SQL完全一樣的情況下,我們使用同一個(gè)SqlSession對(duì)象調(diào)用一個(gè)Mapper方法钦睡,往往只執(zhí)行一次SQL蒂窒,因?yàn)槭褂?code>SelSession第一次查詢(xún)后,MyBatis會(huì)將其放在緩存中荞怒,以后再查詢(xún)的時(shí)候洒琢,如果沒(méi)有聲明需要刷新,并且緩存沒(méi)有超時(shí)的情況下褐桌,SqlSession都會(huì)取出當(dāng)前緩存的數(shù)據(jù)衰抑,而不會(huì)再次發(fā)送SQL到數(shù)據(jù)庫(kù)

1.1.1 一級(jí)緩存的生命周期

MyBatis在開(kāi)啟一個(gè)數(shù)據(jù)庫(kù)會(huì)話(huà)時(shí),會(huì) 創(chuàng)建一個(gè)新的SqlSession對(duì)象荧嵌,SqlSession對(duì)象中會(huì)有一個(gè)新的Executor對(duì)象
Executor對(duì)象中持有一個(gè)新的PerpetualCache對(duì)象呛踊;當(dāng)會(huì)話(huà)結(jié)束時(shí),SqlSession對(duì)象及其內(nèi)部的Executor對(duì)象還有PerpetualCache對(duì)象也一并釋放掉
如果SqlSession調(diào)用了close()方法啦撮,會(huì)釋放掉一級(jí)緩存PerpetualCache對(duì)象谭网,一級(jí)緩存將不可用
如果SqlSession調(diào)用了clearCache(),會(huì)清空PerpetualCache對(duì)象中的數(shù)據(jù)赃春,但是該對(duì)象仍可使用
如果SqlSession中執(zhí)行了任何一個(gè)update操作(update()愉择、delete()、insert()) 织中,都會(huì)清空PerpetualCache對(duì)象的數(shù)據(jù)锥涕,但是該對(duì)象可以繼續(xù)使用

一級(jí)緩存最多緩存 1024SQL

1.1.2 怎么判斷某兩次查詢(xún)是完全相同的查詢(xún)

mybatis認(rèn)為,對(duì)于兩次查詢(xún)狭吼,如果以下條件都完全一樣层坠,那么就認(rèn)為它們是完全相同的兩次查詢(xún):

  • 傳入的statementId
  • 查詢(xún)時(shí)要求的結(jié)果集中的結(jié)果范圍
  • 這次查詢(xún)所產(chǎn)生的最終要傳遞給JDBC java.sql.Preparedstatement的Sql語(yǔ)句字符串(boundSql.getSql() )
  • 傳遞給java.sql.Statement要設(shè)置的參數(shù)值

1.1.3 Springboot集成時(shí)一級(jí)緩存不生效問(wèn)題

因?yàn)橐患?jí)緩存是會(huì)話(huà)級(jí)別的,要生效的話(huà)刁笙,必須要在同一個(gè) SqlSession 中破花。但是與 springboot 集成的 mybatis,默認(rèn)每次執(zhí)行 sql 語(yǔ)句時(shí)采盒,都會(huì)創(chuàng)建一個(gè)新的 SqlSession 旧乞,所以一級(jí)緩存才沒(méi)有生效蔚润。

當(dāng)調(diào)用 mapper 的方法時(shí)磅氨,最終會(huì)執(zhí)行到 SqlSessionUtilsgetSqlSession 方法,在這個(gè)方法中會(huì)嘗試在事務(wù)管理器中獲取 SqlSession嫡纠,如果沒(méi)有開(kāi)啟事務(wù)烦租,那么就會(huì) new 一個(gè) DefaultSqlSession

26aa3ff74556833fdbd37ca3939a5fb1_5b0659fdaee9498f9a7064852397b599.png

所以可以猜測(cè)只要將方法開(kāi)啟事務(wù)延赌,那么一級(jí)緩存就會(huì)生效,加上 @Transactional 注解

那么為什么加了@Transactional注解就可以了呢叉橱,看源碼解析:
看看源碼中是什么時(shí)候?qū)?SqlSession 設(shè)置到事務(wù)管理器中的挫以。
SqlSessionUtils 中,在獲取到 SqlSession 后窃祝,會(huì)調(diào)用 registerSessionHolder 方法注冊(cè) SessionHolder 到事務(wù)管理器:

b61fa1ac87df4ac499c324d5e47aef40_72a451ca9ba84761919a850a973fd5db.png

具體是在 TransactionSynchronizationManagerbindResource 方法中操作的掐松,將 SessionHolder 保存到線(xiàn)程本地變量(ThreadLocal) resources 中,這是每個(gè)線(xiàn)程獨(dú)享的粪小。

9c0fb60f5d5136ac9866eccaf4f40056_5d7580c8d3304559934a5c9147149157.png

然后在下次查詢(xún)時(shí)大磺,就可以從這里取出此 SqlSession,使用同一個(gè) SqlSession 查詢(xún)探膊,一級(jí)緩存就生效了杠愧。
所以基本原理就是:如果當(dāng)前線(xiàn)程存在事物,并且存在相關(guān)會(huì)話(huà)逞壁,就從 ThreadLocal 中取出流济。如果沒(méi)有事務(wù),就重新創(chuàng)建一個(gè) SqlSession 并存儲(chǔ)到 ThreadLocal 當(dāng)中腌闯,共下次查詢(xún)使用绳瘟。
至于緩存查詢(xún)數(shù)據(jù)的地方,是在 BaseExecutor 中的 queryFromDatabase方法中绑嘹。執(zhí)行 doQuery 從數(shù)據(jù)庫(kù)中查詢(xún)數(shù)據(jù)后稽荧,會(huì)立馬緩存到 localCache(PerpetualCache類(lèi)型)中:

595a0e881ad169dd15c1a2c69fa81b67_e21e6ff05271467ea63d865a6c74fb45.png

1.2 二級(jí)緩存

1.2.1 基礎(chǔ)

二級(jí)緩存是mapper級(jí)別的,Mybatis默認(rèn)是沒(méi)有開(kāi)啟二級(jí)緩存的工腋,需要在setting全局參數(shù)中配置開(kāi)啟二級(jí)緩存
第一次調(diào)用mapper下的SQL去查詢(xún)用戶(hù)的信息姨丈,查詢(xún)到的信息會(huì)存放到該mapper對(duì)應(yīng)的二級(jí)緩存區(qū)域。 第二次調(diào)用namespace下的mapper映射文件中擅腰,相同的sql去查詢(xún)用戶(hù)信息蟋恬,會(huì)去對(duì)應(yīng)的二級(jí)緩存內(nèi)取結(jié)果

二級(jí)緩存是多個(gè)SqlSession共享的,多個(gè)SqlSession去操作同一個(gè)Mappersql語(yǔ)句趁冈,其作用域是mapper的同一個(gè)namespace歼争,不同的sqlSession兩次執(zhí)行相同namespace下的sql語(yǔ)句且向sql中傳遞參數(shù)也相同即最終執(zhí)行相同的sql語(yǔ)句,第一次執(zhí)行完畢會(huì)將數(shù)據(jù)庫(kù)中查詢(xún)的數(shù)據(jù)寫(xiě)到緩存(內(nèi)存)渗勘,第二次會(huì)從緩存中獲取數(shù)據(jù)將不再?gòu)臄?shù)據(jù)庫(kù)查詢(xún)沐绒,從而提高查詢(xún)效率
為了更加清楚的描述二級(jí)緩存,先來(lái)看一個(gè)示意圖:

在這里插入圖片描述

sqlSessionFactory層面上的二級(jí)緩存默認(rèn)是不開(kāi)啟的旺坠,二級(jí)緩存的開(kāi)啟需要進(jìn)行配置乔遮,實(shí)現(xiàn)二級(jí)緩存的時(shí)候,MyBatis要求返回的POJO必須是可序列化的取刃。 也就是要求實(shí)現(xiàn)Serializable接口蹋肮,配置方法很簡(jiǎn)單出刷,只需要在映射XML文件配置就可以開(kāi)啟緩存了<cache/>,如果配置了二級(jí)緩存就意味著:

  • 映射語(yǔ)句文件中的所有select語(yǔ)句將會(huì)被緩存
  • 映射語(yǔ)句文件中的所有insert坯辩、update和delete語(yǔ)句會(huì)刷新緩存
  • 緩存會(huì)使用默認(rèn)的Least Recently Used(LRU馁龟,最近最少使用的)算法來(lái)收回
  • 根據(jù)時(shí)間表,比如No Flush Interval,(CNFI沒(méi)有刷新間隔)漆魔,緩存不會(huì)以任何時(shí)間順序來(lái)刷新
  • 緩存會(huì)存儲(chǔ)列表集合或?qū)ο?無(wú)論查詢(xún)方法返回什么)的1024個(gè)引用
  • 緩存會(huì)被視為是read/write(可讀/可寫(xiě))的緩存坷檩,意味著對(duì)象檢索不是共享的,而且可以安全的被調(diào)用者修改改抡,不干擾其他調(diào)用者或線(xiàn)程所做的潛在修改

1.2.2 使用二級(jí)緩存

1.2.2.1 序列化

po類(lèi)實(shí)現(xiàn)Serializable接口
需要將要緩存的pojo實(shí)現(xiàn)Serializable接口淌喻,為了將緩存數(shù)據(jù)取出執(zhí)行反序列化操作,因?yàn)槎?jí)緩存數(shù)據(jù)存儲(chǔ)介質(zhì)多種多樣雀摘,不一定只存在內(nèi)存中裸删,有可能存在硬盤(pán)中,如果我們要再取這個(gè)緩存的話(huà)阵赠,就需要反序列化了涯塔。所以建議mybatis中的pojo都去實(shí)現(xiàn)Serializable接口
由于二級(jí)緩存的數(shù)據(jù)不一定都是存儲(chǔ)到內(nèi)存中,它的存儲(chǔ)介質(zhì)多種多樣清蚀,所以需要給緩存的對(duì)象執(zhí)行序列化

1.2.2.2 配置緩存

<cache 
eviction="FIFO"  <!-- 回收策略為先進(jìn)先出 -->
flushInterval="60000" <!--自動(dòng)刷新時(shí)間60s-->
size="512" <!--最多緩存512個(gè)引用對(duì)象-->
readOnly="true"/> <!--只讀-->

在需要開(kāi)啟的namespace下配置cache標(biāo)簽匕荸,其中標(biāo)簽中的屬性:

  • eviction:代表的是緩存回收策略,目前MyBatis提供以下策略:

LRU:最近最少使用的枷邪,一處最長(zhǎng)時(shí)間不用的對(duì)象
FIFO:先進(jìn)先出榛搔,按對(duì)象進(jìn)入緩存的順序來(lái)移除他們
SOFT:軟引用,移除基于垃圾回收器狀態(tài)和軟引用規(guī)則的對(duì)象
WEAK:弱引用东揣,更積極的移除基于垃圾收集器狀態(tài)和弱引用規(guī)則的對(duì)象践惑。這里采用的是LRU,移除最長(zhǎng)時(shí)間不用的對(duì)形象

  • flushInterval:刷新間隔時(shí)間嘶卧,單位為毫秒尔觉,這里配置的是100秒刷新,如果你不配置它芥吟,那么當(dāng)SQL被執(zhí)行的時(shí)候才會(huì)去刷新緩存
  • size:引用數(shù)目侦铜,一個(gè)正整數(shù),代表緩存最多可以存儲(chǔ)多少個(gè)對(duì)象钟鸵,不宜設(shè)置過(guò)大钉稍。設(shè)置過(guò)大會(huì)導(dǎo)致內(nèi)存溢出。這里配置的是1024個(gè)對(duì)象
  • readOnly:只讀棺耍,意味著緩存數(shù)據(jù)只能讀取而不能修改贡未,這樣設(shè)置的好處是我們可以快速讀取緩存,缺點(diǎn)是我們沒(méi)有辦法修改緩存,他的默認(rèn)值是false羞秤,即不允許修改

在對(duì)應(yīng)的sql語(yǔ)句上增加屬性useCache="true"開(kāi)啟緩存,或者flushCache="true"刷新緩存
namespace中配置緩存demo:

<?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 namespace="com.yihaomen.mybatis.dao.StudentMapper">
    <!--開(kāi)啟本mapper的namespace下的二級(jí)緩存-->
    <cache eviction="LRU" flushInterval="100000" readOnly="true" size="1024"/>
    <resultMap id="studentMap" type="Student">
        <id property="id" column="id" />
        <result property="name" column="name" />
        <result property="age" column="age" />
        <result property="gender" column="gender" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler" />
    </resultMap>
   
    <!--可以通過(guò)設(shè)置useCache來(lái)規(guī)定這個(gè)sql是否開(kāi)啟緩存左敌,ture是開(kāi)啟瘾蛋,false是關(guān)閉-->
    <select id="selectAllStudents" resultMap="studentMap" useCache="true">
        SELECT id, name, age FROM student
    </select>
    <!--刷新二級(jí)緩存
    <select id="selectAllStudents" resultMap="studentMap" flushCache="true">
        SELECT id, name, age FROM student
    </select>
    -->
</mapper>

或者在全局配置中開(kāi)啟緩存開(kāi)關(guān),這樣就把哪些不需要緩存的使用useCache="false"矫限,禁用緩存就可以了

<settings>
    <!-- 打開(kāi)延遲加載的開(kāi)關(guān) -->
    <setting name = "lazyLoadingEnabled" value = "true"/>
    <!-- 把積極加載修改為消極加載哺哼,即延遲加載 -->
    <setting name = "aggressiveLazyLoading" value = "false"/>
    <!-- 開(kāi)啟二級(jí)緩存 -->
    <setting name = "cacheEnabled" value = "true"/>
</settings>

1.2.2.3 SpringBoot使用二級(jí)緩存

在 yaml 中配置 cache-enabled 為 true

mybatis:
  configuration:
    cache-enabled: true

Mapper 接口上添加 @CacheNamespace 注解:
@CacheNamespaceMyBatis 框架中的注解,用于指定命名空間的緩存配置叼风。通過(guò)該注解取董,可以配置該命名空間下的緩存策略,包括緩存類(lèi)型无宿、緩存大小茵汰、緩存過(guò)期時(shí)間等。
使用 @CacheNamespace 注解可以提高 MyBatis 的查詢(xún)效率孽鸡,避免頻繁地訪(fǎng)問(wèn)數(shù)據(jù)庫(kù)蹂午,提高系統(tǒng)的性能。同時(shí)彬碱,也可以通過(guò)該注解來(lái)控制緩存的更新策略豆胸,保證數(shù)據(jù)的一致性。
需要注意的是巷疼,@CacheNamespace 注解只能用于命名空間級(jí)別的緩存配置晚胡,不能用于單個(gè) SQL 語(yǔ)句的緩存配置。如果需要對(duì)單個(gè) SQL 語(yǔ)句進(jìn)行緩存配置嚼沿,可以使用 @Options 注解或在 SQL 語(yǔ)句中使用 <cache> 標(biāo)簽來(lái)實(shí)現(xiàn)估盘。
@CacheNamespace@CacheNamespaceRef 區(qū)別:

  • @CacheNamespace@CacheNamespaceRef 都是 MyBatis 中用于配置緩存的注解,但是它們的作用和使用方式略有不同骡尽。
  • @CacheNamespace 注解用于標(biāo)注一個(gè) Mapper 接口的緩存配置忿檩,可以配置該 Mapper 接口中所有查詢(xún)語(yǔ)句的緩存策略。使用方式如下:
@CacheNamespace(implementation = MybatisRedisCache.class, eviction = MybatisRedisCache.class, flushInterval = 60000, size = 1024)
public interface UserMapper {
    // ...
}

其中爆阶,implementation 屬性指定了緩存實(shí)現(xiàn)類(lèi)燥透,eviction 屬性指定了緩存的清除策略,flushInterval 屬性指定了緩存的刷新時(shí)間間隔辨图,size 屬性指定了緩存的最大容量班套。

  • @CacheNamespaceRef 注解用于引用另一個(gè) Mapper 接口的緩存配置,可以將該 Mapper 接口的緩存配置與另一個(gè) Mapper 接口的緩存配置共享故河。使用方式如下:
@CacheNamespaceRef(UserMapper.class)
public interface OrderMapper {
    // ...
}

其中吱韭,value 屬性指定了被引用的 Mapper 接口。
需要注意的是,@CacheNamespace@CacheNamespaceRef 注解都需要與緩存實(shí)現(xiàn)類(lèi)一起使用理盆,用于指定緩存的具體實(shí)現(xiàn)痘煤。同時(shí),這兩個(gè)注解也可以同時(shí)使用猿规,用于實(shí)現(xiàn)更復(fù)雜的緩存配置衷快。

1.2.3 二級(jí)緩存生效&清除條件

生效的條件:

  • 當(dāng)會(huì)話(huà)提交或關(guān)閉之后才會(huì)填充二級(jí)緩存
  • 必須是同一個(gè) mapper,即同一個(gè)命名空間
  • 必須是相同的 statement姨俩,即同一個(gè) mapper 中的同一個(gè)方法
  • 必須是相同的 SQL 語(yǔ)句和參數(shù)
  • 如果 readWrite=true(默認(rèn)就是true)蘸拔,實(shí)體對(duì)像必須實(shí)現(xiàn) Serializable 接口

緩存清除條件:

  • 只有修改會(huì)話(huà)提交之后,才會(huì)執(zhí)行清空操作
  • xml 中配置的 update 不能清空 @CacheNamespace 中的緩存數(shù)據(jù)
  • 任何一種增刪改操作都會(huì)清空整個(gè) namespace 中的緩存

1.2.4 源碼中填充二級(jí)緩存

在生效條件中提到了环葵,二級(jí)緩存必須要在會(huì)話(huà)提交或關(guān)閉之后调窍,才能生效
在查詢(xún)到結(jié)果后,會(huì)調(diào)用 SqlSessioncommit 方法進(jìn)行提交(如果開(kāi)啟事務(wù)的話(huà)张遭,提交 SqlSession 走的不是這里了邓萨,但最終填充二級(jí)緩存的地方是一樣的。):

image.png

在此方法中菊卷,最終會(huì)調(diào)用到 TransactionalCacheflushPendingEntries 方法中填充二級(jí)緩存:

image.png

注意springboot 集成 mybatis 的話(huà)先誉,如果沒(méi)有開(kāi)啟事務(wù),每次執(zhí)行查詢(xún)的烁,都會(huì)創(chuàng)建新的 SqlSession褐耳,所以即使是在同一個(gè)方法中進(jìn)行查詢(xún)操作,那也是跨會(huì)話(huà)的渴庆。

1.2.5 查詢(xún)時(shí)如何使用二級(jí)緩存

在查詢(xún)的時(shí)候铃芦,最終會(huì)調(diào)用 MybatisCachingExecutorquery方法,里面會(huì)從 TransactionalCacheManager 中嘗試根據(jù) key 獲取二級(jí)緩存的內(nèi)容襟雷。
這個(gè) key 很長(zhǎng)刃滓,由 mapper、調(diào)用的查詢(xún)方法耸弄、SQL 等信息拼接而成咧虎,這也是為什么想要二級(jí)緩存生效,必須滿(mǎn)足前面所說(shuō)的條件计呈。
如果能在二級(jí)緩存中查詢(xún)到砰诵,就直接返回了,不需要訪(fǎng)問(wèn)數(shù)據(jù)庫(kù)捌显。

image.png

具體的調(diào)用層數(shù)實(shí)在太多茁彭,用到了裝飾者模式,最終是在 PerpetualCache 中獲取緩存的:


image.png

打印日志是在 LoggingCache 中:

image.png

1.2.6 為什么mybatis默認(rèn)不開(kāi)啟二級(jí)緩存

二級(jí)緩存雖然能帶來(lái)一定的好處扶歪,但是有很大的隱藏危害
它的緩存是以 namespace(mapper) 為單位的理肺,不同 namespace 下的操作互不影響。且 insert/update/delete 操作會(huì)清空所在 namespace 下的全部緩存。
那么問(wèn)題就出來(lái)了妹萨,假設(shè)現(xiàn)在有 ItemMapper 以及 XxxMapper年枕,在 XxxMapper 中做了表關(guān)聯(lián)查詢(xún),且做了二級(jí)緩存乎完。此時(shí)在 ItemMapper 中將 item 信息給刪了熏兄,由于不同 namespace 下的操作互不影響,XxxMapper 的二級(jí)緩存不會(huì)變囱怕,那之后再次通過(guò) XxxMapper 查詢(xún)的數(shù)據(jù)就不對(duì)了,非常危險(xiǎn)毫别。

來(lái)看一個(gè)例子:

@Mapper
@Repository
@CacheNamespace
public interface XxxMapper {
 
    @Select("select i.id itemId,i.name itemName,p.amount,p.unit_price unitPrice " +
            "from item i JOIN payment p on i.id = p.item_id where i.id = #{id}")
    List<PaymentVO> getPaymentVO(Long id);
 
}
 
 
@Autowired
private XxxMapper xxxMapper;
 
@Test
void test() {
 System.out.println("==================== 查詢(xún)PaymentVO ====================");
 List<PaymentVO> voList = xxxMapper.getPaymentVO(1L);
 System.out.println(JSON.toJSONString(voList.get(0)));
 System.out.println("====================  更新item表的name ==================== ");
 Item item = itemMapper.selectById(1);
 item.setName("java并發(fā)編程");
 itemMapper.updateById(item);
 System.out.println("====================  重新查詢(xún)PaymentVO ==================== ");
 List<PaymentVO> voList2 = xxxMapper.getPaymentVO(1L);
 System.out.println(JSON.toJSONString(voList2.get(0)));
}

上面的代碼娃弓,test()方法中前后兩次調(diào)用了 xxxMapper.getPaymentVO 方法,因?yàn)闆](méi)有加 @Transactional 注解岛宦,所以前后兩次查詢(xún)台丛,是兩個(gè)不同的會(huì)話(huà),第一次查詢(xún)完后砾肺,SqlSession 會(huì)自動(dòng) commit挽霉,所以二級(jí)緩存能夠生效;
然后在中間進(jìn)行了 Item 表的更新操作变汪,修改了下名稱(chēng)侠坎;
由于itemMapperxxxMapper 不是同一個(gè)命名空間,所以 itemMapper 執(zhí)行的更新操作不會(huì)影響到 xxxMapper 的二級(jí)緩存裙盾;
再次調(diào)用 xxxMapper.getPaymentVO实胸,發(fā)現(xiàn)取出的值是走緩存的,itemName 還是老的番官。但實(shí)際上 itemName 在上面已經(jīng)被改了

1.3 使用Ehcache

點(diǎn)擊此處了解Ehcache原理
Mybatis本身是一個(gè)持久層框架庐完,它不是專(zhuān)門(mén)的緩存框架,所以它對(duì)緩存的實(shí)現(xiàn)不夠好徘熔,不能支持分布式门躯。
Ehcache是一個(gè)分布式的緩存框架。

1.3.1 mapper.xml文件中使用

設(shè)置映射文件中cache標(biāo)簽的type值為ehcache的實(shí)現(xiàn)類(lèi)

<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>

<select id="findUserById" parameterType="java.lang.Long" resultType="com.sxt.model.User" flushCache="false">
    SELECT * FROM tb_user WHERE id = #{id}
</select>

1.3.2 添加Ehcache的配置文件

src/main/resources下創(chuàng)建cache文件夾酷师,在文件夾下創(chuàng)建ehcache.xml

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">

    <diskStore path="java.io.tmpdir"/>
    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            overflowToDisk="true"
            maxElementsOnDisk="10000000"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU"
    />
</ehcache>

defaultCache標(biāo)簽中屬性:

  • name:緩存名稱(chēng)讶凉。
  • maxElementsInMemory:緩存最大個(gè)數(shù)。
  • eternal:對(duì)象是否永久有效山孔,一但設(shè)置了缀遍,timeout將不起作用。
  • timeToIdleSeconds:設(shè)置對(duì)象在失效前的允許閑置時(shí)間(單位:秒)饱须。僅當(dāng)eternal=false對(duì)象不是永久有效時(shí)使用域醇,可選屬性,默認(rèn)值是0,也就是可閑置時(shí)間無(wú)窮大譬挚。
  • timeToLiveSeconds:設(shè)置對(duì)象在失效前允許存活時(shí)間(單位:秒)锅铅。最大時(shí)間介于創(chuàng)建時(shí)間和失效時(shí)間之間。僅當(dāng)eternal=false對(duì)象不是永久有效時(shí)使用减宣,默認(rèn)是0盐须,也就是對(duì)象存活時(shí)間無(wú)窮大。
  • overflowToDisk:當(dāng)內(nèi)存中對(duì)象數(shù)量達(dá)到maxElementsInMemory時(shí)漆腌,Ehcache將會(huì)對(duì)象寫(xiě)到磁盤(pán)中贼邓。
  • diskSpoolBufferSizeMB:這個(gè)參數(shù)設(shè)置DiskStore(磁盤(pán)緩存)的緩存區(qū)大小。默認(rèn)是30MB闷尿。每個(gè)Cache都應(yīng)該有自己的一個(gè)緩沖區(qū)塑径。
  • maxElementsOnDisk:硬盤(pán)最大緩存?zhèn)€數(shù)。
  • diskPersistent:是否緩存虛擬機(jī)重啟期數(shù)據(jù) ,The default value is false.
  • diskExpiryThreadIntervalSeconds:磁盤(pán)失效線(xiàn)程運(yùn)行時(shí)間間隔填具,默認(rèn)是120秒
  • memoryStoreEvictionPolicy:當(dāng)達(dá)到maxElementsInMemory限制時(shí)统舀,Ehcache將會(huì)根據(jù)指定的策略去清理內(nèi)存。默認(rèn)策略是LRU(最近最少使用)劳景。你可以設(shè)置為FIFO(先進(jìn)先出)或是LFU(較少使用)誉简。
  • clearOnFlush:內(nèi)存數(shù)量最大時(shí)是否清除

1.4 Mybatis的Executor執(zhí)行器

Mybatis有三種基本的Executor執(zhí)行器,SimpleExecutor盟广、ReuseExecutor闷串、BatchExecutor

  1. SimpleExecutor:每執(zhí)行一次updateselect,就開(kāi)啟一個(gè)Statement對(duì)象筋量,用完立刻關(guān)閉Statement對(duì)象窿克。
  2. ReuseExecutor:執(zhí)行updateselect,以sql作為key查找Statement對(duì)象毛甲,存在就使用年叮,不存在就創(chuàng)建,用完后玻募,不關(guān)閉Statement對(duì)象只损,而是放置于Map<String, Statement>內(nèi),供下一次使用七咧。簡(jiǎn)言之跃惫,就是重復(fù)使用Statement對(duì)象。
  3. BatchExecutor:執(zhí)行update(沒(méi)有select艾栋,JDBC批處理不支持select)渗常,將所有sql都添加到批處理中(addBatch())苔咪,等待統(tǒng)一執(zhí)行(executeBatch() ),它緩存了多個(gè)Statement對(duì)象,每個(gè)Statement對(duì)象都是addBatch()完畢后缩功,等待逐一執(zhí)行executeBatch()批處理。與JDBC批處理相同

2 Mybatis攔截器

2.1 攔截器介紹

2.1.1 簡(jiǎn)介

Mybatis攔截器設(shè)計(jì)的初衷就是為了供用戶(hù)在某些時(shí)候可以實(shí)現(xiàn)自己的邏輯而不必去動(dòng)Mybatis固有的邏輯。減少代碼侵入
通過(guò)Mybatis攔截器我們可以攔截某些方法的調(diào)用,我們可以選擇在這些被攔截的方法執(zhí)行前后加上某些邏輯曾棕,也可以在執(zhí)行這些被攔截的方法時(shí)執(zhí)行自己的邏輯而不再執(zhí)行被攔截的方法。所以Mybatis攔截器的使用范圍是非常廣泛的菜循。

2.1.2 核心概念

MyBatis 攔截器工作時(shí)翘地,核心組件是 InvocationInterceptor癌幕、MethodTarget 對(duì)象等:

  • Interceptor:這是所有自定義攔截器的接口衙耕,MyBatis 會(huì)根據(jù)配置找到并調(diào)用實(shí)現(xiàn)該接口的類(lèi)。
  • Invocation:封裝了方法調(diào)用的對(duì)象勺远,它包含了目標(biāo)方法的信息以及方法的參數(shù)橙喘。通過(guò) Invocation 對(duì)象,我們可以對(duì)方法的執(zhí)行進(jìn)行控制谚中。
  • Method:表示目標(biāo)方法渴杆,它是通過(guò)反射來(lái)獲取的寥枝。
  • Target:表示目標(biāo)對(duì)象宪塔,它是被攔截的對(duì)象。例如囊拜,Executor某筐、StatementHandler 等都是目標(biāo)對(duì)象,攔截器會(huì)通過(guò) Target 對(duì)象來(lái)訪(fǎng)問(wèn)和控制這些對(duì)象的行為冠跷。

2.1.3 目標(biāo)對(duì)象和方法

Mybatis攔截器并不是每個(gè)對(duì)象里面的方法都可以被攔截的南誊。Mybatis攔截器只能攔截Executor、ParameterHandler蜜托、StatementHandler抄囚、ResultSetHandler四個(gè)對(duì)象里面的方法

默認(rèn)情況下,MyBatis允許使用插件來(lái)攔截的方法調(diào)用包括:

  • 攔截執(zhí)行器方法:Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
    執(zhí)行 SQL 語(yǔ)句的核心對(duì)象橄务,它的 update()幔托、query() 等方法負(fù)責(zé)執(zhí)行實(shí)際的增、刪蜂挪、改重挑、查操作
  • 攔截參數(shù)的處理:ParameterHandler (getParameterObject, setParameters)
    處理 SQL 參數(shù)綁定的對(duì)象,它負(fù)責(zé)將參數(shù)設(shè)置到 SQL 語(yǔ)句中
  • 攔截結(jié)果集的處理:ResultSetHandler (handleResultSets, handleOutputParameters)
    處理 SQL 查詢(xún)結(jié)果的對(duì)象棠涮,它負(fù)責(zé)將從數(shù)據(jù)庫(kù)返回的 ResultSet 轉(zhuǎn)換為 Java 對(duì)象谬哀。
  • 攔截Sql語(yǔ)法構(gòu)建的處理:StatementHandler (prepare, parameterize, batch, update, query)
    處理 SQL 語(yǔ)句的對(duì)象,它負(fù)責(zé)將 SQL 語(yǔ)句和參數(shù)綁定严肪,并將其傳遞給數(shù)據(jù)庫(kù)史煎。
  • 不同攔截器順序:Executor -> ParameterHandler -> StatementHandler -> ResultSetHandler

2.2 Mybatis攔截器接口

public interface Interceptor {
//代理對(duì)象每次調(diào)用的方法谦屑,就是要進(jìn)行攔截的時(shí)候要執(zhí)行的方法。在這個(gè)方法里面做我們自定義的邏輯處理
Object intercept(Invocation invocation) throws Throwable;
//plugin方法是攔截器用于封裝目標(biāo)對(duì)象的劲室,通過(guò)該方法我們可以返回目標(biāo)對(duì)象本身伦仍,也可以返回一個(gè)它的代理,
//當(dāng)返回的是代理的時(shí)候我們可以對(duì)其中的方法進(jìn)行攔截來(lái)調(diào)用intercept方法 -- Plugin.wrap(target, this)很洋,
//當(dāng)返回的是當(dāng)前對(duì)象的時(shí)候 就不會(huì)調(diào)用intercept方法充蓝,相當(dāng)于當(dāng)前攔截器無(wú)效
Object plugin(Object target);
//用于在Mybatis配置文件中指定一些屬性的,注冊(cè)當(dāng)前攔截器的時(shí)候可以設(shè)置一些屬性
void setProperties(Properties properties);
}

2.3 @Intercepts注解

Intercepts注解需要一個(gè)Signature(攔截點(diǎn))參數(shù)數(shù)組喉磁。通過(guò)Signature來(lái)指定攔截哪個(gè)對(duì)象里面的哪個(gè)方法
@Intercepts注解定義如下

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
    /**
     * 定義攔截點(diǎn)
     * 只有符合攔截點(diǎn)的條件才會(huì)進(jìn)入到攔截器
     */
    Signature[] value();
}

Signature來(lái)指定咱們需要攔截那個(gè)類(lèi)對(duì)象的哪個(gè)方法谓苟。定義如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
    /**
     * 定義攔截的類(lèi) Executor、ParameterHandler协怒、StatementHandler涝焙、ResultSetHandler當(dāng)中的一個(gè)
     */
    Class<?> type();

    /**
     * 在定義攔截類(lèi)的基礎(chǔ)之上,在定義攔截的方法
     */
    String method();

    /**
     * 在定義攔截方法的基礎(chǔ)之上在定義攔截的方法對(duì)應(yīng)的參數(shù)孕暇,
     * JAVA里面方法可能重載仑撞,不指定參數(shù),不曉得是那個(gè)方法
     */
    Class<?>[] args();
}

我們舉一個(gè)例子來(lái)說(shuō)明妖滔,比如我們自定義一個(gè)MybatisInterceptor類(lèi)隧哮,來(lái)攔截Executor類(lèi)里面的兩個(gè)方法。自定義攔截類(lèi)MybatisInterceptor

@Intercepts({
        @Signature(
                type = Executor.class,
                method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
        ),
        @Signature(
                type = Executor.class,
                method = "update",
                args = {MappedStatement.class, Object.class}
        )
})
public class MybatisInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        // TODO: 自定義攔截邏輯

    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this); // 返回代理類(lèi)
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

把源碼知道攔截的主要是如下方法:

public <E> List<E> selectList(String statement, Object parameter) {  
    return this.selectList(statement, parameter, RowBounds.DEFAULT);  
}  
 
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {  
    try {  
        //1.根據(jù)Statement Id座舍,在mybatis 配置對(duì)象Configuration中查找和配置文件相對(duì)應(yīng)的MappedStatement      
        MappedStatement ms = configuration.getMappedStatement(statement);  
        //2. 將查詢(xún)?nèi)蝿?wù)委托給MyBatis 的執(zhí)行器 Executor  
        List<E> result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);  
        return result;  
    } catch (Exception e) {  
        throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);  
    } finally {  
        ErrorContext.instance().reset();  
    }  
} 
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末沮翔,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子曲秉,更是在濱河造成了極大的恐慌采蚀,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件承二,死亡現(xiàn)場(chǎng)離奇詭異榆鼠,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)亥鸠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)妆够,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人读虏,你說(shuō)我怎么就攤上這事责静。” “怎么了盖桥?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵灾螃,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我揩徊,道長(zhǎng)腰鬼,這世上最難降的妖魔是什么嵌赠? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮熄赡,結(jié)果婚禮上姜挺,老公的妹妹穿的比我還像新娘。我一直安慰自己彼硫,他們只是感情好炊豪,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著拧篮,像睡著了一般词渤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上串绩,一...
    開(kāi)封第一講書(shū)人閱讀 51,115評(píng)論 1 296
  • 那天缺虐,我揣著相機(jī)與錄音,去河邊找鬼礁凡。 笑死高氮,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的顷牌。 我是一名探鬼主播剪芍,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼韧掩!你這毒婦竟也來(lái)了紊浩?” 一聲冷哼從身側(cè)響起窖铡,我...
    開(kāi)封第一講書(shū)人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤疗锐,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后费彼,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體滑臊,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年箍铲,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了雇卷。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡颠猴,死狀恐怖关划,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情翘瓮,我是刑警寧澤贮折,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站资盅,受9級(jí)特大地震影響调榄,放射性物質(zhì)發(fā)生泄漏踊赠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一每庆、第九天 我趴在偏房一處隱蔽的房頂上張望筐带。 院中可真熱鬧,春花似錦缤灵、人聲如沸伦籍。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)鸽斟。三九已至,卻和暖如春利诺,著一層夾襖步出監(jiān)牢的瞬間富蓄,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工慢逾, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留立倍,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓侣滩,卻偏偏與公主長(zhǎng)得像口注,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子君珠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容