java架構之路-(源碼)mybatis的一二級快取問題
- 2019 年 10 月 5 日
- 筆記
上次部落格我們說了mybatis的基本使用,我們還捎帶提到一下Mapper.xml中的select標籤的useCache屬性,這個就是設置是否存入二級快取的。
回到我們正題,經常使用mybatis的小夥伴都知道,我們的mybatis是有兩級快取的,一級快取默認開啟,我們先來一下一級快取吧,超級簡單。 一級快取:
我們還拿上次的源碼來說
package mybatis; import mybatis.bean.StudentBean; import mybatis.dao.StudentMapper; 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.Before; import org.junit.Test; import java.io.IOException; import java.io.InputStream; public class Test1 { public SqlSession session; public SqlSessionFactory sqlSessionFactory; @Before public void init() throws IOException { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); session = sqlSessionFactory.openSession(); } @Test public void studentTest(){ StudentMapper mapper = session.getMapper(StudentMapper.class); StudentBean result = mapper.selectUser(1);//這句執行了sql,也就是說,這句給一級快取塞了值 StudentBean result2 = mapper.selectUser(1);//這句執行了sql,也就是說,這句給一級快取塞了值 System.out.println(result==result2); } }
我們可以看到列印結果為true,說明了命中了我們的一級快取。
一級快取的限制比較多,需要在同一個session,同一個會話,同一個方法(statement),內執行完全相同的sql,才能保證快取的成功。
我們打開Mybatis里的BaseExecutor類我們找到152行程式碼。
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
這個就是我們的一級快取localCache。清空方法是在BaseExcutor的116行clearLocalCache,來清空我們的一級快取的,所以說執行update以後一級快取會被清空,後面有機會我會告訴大家我是怎麼找到的,只要記住一級快取默認開啟,是sqlSession級別的,幾乎是沒有生命的。然後記住什麼情況下可以用,什麼情況下不可以用,初級面試應該可以應付。
二級快取:
二級快取需要手動設置,只要在我們的配置文件內加入Cache標籤就可以了。或者加入@Cache註解也是ok的,二級快取是在session關閉時才寫入的。為什麼這樣設計呢?我們來假想一下,我們開啟session,做了一個insert寫入,這時還沒有提交,然後我們進行了查詢,如果這時寫入快取,然後我們將insert進行回滾,那麼我們的快取就多了我們剛才寫入的數據,這樣的設計是顯然不合理的,我們先來看一下二級快取是怎麼設置的。 誰說查詢時候先查二級快取,二級快取沒有再查一級快取的,一律打死,一級快取作用在session會話範圍,你二級快取的存入條件是session關閉,session都關閉了,還有毛線一級快取了….
還是上次的程式碼:我們來回顧一下。
package mybatis; import mybatis.bean.StudentBean; import mybatis.dao.StudentMapper; 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.Before; import org.junit.Test; import java.io.IOException; import java.io.InputStream; public class Test1 { public SqlSession session; public SqlSessionFactory sqlSessionFactory; @Before public void init() throws IOException { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); session = sqlSessionFactory.openSession(); } @Test public void studentTest(){ StudentMapper mapper = session.getMapper(StudentMapper.class); StudentBean result = mapper.selectUser(1); session.close(); session = sqlSessionFactory.openSession(); StudentMapper mapper2 = session.getMapper(StudentMapper.class); StudentBean result2 = mapper2.selectUser(1); System.out.println(result == result2); } }
<?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="mybatis.dao.StudentMapper"> <cache></cache> <select id="selectUser" resultType="mybatis.bean.StudentBean"> select * from student t where t.id = #{id} </select> </mapper>
我們只需要加入cache標籤即可以使用我們的二級快取。select標籤內有一個useCache屬性設置成false就是說,這個sql不寫入我們的快取。需要注意的是要給予我們的實體Bean序列化,正因為序列化,我們的輸入結果是false,說明並不是一個對象的。後面我會解釋為什麼需要做一個序列化,可以帶著問題繼續閱讀。

