1. MyBatis 的快取機制
快取(Cache)
快取的作用:透過減少 IO 的方式,來提高程式的執行效率 。
MyBatis 的快取:將 Select 語句的查詢結果放到快取(記憶體)當中,下一次還是這條 Select 語句的話,直接就從快取當中取了,不再查詢資料庫。這樣一方面減少了 IO,另一方面不再執行繁瑣的查詢演算法。效率大大提升。
MyBatis 快取包括:
一級快取:將查詢到的資料儲存到 SqlSession 中
二級快取:將查詢到的資料儲存到 SqlSessionFactory 中
或者是整合其它第三方的快取:比如 EhCache(Java語言開發的),Memcache(C語言開發的)等。
注意:快取只針對於 DQL(查詢)語句,也就是說快取機制只對應 Select 語句。
一旦你執行了,insert 或者delete或者 update 更新語句,無論是否是更新修改刪除那個資料表中的記錄,都會清空快取,所以,這樣就不會導致 快取當中的 select 語句的資料是:舊的無用的資料了。
2. 準備工作
資料表結構的設計,資料表名為:t_car
t_car 表中的資料資訊:
在pom.xml
檔案當中配置相關的依賴的 jar 包如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.rainbowsea</groupId> <artifactId>mybatis-005-crud-blog</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> </properties> <dependencies> <!-- mybatis 的依賴--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.10</version> </dependency> <!-- mysql --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> <!-- 引入 logback的依賴,這個日誌框架實現了slf4j 規範--> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.11</version> </dependency> </dependencies> </project>
配置 logback 的配置檔案,用於列印顯示,我們的日誌資訊,方便我們檢視我們的執行過程,效果。
<?xml version="1.0" encoding="UTF-8"?> <configuration debug="false"> <!-- 控制檯輸出 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <!--格式化輸出:%d表示日期,%thread表示執行緒名,%-5level:級別從左顯示5個字元寬度%msg:日誌訊息,%n是換行符--> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> </encoder> </appender> <!--mybatis log configure--> <logger name="com.apache.ibatis" level="TRACE"/> <logger name="java.sql.Connection" level="DEBUG"/> <logger name="java.sql.Statement" level="DEBUG"/> <logger name="java.sql.PreparedStatement" level="DEBUG"/> <!-- 日誌輸出級別,logback日誌級別包括五個:TRACE < DEBUG < INFO < WARN < ERROR --> <root level="DEBUG"> <appender-ref ref="STDOUT"/> <appender-ref ref="FILE"/> </root> </configuration>
配置 MyBatis 的核心配置檔案,
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!-- 使用 <package> 還可以將這個包下的所有的類的全部自動起別名,別名就是簡名,不區分大小寫 --> <package name="com.rainbowsea.mybatis.pojo"/> </typeAliases> <environments default="mybatis"> <environment id="mybatis"> <!-- MANAGED 沒有用第三框架管理的話,都是會被提交的,沒有事務上的管理了 --> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/mybatis"/> <property name="username" value="root"/> <property name="password" value="MySQL123"/> </dataSource> </environment> </environments> <mappers> <!-- 這裏也是可以使用 package 包名掃描,但是同樣的:對應介面路徑要一致,介面名一致--> <package name="com.rainbowsea.mybatis.mapper"></package> </mappers> </configuration>
對照 t_car 建立的ORM 對映的 Car 類
注意:在MyBatis 當中對應的ORM ,一般在框架裡對應的 Bean實體類,一定要實現該 set 和 get 方法以及無引數構造方法,無法框架無法使用反射機制,進行操作 。
建議用包裝類,這樣可以防止 Null的問題,因為(簡單型別 int num = null ,是不可以賦值為 null)的編譯無法透過
package com.rainbowsea.mybatis.pojo; public class Car { // 資料庫表當中的欄位應該和pojo類的屬性一一對應 // 建議使用包裝類,這樣可以防止null的問題 private Long id; private String carNum; private String brand; private Double guidePrice; private String produceTime; private String carType; public Car() { } public Car(Long id, String carNum, String brand, Double guidePrice, String produceTime, String carType) { this.id = id; this.carNum = carNum; this.brand = brand; this.guidePrice = guidePrice; this.produceTime = produceTime; this.carType = carType; } @Override public String toString() { return "Car{" + "id=" + id + ", carNum='" + carNum + '\'' + ", brand='" + brand + '\'' + ", guidePrice=" + guidePrice + ", produceTime='" + produceTime + '\'' + ", catType='" + carType + '\'' + '}'; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getCarNum() { return carNum; } public void setCarNum(String carNum) { this.carNum = carNum; } public String getBrand() { return brand; } public void setBrand(String brand) { this.brand = brand; } public Double getGuidePrice() { return guidePrice; } public void setGuidePrice(Double guidePrice) { this.guidePrice = guidePrice; } public String getProduceTime() { return produceTime; } public void setProduceTime(String produceTime) { this.produceTime = produceTime; } public String getcarType() { return carType; } public void setcarType(String catType) { this.carType = catType; } }
3. MyBatis 的一級快取
一級快取預設是開啟的。不需要做任何配置。
原理:只要使用同一個SqlSession物件執行同一條SQL語句,就會走快取。
一級快取的內容是:將查詢到的資料儲存到 SqlSession 當中的。注意:其快取的作用域
package com.rainbowsea.mybatis.mapper; import com.rainbowsea.mybatis.pojo.Car; import com.rainbowsea.mybatis.pojo.Clazz; public interface CarMapper { Car selectById(Long id); }
<?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"> <!--namespace 一定要是:對應的介面的全限定類名--> <mapper namespace="com.rainbowsea.mybatis.mapper.CarMapper"> <!-- id 要是 namespace 對應介面上的方法名: --> <select id="selectById" resultType="Car"> select id, car_num, brand, guide_price, produce_time, car_type from t_car where id = #{id} </select> </mapper>
執行測試:
@Test public void testSelectById() throws IOException { SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"), "mybatis"); SqlSession sqlSession = sqlSessionFactory.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); Car car = mapper.selectById(118L); System.out.println(car); CarMapper mapper1 = sqlSession.getMapper(CarMapper.class); Car car1 = mapper1.selectById(118L); System.out.println(car1); }
執行 Select 查詢語句的時候,首先從對應的這個 Select 語句的 SqlSession 物件(一級快取)當中查詢是否有對應該Select 查詢語句的快取有的話,就不執行該 查詢的 SQL 語句了,而是直接從一級快取當中取出這個 Select 查詢語句的資料結果。第一次執行 Select 語句(因為MyBatis 一級快取預設是開啟的)就會將存入到 sqlSession 物件(一級快取)當中,方便後續的查詢。
3.1 一級快取失效情況/條件
一級快取失效了,二級快取同樣也是失效的了,所以一級快取失效的條件也是二級快取失效的條件,他們的條件都是一樣的。
思考:什麼時候不走快取?
sqlSession 物件不是同一個,肯定不走快取。
查詢條件不一樣,肯定不走快取。
一級快取失效情況包括兩種:
第一種:第一次查詢和第二次查詢之間,手動清空了一級快取。執行:執行了 sqlSession.clearCache()方法,這是手動情況快取。
第二種:執行了INSERT 或 DELETE 或UPDATE語句,不管你是操作任意一張表,都會清空一級快取。
無論你是,你做了以上兩件事的任意一種,都會讓一級快取清空 。
第一種:第一次查詢和第二次查詢之間,手動清空了一級快取。執行:執行了 sqlSession.clearCache()方法,這是手動情況快取。
測試:
package com.rainbowsea.mybatis.test; import com.rainbowsea.mybatis.mapper.CarMapper; import com.rainbowsea.mybatis.pojo.Car; import com.rainbowsea.mybatis.pojo.Clazz; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.junit.Test; import java.io.IOException; public class CarMapperTest { /** * 思考:什麼時候不走快取? * sqlsession 物件不是同一個,肯定不走快取 * 查詢條件不一樣,肯定不走快取 * <p> * 思考什麼時候一級快取失敗? * 第一次DQL和第二次DQL之間你做了一下兩件事的任意一種,都會讓一級快取清空 * 1. 執行了 sqlSession.clearCache()方法,這是手動情況快取 * 2. 執行了INSERT 或 DELETE 或UPDATE語句,不管你是操作那張表,都會清空一級快取 * * @throws IOException */ @Test public void testSelectById3() throws IOException { SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"), "mybatis"); SqlSession sqlSession = sqlSessionFactory.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); Car car = mapper.selectById(118L); System.out.println(car); // 手動清空一級快取 sqlSession.clearCache(); CarMapper mapper1 = sqlSession.getMapper(CarMapper.class); Car car1 = mapper1.selectById(118L); System.out.println(car1); sqlSession.close(); } }
第二種:第一次查詢和第二次查詢之間,執行了增刪改操作。【這個增刪改和哪張表沒有關係,只要有insert delete update操作,一級快取就失效。】
@Test public void testSelectById3() throws IOException { SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"), "mybatis"); SqlSession sqlSession = sqlSessionFactory.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); Car car = mapper.selectById(118L); System.out.println(car); // 在這裏執行 insert或者 delete 或者 update 中的任意一個語句,並且和表沒有關係 CarMapper mapper2 = sqlSession.getMapper(CarMapper.class); mapper2.insertClazz(new Clazz(2000,"高三三班")); CarMapper mapper1 = sqlSession.getMapper(CarMapper.class); Car car1 = mapper1.selectById(118L); System.out.println(car1); sqlSession.close(); }
4. MyBatis 的二級快取
二級快取的範圍是SqlSessionFactory。
二級快取:將查詢到的資料儲存到 SqlSessionFactory 中,範圍比一級快取中更大一些。
使用二級快取步驟/條件 :
要在 MyBatis 的核心配置檔案當中,設定<setting name="cacheEnabled" value="true"> 全域性性地開啟或關閉所有對映器配置檔案中已配置的任何快取。預設就是true,無需設定 。
在需要使用二級快取的 對應的
SqlMapper.xml
檔案中新增配置:使用二級快取的實體類物件必須是可序列化的,也就是對應的POJO實體類,必須實現java.io.Serializable 介面
只有 當 SqlSession物件關閉或提交之後,一級快取中的資料纔會被寫入到二級快取當中。此時二級快取纔可用,不然沒有提交/關閉,二級快取是沒有儲存到資料資訊的,是無效的。
第一步: 要在 MyBatis 的核心配置檔案當中,設定<setting name="cacheEnabled" value="true"> 全域性性地開啟或關閉所有對映器配置檔案中已配置的任何快取。預設就是true,無需設定 。
第二步: 在需要使用二級快取的 對應的 SqlMapper.xml
檔案中新增配置:
<?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"> <!--namespace 一定要是:對應的介面的全限定類名--> <mapper namespace="com.rainbowsea.mybatis.mapper.CarMapper"> <!-- 預設情況下,二級快取機制是開啟的 只需要在對應的SqlMapper.xml檔案中新增以下標籤,用來表示“我”使用該二級快取 --> <cache></cache> <insert id="insertClazz"> insert into t_clazz values (#{cid},#{cname}) </insert> </mapper>
第三步: 使用二級快取的實體類物件必須是可序列化的,也就是對應的POJO實體類,必須實現java.io.Serializable 介面
第四步: 只有 當 SqlSession物件關閉或提交之後,一級快取中的資料纔會被寫入到二級快取當中。此時二級快取纔可用,不然沒有提交/關閉,二級快取是沒有儲存到資料資訊的,是無效的。
import com.rainbowsea.mybatis.mapper.CarMapper; import com.rainbowsea.mybatis.pojo.Car; import com.rainbowsea.mybatis.pojo.Clazz; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.junit.Test; import java.io.IOException; public class CarMapperTest { @Test public void testSelectById4() throws IOException { // 這裏只有一個SqlSessionFactory 物件,二級快取對應的就是SqlSessionFactory SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"), "mybatis"); SqlSession sqlSession1 = sqlSessionFactory.openSession(); SqlSession sqlSession2 = sqlSessionFactory.openSession(); CarMapper mapper1 = sqlSession1.getMapper(CarMapper.class); CarMapper mapper2 = sqlSession2.getMapper(CarMapper.class); // 這行程式碼執行結束之後,時間上資料快取到一級快取當中了,(sqlSession是一級快取) Car car = mapper1.selectById(118L); System.out.println(car); // 如果這裏不關閉sqlSession物件的話,二級快取中還是沒有資料的 // 如果執行了這行程式碼,sqlSession1的一級快取中的資料會放到二級快取當中 sqlSession1.close(); // 這行程式碼執行結束之後,實際上資料會快取到一級快取當中。(sqlSession2 是一級快取) Car car1 = mapper2.selectById(118L); System.out.println(car1); // 程式執行到這裏的時候,會有SqlSession1這個一級快取中的資料寫入到二級快取當中 // sqlSession1.close() // 程式執行到這裏的時候,會將sqlSession2這個一級快取中的資料寫入到二級快取當中 sqlSession2.close(); } }
執行測試:
二級快取的失效:只要兩次查詢之間出現了增刪改操作。二級快取就會失效。【一級快取也會失效】
二級快取的相關配置:
eviction :指定從快取中移除某個物件的淘汰演算法。預設採用LRU策略。
LRU:Least Recently Used。最近最少使用。優先淘汰在間隔時間內使用頻率最低的物件。(其實還有一種淘汰演算法LFU,最不常用。)
FIFO:First In First Out。一種先進先出的資料快取器。先進入二級快取的物件最先被淘汰。
SOFT:軟引用。淘汰軟引用指向的物件。具體演算法和JVM的垃圾回收演算法有關。
WEAK:弱引用。淘汰弱引用指向的物件。具體演算法和JVM的垃圾回收演算法有關。
flushInterval :
二級快取的重新整理時間間隔。單位毫秒。如果沒有設定。就代表不重新整理快取,只要記憶體足夠大,一直會向二級快取中快取資料。除非執行了增刪改。
readOnly :
true:多條相同的sql語句執行之後返回的物件是共享的同一個。效能好。但是多執行緒併發可能會存在安全問題。
false:多條相同的sql語句執行之後返回的物件是副本,呼叫了clone方法。效能一般。但安全。
size :
設定二級快取中最多可儲存的java物件數量。預設值1024。
5. MyBatis 整合 EhCache 第三方快取
整合EhCache是爲了代替mybatis自帶的二級快取。一級快取是無法替代的。
mybatis對外提供了介面,也可以整合第三方的快取元件。比如EhCache、Memcache等。都可以。
EhCache是Java寫的。Memcache是C語言寫的。所以mybatis整合EhCache較為常見,按照以下步驟操作,就可以完成整合:
第一步: 引入mybatis 整合 ehcache 的依賴。
<!--mybatis整合ehcache的元件--> <dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-ehcache</artifactId> <version>1.2.2</version> </dependency>
第二步: 在類的根路徑下新建 echcache.xml
(檔名必須是:echcache.xml 不可以修改)檔案,並提供以下配置資訊。
<?xml version="1.0" encoding="UTF-8"?> <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd" updateCheck="false"> <!--磁碟儲存:將快取中暫時不使用的物件,轉移到硬碟,類似於Windows系統的虛擬記憶體--> <diskStore path="e:/ehcache"/> <!--defaultCache:預設的管理策略--> <!--eternal:設定快取的elements是否永遠不過期。如果為true,則快取的資料始終有效,如果為false那麼還要根據timeToIdleSeconds,timeToLiveSeconds判斷--> <!--maxElementsInMemory:在記憶體中快取的element的最大數目--> <!--overflowToDisk:如果記憶體中資料超過記憶體限制,是否要快取到磁碟上--> <!--diskPersistent:是否在磁碟上持久化。指重啟jvm後,資料是否有效。預設為false--> <!--timeToIdleSeconds:物件空閒時間(單位:秒),指物件在多長時間沒有被訪問就會失效。只對eternal為false的有效。預設值0,表示一直可以訪問--> <!--timeToLiveSeconds:物件存活時間(單位:秒),指物件從建立到失效所需要的時間。只對eternal為false的有效。預設值0,表示一直可以訪問--> <!--memoryStoreEvictionPolicy:快取的3 種清空策略--> <!--FIFO:first in first out (先進先出)--> <!--LFU:Less Frequently Used (最少使用).意思是一直以來最少被使用的。快取的元素有一個hit 屬性,hit 值最小的將會被清出快取--> <!--LRU:Least Recently Used(最近最少使用). (ehcache 預設值).快取的元素有一個時間戳,當快取容量滿了,而又需要騰出地方來快取新的元素的時候,那麼現有快取元素中時間戳離當前時間最遠的元素將被清出快取--> <defaultCache eternal="false" maxElementsInMemory="1000" overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="0" timeToLiveSeconds="600" memoryStoreEvictionPolicy="LRU"/> </ehcache>
第三步: 修改對應的 SqlMapper.xml檔案中的標籤,新增type屬性。
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
import com.rainbowsea.mybatis.mapper.CarMapper; import com.rainbowsea.mybatis.pojo.Car; import com.rainbowsea.mybatis.pojo.Clazz; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.junit.Test; import java.io.IOException; public class CarMapperTest { @Test public void testSelectById5() throws Exception { // 這裏只有一個SqlSessionFactory 物件,二級快取對應的就是SqlSessionFactory SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"), "mybatis"); SqlSession sqlSession1 = sqlSessionFactory.openSession(); CarMapper mapper1 = sqlSession1.getMapper(CarMapper.class); Car car1 = mapper1.selectById(118L); System.out.println(car1); sqlSession1.close(); SqlSession sqlSession2 = sqlSessionFactory.openSession(); CarMapper mapper2 = sqlSession2.getMapper(CarMapper.class); Car car2 = mapper2.selectById(118L); System.out.println(car2); sqlSession2.close(); } }
**第四步:**編寫測試程式使用。
import com.rainbowsea.mybatis.mapper.CarMapper; import com.rainbowsea.mybatis.pojo.Car; import com.rainbowsea.mybatis.pojo.Clazz; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.junit.Test; import java.io.IOException; public class CarMapperTest { @Test public void testSelectById5() throws Exception { // 這裏只有一個SqlSessionFactory 物件,二級快取對應的就是SqlSessionFactory SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"), "mybatis"); SqlSession sqlSession1 = sqlSessionFactory.openSession(); CarMapper mapper1 = sqlSession1.getMapper(CarMapper.class); Car car1 = mapper1.selectById(118L); System.out.println(car1); sqlSession1.close(); SqlSession sqlSession2 = sqlSessionFactory.openSession(); CarMapper mapper2 = sqlSession2.getMapper(CarMapper.class); Car car2 = mapper2.selectById(118L); System.out.println(car2); sqlSession2.close(); } }
6. 總結:
MyBatis 快取包括:
一級快取:將查詢到的資料儲存到 SqlSession 中
二級快取:將查詢到的資料儲存到 SqlSessionFactory 中
或者是整合其它第三方的快取:比如 EhCache(Java語言開發的),Memcache(C語言開發的)等。
注意:快取只針對於 DQL(查詢)語句,也就是說快取機制只對應 Select 語句。
一級快取預設是開啟的
一級快取失效情況包括兩種:
第一種:第一次查詢和第二次查詢之間,手動清空了一級快取。執行:執行了 sqlSession.clearCache()方法,這是手動情況快取。
第二種:執行了INSERT 或 DELETE 或UPDATE語句,不管你是操作任意一張表,都會清空一級快取。
一級快取失效了,二級快取也是失效了,二級快取是透過將一級快取的快取儲存到二級快取當中的,所以一級失效,二級也是失效的
二級快取:將查詢到的資料儲存到 SqlSessionFactory 中,範圍比一級快取中更大一些。