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