1. 程式人生 > >java架構之路-(原始碼)mybatis的一二級快取問題

java架構之路-(原始碼)mybatis的一二級快取問題

  上次部落格我們說了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的執行流程,原始碼級。

感覺自己現在心中知道怎麼去讀原始碼,但是還是說不清楚,不能很好的表達出來,我再改進改進,可能還是看的不夠深吧。