带你彻底搞懂MyBatis的底层实现之缓存模块(Cache)-吊打面试官必备技能
??基礎支持層位于MyBatis整體架構的最底層,支撐著MyBatis的核心處理層,是整個框架的基石。基礎支持層中封裝了多個較為通用的、獨立的模塊。不僅僅為MyBatis提供基礎支撐,也可以在合適的場景中直接復用。
??上篇文章我們給大家聊了下binding模塊,本篇文章我們重點來聊下緩存(Cache)模塊。
緩存模塊
??MyBatis作為一個強大的持久層框架,緩存是其必不可少的功能之一,Mybatis中的緩存分為一級緩存和二級緩存。但本質上是一樣的,都是使用Cache接口實現的。緩存位于 org.apache.ibatis.cache包下。
??通過結構我們能夠發現Cache其實使用到了裝飾器模式來實現緩存的處理。首先大家需要先回顧下裝飾器模式的相關內容哦。我們先來看看Cache中的基礎類的API
// 煎餅加雞蛋加香腸
“裝飾者模式(Decorator Pattern)是指在不改變原有對象的基礎之上,將功能附加到對象上,提供了比繼承更有彈性的替代方案(擴展原有對象的功能)。”
1 Cache接口
??Cache接口是緩存模塊中最核心的接口,它定義了所有緩存的基本行為,Cache接口的定義如下:
public interface Cache {/*** 緩存對象的 ID* @return The identifier of this cache*/String getId();/*** 向緩存中添加數據,一般情況下 key是CacheKey value是查詢結果* @param key Can be any object but usually it is a {@link CacheKey}* @param value The result of a select.*/void putObject(Object key, Object value);/*** 根據指定的key,在緩存中查找對應的結果對象* @param key The key* @return The object stored in the cache.*/Object getObject(Object key);/*** As of 3.3.0 this method is only called during a rollback* for any previous value that was missing in the cache.* This lets any blocking cache to release the lock that* may have previously put on the key.* A blocking cache puts a lock when a value is null* and releases it when the value is back again.* This way other threads will wait for the value to be* available instead of hitting the database.* 刪除key對應的緩存數據** @param key The key* @return Not used*/Object removeObject(Object key);/*** Clears this cache instance.* 清空緩存*/void clear();/*** Optional. This method is not called by the core.* 緩存的個數。* @return The number of elements stored in the cache (not its capacity).*/int getSize();/*** Optional. As of 3.2.6 this method is no longer called by the core.* <p>* Any locking needed by the cache must be provided internally by the cache provider.* 獲取讀寫鎖* @return A ReadWriteLock*/default ReadWriteLock getReadWriteLock() {return null;}}??Cache接口的實現類很多,但是大部分都是裝飾器,只有PerpetualCache提供了Cache接口的基本實現。
2 PerpetualCache
??PerpetualCache在緩存模塊中扮演了ConcreteComponent的角色,其實現比較簡單,底層使用HashMap記錄緩存項,具體的實現如下:
/*** 在裝飾器模式用 用來被裝飾的對象* 緩存中的 基本緩存處理的實現* 其實就是一個 HashMap 的基本操作* @author Clinton Begin*/ public class PerpetualCache implements Cache {private final String id; // Cache 對象的唯一標識// 用于記錄緩存的Map對象private final Map<Object, Object> cache = new HashMap<>();public PerpetualCache(String id) {this.id = id;}@Overridepublic String getId() {return id;}@Overridepublic int getSize() {return cache.size();}@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();}@Overridepublic boolean equals(Object o) {if (getId() == null) {throw new CacheException("Cache instances require an ID.");}if (this == o) {return true;}if (!(o instanceof Cache)) {return false;}Cache otherCache = (Cache) o;// 只關心IDreturn getId().equals(otherCache.getId());}@Overridepublic int hashCode() {if (getId() == null) {throw new CacheException("Cache instances require an ID.");}// 只關心IDreturn getId().hashCode();}}??然后我們可以來看看cache.decorators包下提供的裝飾器。他們都實現了Cache接口。這些裝飾器都在PerpetualCache的基礎上提供了一些額外的功能,通過多個組合實現一些特殊的需求。
3 BlockingCache
??通過名稱我們能看出來是一個阻塞同步的緩存,它保證只有一個線程到緩存中查找指定的key對應的數據。
public class BlockingCache implements Cache {private long timeout; // 阻塞超時時長private final Cache delegate; // 被裝飾的底層 Cache 對象// 每個key 都有對象的 ReentrantLock 對象private final ConcurrentHashMap<Object, ReentrantLock> locks;public BlockingCache(Cache delegate) {// 被裝飾的 Cache 對象this.delegate = delegate;this.locks = new ConcurrentHashMap<>();}@Overridepublic String getId() {return delegate.getId();}@Overridepublic int getSize() {return delegate.getSize();}@Overridepublic void putObject(Object key, Object value) {try {// 執行 被裝飾的 Cache 中的方法delegate.putObject(key, value);} finally {// 釋放鎖releaseLock(key);}}@Overridepublic Object getObject(Object key) {acquireLock(key); // 獲取鎖Object value = delegate.getObject(key); // 獲取緩存數據if (value != null) { // 有數據就釋放掉鎖,否則繼續持有鎖releaseLock(key);}return value;}@Overridepublic Object removeObject(Object key) {// despite of its name, this method is called only to release locksreleaseLock(key);return null;}@Overridepublic void clear() {delegate.clear();}private ReentrantLock getLockForKey(Object key) {return locks.computeIfAbsent(key, k -> new ReentrantLock());}private void acquireLock(Object key) {Lock lock = getLockForKey(key);if (timeout > 0) {try {boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);if (!acquired) {throw new CacheException("Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId());}} catch (InterruptedException e) {throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);}} else {lock.lock();}}private void releaseLock(Object key) {ReentrantLock lock = locks.get(key);if (lock.isHeldByCurrentThread()) {lock.unlock();}}public long getTimeout() {return timeout;}public void setTimeout(long timeout) {this.timeout = timeout;} }??通過源碼我們能夠發現,BlockingCache本質上就是在我們操作緩存數據的前后通過 ReentrantLock對象來實現了加鎖和解鎖操作。其他的具體實現類,大家可以自行查閱
| 基本緩存 | 緩存基本實現類 | 默認是PerpetualCache,也可以自定義比如RedisCache、EhCache等,具備基本功能的緩存類 | 無 |
| LruCache | LRU策略的緩存 | 當緩存到達上限時候,刪除最近最少使用的緩存(Least Recently Use) | eviction=“LRU”(默認) |
| FifoCache | FIFO策略的緩存 | 當緩存到達上限時候,刪除最先入隊的緩存 | eviction=“FIFO” |
| SoftCacheWeakCache | 帶清理策略的緩存 | 通過JVM的軟引用和弱引用來實現緩存,當JVM內存不足時,會自動清理掉這些緩存,基于SoftReference和WeakReference | eviction="SOFT"eviction=“WEAK” |
| LoggingCache | 帶日志功能的緩存 | 比如:輸出緩存命中率 | 基本 |
| SynchronizedCache | 同步緩存 | 基于synchronized關鍵字實現,解決并發問題 | 基本 |
| BlockingCache | 阻塞緩存 | 通過在get/put方式中加鎖,保證只有一個線程操作緩存,基于Java重入鎖實現 | blocking=true |
| SerializedCache | 支持序列化的緩存 | 將對象序列化以后存到緩存中,取出時反序列化 | readOnly=false(默認) |
| ScheduledCache | 定時調度的緩存 | 在進行get/put/remove/getSize等操作前,判斷緩存時間是否超過了設置的最長緩存時間(默認是一小時),如果是則清空緩存–即每隔一段時間清空一次緩存 | flushInterval不為空 |
| TransactionalCache | 事務緩存 | 在二級緩存中使用,可一次存入多個緩存,移除多個緩存 | 在TransactionalCacheManager中用Map維護對應關系 |
4 緩存的應用
4.1 緩存對應的初始化
??在Configuration初始化的時候會為我們的各種Cache實現注冊對應的別名
在解析settings標簽的時候,設置的默認值有如下
cacheEnabled默認為true,localCacheScope默認為 SESSION
在解析映射文件的時候會解析我們相關的cache標簽
然后解析映射文件的cache標簽后會在Configuration對象中添加對應的數據在
private void cacheElement(XNode context) {// 只有 cache 標簽不為空才解析if (context != null) {String type = context.getStringAttribute("type", "PERPETUAL");Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);String eviction = context.getStringAttribute("eviction", "LRU");Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);Long flushInterval = context.getLongAttribute("flushInterval");Integer size = context.getIntAttribute("size");boolean readWrite = !context.getBooleanAttribute("readOnly", false);boolean blocking = context.getBooleanAttribute("blocking", false);Properties props = context.getChildrenAsProperties();builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);}}繼續
然后我們可以發現 如果存儲 cache 標簽,那么對應的 Cache對象會被保存在 currentCache 屬性中。
進而在 Cache 對象 保存在了 MapperStatement 對象的 cache 屬性中。
然后我們再看看openSession的時候又做了哪些操作,在創建對應的執行器的時候會有緩存的操作
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {executorType = executorType == null ? defaultExecutorType : executorType;executorType = executorType == null ? ExecutorType.SIMPLE : executorType;Executor executor;if (ExecutorType.BATCH == executorType) {executor = new BatchExecutor(this, transaction);} else if (ExecutorType.REUSE == executorType) {executor = new ReuseExecutor(this, transaction);} else {// 默認 SimpleExecutorexecutor = new SimpleExecutor(this, transaction);}// 二級緩存開關,settings 中的 cacheEnabled 默認是 trueif (cacheEnabled) {executor = new CachingExecutor(executor);}// 植入插件的邏輯,至此,四大對象已經全部攔截完畢executor = (Executor) interceptorChain.pluginAll(executor);return executor;}??也就是如果 cacheEnabled 為 true 就會通過 CachingExecutor 來裝飾executor 對象,然后就是在執行SQL操作的時候會涉及到緩存的具體使用。這個就分為一級緩存和二級緩存,這個我們來分別介紹
4.2 一級緩存
??一級緩存也叫本地緩存(Local Cache),MyBatis的一級緩存是在會話(SqlSession)層面進行緩存的。MyBatis的一級緩存是默認開啟的,不需要任何的配置(如果要關閉,localCacheScope設置為STATEMENT)。在BaseExecutor對象的query方法中有關閉一級緩存的邏輯
??然后我們需要考慮下在一級緩存中的 PerpetualCache 對象在哪創建的,因為一級緩存是Session級別的緩存,肯定需要在Session范圍內創建,其實PerpetualCache的實例化是在BaseExecutor的構造方法中創建的
??一級緩存的具體實現也是在BaseExecutor的query方法中來實現的
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {// 異常體系之 ErrorContextErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());if (closed) {throw new ExecutorException("Executor was closed.");}if (queryStack == 0 && ms.isFlushCacheRequired()) {// flushCache="true"時,即使是查詢,也清空一級緩存clearLocalCache();}List<E> list;try {// 防止遞歸查詢重復處理緩存queryStack++;// 查詢一級緩存// ResultHandler 和 ResultSetHandler的區別list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list != null) {handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {// 真正的查詢流程list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}} finally {queryStack--;}if (queryStack == 0) {for (DeferredLoad deferredLoad : deferredLoads) {deferredLoad.load();}// issue #601deferredLoads.clear();if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {// issue #482clearLocalCache();}}return list;}一級緩存的驗證:
同一個Session中的多個相同操作
@Testpublic void test1() throws Exception{// 1.獲取配置文件InputStream in = Resources.getResourceAsStream("mybatis-config.xml");// 2.加載解析配置文件并獲取SqlSessionFactory對象SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);// 3.根據SqlSessionFactory對象獲取SqlSession對象SqlSession sqlSession = factory.openSession();// 4.通過SqlSession中提供的 API方法來操作數據庫List<User> list = sqlSession.selectList("com.gupaoedu.mapper.UserMapper.selectUserList");System.out.println(list.size());// 一級緩存測試System.out.println("---------");list = sqlSession.selectList("com.gupaoedu.mapper.UserMapper.selectUserList");System.out.println(list.size());// 5.關閉會話sqlSession.close();}輸出日志
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@477b4cdf] ==> Preparing: select * from t_user ==> Parameters: <== Columns: id, user_name, real_name, password, age, d_id <== Row: 1, zhangsan, 張三, 123456, 18, null <== Row: 2, lisi, 李四, 11111, 19, null <== Row: 3, wangwu, 王五, 111, 22, 1001 <== Row: 4, wangwu, 王五, 111, 22, 1001 <== Row: 5, wangwu, 王五, 111, 22, 1001 <== Row: 6, wangwu, 王五, 111, 22, 1001 <== Row: 7, wangwu, 王五, 111, 22, 1001 <== Row: 8, aaa, bbbb, null, null, null <== Row: 9, aaa, bbbb, null, null, null <== Row: 10, aaa, bbbb, null, null, null <== Row: 11, aaa, bbbb, null, null, null <== Row: 12, aaa, bbbb, null, null, null <== Row: 666, hibernate, 持久層框架, null, null, null <== Total: 13 13 --------- 13可以看到第二次查詢沒有經過數據庫操作
不同Session的相同操作
@Testpublic void test2() throws Exception{// 1.獲取配置文件InputStream in = Resources.getResourceAsStream("mybatis-config.xml");// 2.加載解析配置文件并獲取SqlSessionFactory對象SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);// 3.根據SqlSessionFactory對象獲取SqlSession對象SqlSession sqlSession = factory.openSession();// 4.通過SqlSession中提供的 API方法來操作數據庫List<User> list = sqlSession.selectList("com.gupaoedu.mapper.UserMapper.selectUserList");System.out.println(list.size());sqlSession.close();sqlSession = factory.openSession();// 一級緩存測試System.out.println("---------");list = sqlSession.selectList("com.gupaoedu.mapper.UserMapper.selectUserList");System.out.println(list.size());// 5.關閉會話sqlSession.close();}輸出結果
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@477b4cdf] ==> Preparing: select * from t_user ==> Parameters: <== Columns: id, user_name, real_name, password, age, d_id <== Row: 1, zhangsan, 張三, 123456, 18, null <== Row: 2, lisi, 李四, 11111, 19, null <== Row: 3, wangwu, 王五, 111, 22, 1001 <== Row: 4, wangwu, 王五, 111, 22, 1001 <== Row: 5, wangwu, 王五, 111, 22, 1001 <== Row: 6, wangwu, 王五, 111, 22, 1001 <== Row: 7, wangwu, 王五, 111, 22, 1001 <== Row: 8, aaa, bbbb, null, null, null <== Row: 9, aaa, bbbb, null, null, null <== Row: 10, aaa, bbbb, null, null, null <== Row: 11, aaa, bbbb, null, null, null <== Row: 12, aaa, bbbb, null, null, null <== Row: 666, hibernate, 持久層框架, null, null, null <== Total: 13 13 Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@477b4cdf] Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@477b4cdf] Returned connection 1199262943 to pool. --------- Opening JDBC Connection Checked out connection 1199262943 from pool. Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@477b4cdf] ==> Preparing: select * from t_user ==> Parameters: <== Columns: id, user_name, real_name, password, age, d_id <== Row: 1, zhangsan, 張三, 123456, 18, null <== Row: 2, lisi, 李四, 11111, 19, null <== Row: 3, wangwu, 王五, 111, 22, 1001 <== Row: 4, wangwu, 王五, 111, 22, 1001 <== Row: 5, wangwu, 王五, 111, 22, 1001 <== Row: 6, wangwu, 王五, 111, 22, 1001 <== Row: 7, wangwu, 王五, 111, 22, 1001 <== Row: 8, aaa, bbbb, null, null, null <== Row: 9, aaa, bbbb, null, null, null <== Row: 10, aaa, bbbb, null, null, null <== Row: 11, aaa, bbbb, null, null, null <== Row: 12, aaa, bbbb, null, null, null <== Row: 666, hibernate, 持久層框架, null, null, null <== Total: 13 13 Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@477b4cdf] Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@477b4cdf] Returned connection 1199262943 to pool.通過輸出我們能夠發現,不同的Session中的相同操作,一級緩存是沒有起作用的。
4.3 二級緩存
??二級緩存是用來解決一級緩存不能跨會話共享的問題的,范圍是namespace級別的,可以被多個SqlSession共享(只要是同一個接口里面的相同方法,都可以共享),生命周期和應用同步。
??二級緩存的設置,首先是settings中的cacheEnabled要設置為true,當然默認的就是為true,這個步驟決定了在創建Executor對象的時候是否通過CachingExecutor來裝飾。
??那么設置了cacheEnabled標簽為true是否就意味著 二級緩存是否一定可用呢?當然不是,我們還需要在 對應的映射文件中添加 cache 標簽才行。
<!-- 聲明這個namespace使用二級緩存 --> <cache type="org.apache.ibatis.cache.impl.PerpetualCache"size="1024" <!—最多緩存對象個數,默認1024-->eviction="LRU" <!—回收策略-->flushInterval="120000" <!—自動刷新時間 ms,未配置時只有調用時刷新-->readOnly="false"/> <!—默認是false(安全),改為true可讀寫時,對象必須支持序列化 -->cache屬性詳解:
| type | 緩存實現類 | 需要實現Cache接口,默認是PerpetualCache,可以使用第三方緩存 |
| size | 最多緩存對象個數 | 默認1024 |
| eviction | 回收策略(緩存淘汰算法) | LRU – 最近最少使用的:移除最長時間不被使用的對象(默認)。FIFO – 先進先出:按對象進入緩存的順序來移除它們。SOFT – 軟引用:移除基于垃圾回收器狀態和軟引用規則的對象。WEAK – 弱引用:更積極地移除基于垃圾收集器狀態和弱引用規則的對象。 |
| flushInterval | 定時自動清空緩存間隔 | 自動刷新時間,單位 ms,未配置時只有調用時刷新 |
| readOnly | 是否只讀 | true:只讀緩存;會給所有調用者返回緩存對象的相同實例。因此這些對象不能被修改。這提供了很重要的性能優勢。false:讀寫緩存;會返回緩存對象的拷貝(通過序列化),不會共享。這會慢一些,但是安全,因此默認是 false。改為false可讀寫時,對象必須支持序列化。 |
| blocking | 啟用阻塞緩存 | 通過在get/put方式中加鎖,保證只有一個線程操作緩存,基于Java重入鎖實現 |
再來看下cache標簽在源碼中的體現,創建cacheKey
@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {// 獲取SQLBoundSql boundSql = ms.getBoundSql(parameterObject);// 創建CacheKey:什么樣的SQL是同一條SQL? >>CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}createCacheKey自行進去查看
而這看到的和我們前面在緩存初始化時看到的 cache 標簽解析操作是對應上的。所以我們要開啟二級緩存兩個條件都要滿足。
??這樣的設置表示當前的映射文件中的相關查詢操作都會觸發二級緩存,但如果某些個別方法我們不希望走二級緩存怎么辦呢?我們可以在標簽中添加一個 useCache=false 來實現的設置不使用二級緩存
還有就是當我們執行的對應的DML操作,在MyBatis中會清空對應的二級緩存和一級緩存。
在解析映射文件的時候DML操作flushCacheRequired為true
4.4 第三方緩存
???在實際開發的時候我們一般也很少使用MyBatis自帶的二級緩存,這時我們會使用第三方的緩存工具Ehcache獲取Redis來實現,那么他們是如何來實現的呢?
https://github.com/mybatis/redis-cache
添加依賴
<dependency><groupId>org.mybatis.caches</groupId><artifactId>mybatis-redis</artifactId><version>1.0.0-beta2</version> </dependency>然后加上Cache標簽的配置
<cache type="org.mybatis.caches.redis.RedisCache"eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>然后添加redis的屬性文件
host=192.168.100.120 port=6379 connectionTimeout=5000 soTimeout=5000 database=0測試效果
數據存儲到了Redis中
??然后大家也可以自行分析下第三方的Cache是如何替換掉PerpetualCache的,因為PerpetualCache是基于HashMap處理的,而RedisCache是基于Redis來存儲緩存數據的。
提示
緩存模塊我們就介紹到此。 然后大家可以基于我們上面所介紹的基礎支持層,再系統的來梳理下核心處理層的流程
~~ 好了,緩存模塊的內容就給大家介紹到這里,如果對你有幫助,歡迎點贊關注加收藏
下篇我們介紹 MyBatis中的插件機制,敬請期待 V_V
總結
以上是生活随笔為你收集整理的带你彻底搞懂MyBatis的底层实现之缓存模块(Cache)-吊打面试官必备技能的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++之动态联编
- 下一篇: linux下批量改文件名命令,Linux