MyBatis 緩存機制源碼深度解析:一級緩存與二級緩存
- 一、一級緩存
- 1.1 邏輯位置與核心源碼解析
- 1.2 一級緩存容器:PerpetualCache
- 1.3 createCacheKey 方法與緩存命中
- 1.4 命中與失效時機
- 1.5 使用方式
- 二、二級緩存
- 2.1 邏輯位置與核心源碼解析
- 2.2 查詢流程、命中與失效時機
- 2.3 使用方式
- 三、一級緩存與二級緩存的差異
在 Java
開發領域,MyBatis
作為主流的持久層框架,其緩存機制是提升系統性能的關鍵利器。MyBatis
提供的一級緩存和二級緩存,通過不同的策略與實現,有效減少數據庫訪問次數。
本文基于 MyBatis
3.4.6 版本,結合關鍵源碼,深入解析兩種緩存的原理、使用及差異。
一、一級緩存
1.1 邏輯位置與核心源碼解析
一級緩存又稱會話級緩存,其核心邏輯主要存在于org.apache.ibatis.executor.BaseExecutor
類中。BaseExecutor
是 MyBatis
執行器的基礎抽象類,負責管理一級緩存相關操作。最外層執行的query
方法,包含了緩存的核心邏輯,而doQuery
方法則是具體的數據庫查詢操作,由BaseExecutor
的子類(如SimpleExecutor
、ReuseExecutor
等)實現。
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameterObject);// 構造緩存keyCacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);// 執行查詢邏輯return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());// ...List<E> list;try {queryStack++;// 優先從一級緩存(localCache)中獲取結果list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list != null) {// 處理存儲過程的輸出參數緩存handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {// 一級緩存未命中,執行數據庫查詢(queryFromDatabase有具體的數據庫查詢邏輯)list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}} finally {queryStack--;}// ...return list;
}
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {List<E> list;// 在本地緩存中標記查詢執行中,防止循環引用導致的無限遞歸localCache.putObject(key, EXECUTION_PLACEHOLDER);try {// 執行實際的數據庫查詢操作list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);} finally {localCache.removeObject(key);}// 將查詢結果存入一級緩存localCache.putObject(key, list);if (ms.getStatementType() == StatementType.CALLABLE) {localOutputParameterCache.putObject(key, parameter);}return list;
}
上述代碼中,query
方法先判斷緩存相關條件,嘗試從localCache
獲取數據。
若緩存未命中,則調用queryFromDatabase
方法執行數據庫查詢,并將結果存入一級緩存。
1.2 一級緩存容器:PerpetualCache
一級緩存的數據存儲在org.apache.ibatis.cache.PerpetualCache
類實例中。PerpetualCache
是一個基于 HashMap
實現的簡單緩存類,用于存儲緩存數據。
public class PerpetualCache implements Cache {private final String id;// 使用HashMap存儲緩存數據,key為緩存鍵,value為緩存值private Map<Object, Object> cache = new HashMap<Object, Object>();public PerpetualCache(String id) {this.id = id;}@Overridepublic void putObject(Object key, Object value) {cache.put(key, value);}@Overridepublic Object getObject(Object key) {return cache.get(key);}@Overridepublic Object removeObject(Object key) {return cache.remove(key);}@Overridepublic void clear() {cache.clear();}// ...
}
PerpetualCache
通過putObject
、getObject
等方法實現對緩存數據的操作,BaseExecutor
通過操作該實例來管理一級緩存。
1.3 createCacheKey 方法與緩存命中
CacheKey
是緩存的唯一標識,MyBatis
通過createCacheKey
方法生成CacheKey
對象。該方法位于org.apache.ibatis.executor.BaseExecutor
類,其核心邏輯是將 SQL
語句、參數、RowBounds
等信息進行哈希計算,生成唯一的緩存鍵。
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {if (closed) {throw new ExecutorException("Executor was closed.");}CacheKey cacheKey = new CacheKey();// 設置Mapper語句的唯一標識cacheKey.update(ms.getId());// 添加分頁查詢的偏移量cacheKey.update(rowBounds.getOffset());// 添加分頁查詢的限制數量cacheKey.update(rowBounds.getLimit());// 添加SQL語句本身cacheKey.update(boundSql.getSql());List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();// mimic DefaultParameterHandler logicfor (ParameterMapping parameterMapping : parameterMappings) {if (parameterMapping.getMode() != ParameterMode.OUT) {Object value;String propertyName = parameterMapping.getProperty();// 優先從SQL綁定參數中獲取值if (boundSql.hasAdditionalParameter(propertyName)) {value = boundSql.getAdditionalParameter(propertyName);} else if (parameterObject == null) {value = null;} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {value = parameterObject;} else {// 通過反射獲取參數對象對應屬性的值MetaObject metaObject = configuration.newMetaObject(parameterObject);value = metaObject.getValue(propertyName);}// 將參數值添加到CacheKey中cacheKey.update(value);}}if (configuration.getEnvironment() != null) {// 添加環境ID到CacheKey中cacheKey.update(configuration.getEnvironment().getId());}return cacheKey;
}
當再次執行相同 SQL
查詢時,若生成的CacheKey
與緩存中已存在的CacheKey
一致,且在同一個SqlSession
內,就會觸發一級緩存命中,直接從緩存獲取數據。
1.4 命中與失效時機
-
命中時機:在同一個
SqlSession
中,執行的SQL
語句、輸入參數完全相同(即生成的CacheKey
相同)時,一級緩存命中。 -
失效時機:
SqlSession
關閉,或執行insert
、update
、delete
操作時,一級緩存會失效。以update
操作的源碼為例,在org.apache.ibatis.executor.BaseExecutor
類的update
方法中:public int update(MappedStatement ms, Object parameter) throws SQLException {ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());if (closed) {throw new ExecutorException("Executor was closed.");}// 調用該方法清空一級緩存clearLocalCache();return doUpdate(ms, parameter); }
執行
update
操作時,會調用clearLocalCache
方法清空當前SqlSession
的一級緩存,保證數據一致性。insert
和delete
操作也有類似邏輯。
1.5 使用方式
一級緩存默認開啟,無需額外配置。在同一個SqlSession
中,MyBatis
會自動管理緩存的讀寫與失效。以下是一個簡單的 demo 案例代碼:
public class CacheTest {private SqlSessionFactory sessionFactory;/*** 加載配置文件。并且初始化SqlSessionFactory*/@Beforepublic void before() throws IOException {InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);}@Testpublic void testGetById() {SqlSession sqlSession = sessionFactory.openSession();UserMapper userMapper = sqlSession.getMapper(UserMapper.class);// 第一次查詢,會執行數據庫查詢User user1 = userMapper.getUserById(6);// 第二次查詢,由于在同一個SqlSession且查詢條件相同,會命中一級緩存User user2 = userMapper.getUserById(6);System.out.println(user1 == user2); // 輸出true}
}
二、二級緩存
2.1 邏輯位置與核心源碼解析
MyBatis
執行SQL
時,整體流程是先經過CachingExecutor
(最外層),最后才是其他Executor
(BaseExecutor
子類)CachingExecutor
作為二級緩存的核心執行者,采用適配器模式,內部持有一個Executor對象(delegate
),該delegate
由具體的子類執行器(如SimpleExecutor
)實例化,負責執行具體的數據庫操作*MyBatis
默認未開啟二級緩存,需要在配置文件和映射文件中進行配置才能啟用。二級緩存又稱全局緩存,其核心邏輯存在于org.apache.ibatis.executor.CachingExecutor
類
public class CachingExecutor implements Executor {private final Executor delegate;private final TransactionalCacheManager tcm = new TransactionalCacheManager();public CachingExecutor(Executor delegate) {this.delegate = delegate;delegate.setExecutorWrapper(this);}@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {Cache cache = ms.getCache();// 判斷二級緩存是否開啟且可用if (cache != null) {// 處理可刷新緩存的情況,如執行增刪改操作后需要刷新緩存flushCacheIfRequired(ms);if (ms.isUseCache() && resultHandler == null) {ensureNoOutParams(ms, boundSql);// 優先從二級緩存中獲取結果List<E> list = (List<E>) tcm.getObject(cache, key);if (list!= null) {return list;}}}// 二級緩存未命中,委托給具體的執行器執行查詢return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}// 省略其他方法...
}
在CachingExecutor
的query
方法中,首先通過ms.getCache()
獲取當前 Mapper
語句對應的緩存對象,判斷二級緩存是否開啟。
若開啟且可用,則嘗試從二級緩存獲取數據。
若未命中,則將查詢委托給delegate
,由delegate
(如SimpleExecutor
)調用BaseExecutor
的相關方法執行具體數據庫查詢操作,并在事務提交后將結果寫入二級緩存。
2.2 查詢流程、命中與失效時機
-
查詢流程:
MyBatis
先在二級緩存中查找CacheKey
對應的結果,若未命中,再檢查一級緩存,若一級緩存也未命中,則執行數據庫查詢,查詢結果先存入一級緩存,事務提交后存入二級緩存。? -
命中時機:
namespace
相同,執行的SQL
語句和輸入參數相同(CacheKey
相同),且事務已提交,數據已寫入二級緩存時,二級緩存命中。? -
失效時機:執行
insert
、update
、delete
操作,或手動調用方法,會清空當前namespace
下的二級緩存。以update
操作為例,在CachingExecutor
類的update
方法中,通過flushCacheIfRequired
方法處理緩存刷新public int update(MappedStatement ms, Object parameterObject) throws SQLException {flushCacheIfRequired(ms);return delegate.update(ms, parameterObject); } private void flushCacheIfRequired(MappedStatement ms) {Cache cache = ms.getCache();if (cache!= null && ms.isFlushCacheRequired()) {tcm.clear(cache);} }
當檢測到當前
Mapper
語句配置了需要刷新緩存(ms.isFlushCacheRequired()
為true
),就會通過TransactionalCacheManager
的clear
方法清空對應的二級緩存,實現緩存失效,保證數據一致性。insert
和delete
操作也會觸發類似的緩存清空邏輯 。
2.3 使用方式
-
在
MyBatis
的配置文件中開啟二級緩存:<configuration><settings><setting name="cacheEnabled" value="true"/></settings> </configuration>
-
在
Mapper
映射文件中添加<cache>
標簽啟用二級緩存,并可配置緩存屬性:<mapper namespace="com.coderzpw.dao.UserMapper"><cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/><select id="getUserById" resultType="com.coderzpw.domain.User">SELECT * FROM user WHERE id = #{id}</select> </mapper>
-
編寫測試代碼驗證二級緩存效果:
User user1 = null; User user2 = null; try (SqlSession sqlSession1 = sessionFactory.openSession()) {UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);user1 = userMapper1.getUserById(6);sqlSession1.commit(); } try (SqlSession sqlSession2 = sessionFactory.openSession()) {UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);user2 = userMapper2.getUserById(6);sqlSession2.commit(); } System.out.println(user1 == user2); // 如果readOnly為true,這里會輸出true
三、一級緩存與二級緩存的差異
對比項 | 一級緩存 | 二級緩存 |
---|---|---|
作用范圍 | SqlSession 級別,會話內有效,僅在當前SqlSession 中共享 | namespace 級別,全局有效,可在多個SqlSession 間共享 |
開啟方式 | 默認開啟,無需配置 | 需要在配置文件和映射文件中配置開啟 |
失效機制 | SqlSession 關閉或執行增刪改操作時,通過調用clearLocalCache 清空緩存 | 執行增刪改操作或手動調用方法,通過TransactionalCacheManager 清空對應namespace 下的緩存 |
實現核心類 | BaseExecutor 、PerpetualCache | CachingExecutor 、TransactionalCacheManager |
數據存儲位置 | 存儲在SqlSession 對應的localCache 中 | 存儲在namespace 對應的緩存區域,由TransactionalCacheManager 管理 |
深入理解 MyBatis
一級緩存和二級緩存的原理與使用,有助于開發者根據業務場景靈活配置緩存策略,在提升系統性能的同時,確保數據的一致性與準確性。