註解方式這樣寫就ok了。
package mybatis.dao; import mybatis.bean.StudentBean; import org.apache.ibatis.annotations.CacheNamespace; import org.apache.ibatis.annotations.Select; @CacheNamespace public interface StudentMapper { @Select("select * from student t where t.id = #{id}") StudentBean selectUser(int id); }
二級快取適用範圍:
1,必須是session提交以後,二級快取才寫入。
2,必須是同一個命名空間之下。
3,必須是相同的sql和參數。
4,如果是readWrite=true,實體類必須序列化
@CacheNamespace(readWrite = false)
這也就是我們說的為什麼需要實例化,其實也可以不序列化的。但是我們要是改了其中一個數據,另外一個拿到的數據一定是修改後的,沒有特殊需求最好是做一個序列化,不要寫readWrite=false的設置,不寫readWrite=false會提高一點點性能,但是自我覺得沒必要冒那種風險。拿著這段程式碼自己測試一下,不帶序列化的深拷貝對象會造成的結果。
package mybatis; import mybatis.bean.StudentBean; import mybatis.dao.StudentMapper; 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.Before; import org.junit.Test; import java.io.IOException; import java.io.InputStream; public class Test1 { public SqlSession session; public SqlSessionFactory sqlSessionFactory; @Before public void init() throws IOException { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); session = sqlSessionFactory.openSession(); } @Test public void studentTest(){ StudentMapper mapper = session.getMapper(StudentMapper.class); StudentBean result = mapper.selectUser(1); System.out.println(result); result.setId(2222); session.commit(); session = sqlSessionFactory.openSession(); StudentMapper mapper2 = session.getMapper(StudentMapper.class); StudentBean result2 = mapper2.selectUser(1); System.out.println(result2); System.out.println(result == result2); } }
5,必須是相同的statement相同的方法。
內部還可以加很多屬性的。
@CacheNamespace( implementation = PerpetualCache.class, // 快取實現 Cache介面 實現類 eviction = LruCache.class,// 快取演算法 flushInterval = 60000, // 刷新間隔時間 毫秒 size = 1024, // 最大快取引用對象 readWrite = true, // 是否可寫 blocking = false // 是否阻塞,防止快取擊穿的。 )
我們來簡單的深入一下二級快取的源碼,我們在Mybatis的包里會看到這樣一個文件,一個叫Cache的文件,也就是我們的快取文件。

而且我們發現很多叫***Cahe的類都實現了他

TransactionalCache注釋里明顯的寫到The 2nd level cache transactional buffer.二級快取事務緩衝區。那麼我們把斷點打在他的get和put方法上,(可能是一個錯誤的示範,我會一步步告訴你們錯了怎麼改)
斷點進到了getObject方法,我們點擊開右邊的參數欄,點擊this,我們會看到我們的delegate參數,寫著什麼什麼cache,再次點擊還會發現什麼什麼Cache,直到不能向下點擊為止

我們發現貌似實際存儲的貌似是PerpetualCache,我們發現我們的錯誤了,重新來過,清楚斷點,打開我們的PerpetualCache類,斷點重新打在PerpetualCache類的get和put方法下。我們左側的方法區,我們看到是這樣的

從Perpetualcahe一直查到TransactionalCache,我們來張圖解釋一下。

大致就是這樣的,逐層去尋找的。這裡就是一個裝飾者模式。
我們還可以將斷點打在CachingExecutor方法的query方法下來觀察我們的二級快取。這個方法在很早就先幫我們把Cache獲取好了,且直接獲取到SynchronizedCache層了。有興趣的小夥伴可以自行測試一下,這裡我就不再多說了,下次部落格我們來具體深入的來看看Mybatis的執行流程,源碼級。
感覺自己現在心中知道怎麼去讀源碼,但是還是說不清楚,不能很好的表達出來,我再改進改進,可能還是看的不夠深吧。。。
我的部落格即將同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan