一、緩存機制的原理
計算機每次從mysql中執行sql語句,都是內存與硬盤的通信,對計算機來說,影響效率。
因此使用緩存機制。
?
1-1、MyBatis 的緩存機制:
執行 DQL(select 語句)的時候,將查詢結果放到緩存當中(內存當中),如果下一次還是執行完全相同的 DQL(select 語句)語句,直接從緩存中拿數據,不再查數據庫了。不再去硬盤上找數據了。
示例:
第一次執行這個 SQL:
select * from t_car where id=1;
第二次還是執行這個 SQL,完全一樣:
select * from t_car where id=1;
此時,從緩存中獲取,不再查數據庫了。
當兩條sql語句之間,對數據庫做了任何修改操作,緩存將從內存中清除。
目的:提高執行效率。
緩存機制:使用減少 IO 的方式來提高效率。
IO:讀文件和寫文件。
緩存通常是我們程序開發中優化程序的重要手段:
- 字符串常量池
- 整數型常量池
- 線程池
- 連接池
- ……
【小結】:
????????緩存(cache)就是內存,提前把數據放到內存中,下一次用的時候,直接從緩存中拿,效率高!
二、幾種常見的緩存/池化技術
這些“池”技術,其實都是 Java 中的 緩存/復用機制,目的是:提升性能、減少資源消耗、避免頻繁創建和銷毀對象。下面來系統講解幾種常見的緩存/池化技術:
2-1、字符串常量池(String Constant Pool)
1、原理:
Java 中字符串是不可變的(final
),所以 JVM 會把相同的字符串常量只保留一份副本,存放在一個稱為 字符串常量池 的內存區域。
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // true
"hello"
是一個字符串字面量,保存在常量池中s1 和 s2 都指向同一個地址
2、注意:
String s3 = new String("hello");
System.out.println(s1 == s3); // false(堆 vs 常量池)
如果你想把 new
出來的字符串放入常量池:
String interned = s3.intern();
System.out.println(s1 == interned); // true
?
2-2、整數型常量池(Integer Cache)
1、原理:
Java 對于包裝類型 Integer,有一個緩存區([-128, 127]),當你使用 valueOf()
方法創建時,會從緩存中取對象而不是創建新對象。
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true(緩存)Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false(未緩存)
2、范圍:
JVM 默認緩存范圍為 [-128, 127]
,可以通過啟動參數修改:
-XX:AutoBoxCacheMax=300
2-3、線程池(Thread Pool)
1、原理:
線程的創建與銷毀成本高(涉及操作系統資源),頻繁創建新線程會拖慢系統。
所以,線程池把線程復用起來,讓多個任務共享固定線程,提高并發效率。
2、常用方式:
// 創建固定大小為 3 的線程池ExecutorService pool = Executors.newFixedThreadPool(3);// 模擬提交 5 個任務for (int i = 1; i <= 15; i++) {int taskId = i;pool.submit(new Runnable() {public void run() {String threadName = Thread.currentThread().getName();System.out.println("任務 " + taskId + " 開始,線程:" + threadName);try {Thread.sleep(20000); // 模擬任務耗時} catch (InterruptedException e) {e.printStackTrace();}System.out.println("任務 " + taskId + " 結束,線程:" + threadName);}});}// 關閉線程池(注意不是立刻關閉)pool.shutdown();
打印結果:?
?
可以看到線程是復用的。
Executors
提供常見線程池工廠方法:
newFixedThreadPool(n)
newCachedThreadPool()
newSingleThreadExecutor()
newScheduledThreadPool(n)
2-4、連接池(Connection Pool)
1、?原理:
數據庫連接創建代價高(要連接服務器、授權、建會話),所以使用連接池 復用已建立的連接。
2、常見實現:
HikariCP(Spring Boot 默認)
DBCP
C3P0
Druid
3、示例(Spring Boot):
spring.datasource.hikari.maximum-pool-size: 10
應用啟動后,會提前建立 10 個連接,放入連接池,供業務查詢復用。
2-5、對象池(Object Pool)
對于那些頻繁使用又比較重量級的對象(如:ByteBuffer
, Socket
, 數據庫連接
),也可以池化處理。
Java 標準庫沒有通用的 ObjectPool,但 Apache Commons Pool 提供支持。
GenericObjectPool<MyReusableObject> pool = new GenericObjectPool<>(new MyObjectFactory());MyReusableObject obj = pool.borrowObject(); // 借
obj.doSomething(); // 用
pool.returnObject(obj); // 還
你需要在項目中引入 Apache Commons Pool 的依賴(如果用 Maven):
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId><version>2.11.1</version> <!-- 可根據需要換版本 -->
</dependency>
?
2-6、內存緩存(如 LRU 緩存)
Java 中可以自己實現緩存算法(如 LRU),也可以使用:
Guava Cache
Caffeine(高性能)
Ehcache
Redis(分布式)
示例(Caffeine):
Cache<String, Object> cache = Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(10, TimeUnit.MINUTES).build();
2-7、類加載緩存(ClassLoader)
JVM 對每個類只加載一次,會將 .class
文件緩存到內存中,后續實例化只需創建對象而不重復加載。
2-8、反射緩存(Method/Field 緩存)
使用反射獲取字段/方法(如 Class.getDeclaredMethods()
)是慢操作。JVM 會自動緩存這些反射結構,Spring、MyBatis 等框架也會自己做緩存。
2-9、JVM 運行時常見緩存(底層)
緩存類型 | 說明 |
---|---|
字符串常量池 | 復用 String 常量 |
Integer 緩存 | 避免頻繁裝箱創建 Integer |
Class 常量池 | 常量值如 final 字段、枚舉等 |
方法句柄緩存 | JVM 優化調用性能 |
Lambda 表達式緩存 | 編譯后只創建一次匿名類對象 |
2-10、總結對比表
技術名稱 | 類型 | 緩存對象 | 控制方式 | 是否可配置 |
---|---|---|---|---|
字符串常量池 | 編譯期/運行期 | String | 自動/intern() | ? |
Integer 緩存 | 運行期 | Integer | 自動/valueOf() | ? |
線程池 | 并發 | Thread | Executors | ? |
連接池 | IO資源 | DB連接 | DataSource | ? |
ObjectPool | 自定義 | 業務對象 | Apache Commons | ? |
Guava/Caffeine | 本地緩存 | 任意對象 | API構建 | ? |
Redis | 分布式緩存 | 任意對象 | 客戶端控制 | ? |
三、Mybatis的緩存
mybatis 緩存包括:
- 一級緩存:將查詢到的數據存儲到 SqlSession 中。
- 二級緩存:將查詢到的數據存儲到 SqlSessionFactory 中。
- 集成其它第三方的緩存:比如 EhCache【Java 語言開發的】、Memcache【C 語言開發的】等。
SqlSessionFactory是一個數據庫一個,SqlSession作用域是當前的sql會話。
緩存只針對DQL語句,也就是說:緩存只針對select語句!
?
3-1、MyBatis 一級緩存
3-1-1、什么是一級緩存?
一級緩存是 MyBatis 的默認緩存機制,作用范圍是 一次 SqlSession 內部。簡單說:
同一個 SqlSession 中,相同的查詢語句和參數,MyBatis 會從緩存中取數據,不會再次訪問數據庫。
一級緩存mybatis默認開啟,不需要任何配置!
3-1-2、一級緩存工作流程圖
SqlSession├── 查詢語句 1(未命中緩存) → 查數據庫,緩存結果├── 查詢語句 1(再次執行) → 命中緩存,直接返回└── SqlSession.close() → 緩存銷毀
3-1-3、一級緩存使用示例
@Test
public void testFirstLevelCache() {SqlSession session = sqlSessionFactory.openSession();UserMapper mapper = session.getMapper(UserMapper.class);// 第一次查詢,去數據庫User u1 = mapper.selectById(1L);System.out.println("第一次查詢:" + u1);// 第二次查詢相同 ID,命中緩存User u2 = mapper.selectById(1L);System.out.println("第二次查詢:" + u2);System.out.println(u1 == u2); // true(同一個對象)session.close();
}
【注意】:
此時,控制臺只執行一條sql select語句!?
?
3-1-4、哪些情況會導致緩存失效?
SqlSession 不是同一個
每次
openSession()
創建新的 Session,緩存就不同。示例:
@Testpublic void testFirstCache2() throws IOException {SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));SqlSession sqlSession1 = sqlSessionFactory.openSession();CarMapper mapper1 = sqlSession1.getMapper(CarMapper.class);Car car1 = mapper1.selectOneById(1L);System.out.println(car1);SqlSession sqlSession2 = sqlSessionFactory.openSession();CarMapper mapper2 = sqlSession2.getMapper(CarMapper.class);Car car2 = mapper2.selectOneById(1L);System.out.println(car2);sqlSession1.close();sqlSession2.close();}
此時,控制臺會打印兩條sql select語句。
執行了
update
/insert
/delete
操作寫操作會清空緩存(保證數據一致性),修改任意一張表,都會清空緩存!
手動清空緩存
session.clearCache();
查詢的 SQL 或參數不同
?
3-1-5、一級緩存原理簡述
MyBatis 內部維護了一個
PerpetualCache
(HashMap 實現)每次查詢前會根據 SQL+參數,生成 key,先查緩存
如果命中,直接返回
如果沒命中,查數據庫并存入緩存
3-2、MyBatis 二級緩存
MyBatis 的二級緩存,這對優化多次查詢、減少數據庫壓力非常重要,尤其是跨 SqlSession 的查詢場景。
?
3-2-1、什么是二級緩存?
二級緩存是 MyBatis 提供的跨 SqlSession 的緩存機制。
它的作用范圍是:Mapper 映射級別(namespace),不同 SqlSession 之間共享緩存數據。
3-2-2、一級 vs 二級緩存對比
對比項 | 一級緩存(默認) | 二級緩存(需開啟) |
---|---|---|
緩存范圍 | 單個 SqlSession 內 | 多個 SqlSession 共享 |
默認狀態 | 開啟 | 默認關閉 |
生命周期 | SqlSession 生命周期 | 應用級、映射器級別 |
存儲結構 | 基于 HashMap(PerpetualCache) | 可自定義實現 |
典型用途 | 同一次操作內避免重復查詢 | 緩解高頻讀操作數據庫壓力 |
3-2-3、使用二級緩存的三步配置
Step 1:開啟全局二級緩存
<settings><setting name="cacheEnabled" value="true"/>
</settings>
【注意】:
是的,
<setting name="cacheEnabled" value="true"/>
在 MyBatis 中默認就是開啟的。?
Step 2:在 Mapper 映射文件中開啟 <cache/>
例如:UserMapper.xml
<mapper namespace="com.example.mapper.UserMapper"><cache /><select id="selectById" resultType="User">SELECT * FROM users WHERE id = #{id}</select>
</mapper>
你也可以配置更多參數(見下方擴展)
Step 3:實體類實現 Serializable
接口(因為緩存需要序列化)
public class User implements Serializable {private Long id;private String name;// ...其他字段
}
3-2-4、二級緩存使用示例
示例1:沒有使用二級緩存
示例2:數據從二級緩存中獲取
sqlSession1關閉,數據保存到二級緩存中,再執行sqlSession2中的select語句,會從二級緩存中獲取。
【注意】:
一級緩存的優先級高,先從一級緩存中取數據,若是一級緩存關閉,則從二級中取數據!
?
示例3:跨namespace測試二級緩存
【注意】:
兩個mapper不一樣,但是執行的select語句和參數都是一樣的,但是控制臺依舊會執行兩條select查詢語句,說明二級緩存不能跨namespace!
3-2-5、哪些操作會清空二級緩存?
對該
namespace
進行了update/insert/delete(增、刪、改)
顯式調用了
clearCache()
配置
<cache flushInterval="..."/>
自動過期跨 namespace 無法共享(除非手動自定義)
3-2-6、常見 <cache>
配置項
<cache eviction="LRU" <!-- 緩存淘汰策略:LRU, FIFO, SOFT, WEAK -->flushInterval="60000" <!-- 自動刷新間隔:毫秒;刷新后二級緩存失效 -->size="512" <!-- 最大緩存對象數量 -->readOnly="false" <!-- 是否只讀(只讀更快但不可修改), car1 == car2 -->blocking="true" <!-- 防止緩存擊穿 -->
/>
?
3-2-7、一級緩存 vs 二級緩存(圖解理解)
+------------------------+
| SqlSession A |
| └── 一級緩存(僅自己用) |
| |
| SqlSession B |
| └── 一級緩存(僅自己用) |
+------------------------+↓(關閉 SqlSession 后)
+------------------------+
| 二級緩存(共享) |
| key: SQL + 參數 |
| value: 查詢結果對象 |
+------------------------+
3-3、自定義緩存實現(可選)
MyBatis 允許你自定義二級緩存邏輯(如整合 Redis),也就是集成第三方的緩存組件。
【注意】:
MyBatis的一級緩存是不可替代的!集成第三方的緩存組件,替代的是二級緩存!
MyBatis 如何集成第三方緩存組件,比如 Redis、EhCache、Caffeine 等。這種方式可以將 MyBatis 的二級緩存升級為分布式或高性能緩存,實現更強的可擴展性與性能提升。?
1、示例:集成EhCache?
step1:pom.xml中添加依賴
<!-- MyBatis 對 EhCache 的支持 -->
<dependency><groupId>org.mybatis.caches</groupId><artifactId>mybatis-ehcache</artifactId><version>1.2.1</version>
</dependency>
step2:?添加 ehcache.xml
配置文件
<ehcache><cache name="com.example.mapper.UserMapper"maxEntriesLocalHeap="1000"timeToLiveSeconds="600"/>
</ehcache>
step3: 在對應的xxxMapper.xml 中配置:
step4: 編寫測試類
測試類和測試二級緩存一樣,沒有變動!