MyBatis快取機制(一級快取,二級快取)

小L要努力吖發表於2020-05-20

一,MyBatis一級快取(本地快取)

  My Batis 一級快取存在於 SqlSession 的生命週期中,是SqlSession級別的快取。在運算元據庫時需要構造SqlSession物件,在物件中有一個資料結構用來儲存快取資料。不同的SqlSession之間的資料快取是不能共享的。
  在同一個SqlSession 中查詢資料時,sqlSession會先在一級快取中查詢,如果有,直接讀取,如果沒有,則從資料庫中查詢, 接著把執行的方法和引數通過演算法生成快取的鍵值,將鍵值和查詢結果存入一級快取中(以Map物件的形式)。如果後面再次執行相同方法,SqlSession通過演算法會生成相同的鍵值,然後在一級快取中查詢,由於一級快取中己經存在該鍵值,所以會返回快取中的物件。與執行select不同的是,執行update,insert,delect操作後會清空一級快取中的資料,而不是通過演算法生成快取的鍵值存入一級快取,之所以有這種差別是因為 select的flushCache(清空快取)預設為false,而update,insert,delect的flushCache(清空快取)預設為true。
  當然也可以使用下面的方法對select操作進行設定,
<select id="selectStudentByIdAndName"  flushCache=”true  resultType="student">
    select * from student where sid=#{Sid} and s_name=#{Sname}
</select
  就是在原來方法的基礎上增加了 flushCache= true ,這個屬性配置為 true 後,在查詢資料後會清空當前的一級快取,因此呼叫該方法後每次都會重新從資料庫中查詢資料,但是由於這個方法清空了一級快取,會影響當前 SqlSession 中所有快取的查詢,因此在需要反覆查詢獲取只讀資料的情況下,會增加資料庫的查詢次數,所以要避免這麼使用。
  除了上面講的將 flushCache賦值為true的情況外,還會導致一級快取清空的情況就是關閉第一個 SqlSession,然後重新開啟一個SqlSession,由於一級快取是和 SqlSession 繫結的,只存在於 SqlSession的生命週期中,所以在新的SqlSession中呼叫剛才的方法,在快取中就查不到,必須去資料庫中查詢,當然之後在呼叫過該方法並不清除的情況下就可以在快取中取到了。
一級快取原理圖:
程式碼:
public class MyBatisTest {
    public static void main( String[] args ) {
        SqlSession openSession = null;
        try {
            //mybatis配置檔案
            String resourse="mybatis-cfg.xml";
            //通過 Resources 工具類將 ti -config.xm 配置檔案讀入 Reader
            InputStream inputStream=Resources.getResourceAsStream(resourse);
            //通過 SqlSessionFactoryBuilder 建造類使用 Reader 建立 SqlSessionFactory工廠物件
            SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);
            //通過SqlSessionFactory工廠得到SqlSession
            openSession = sqlSessionFactory.openSession();
            //通過反射機制來獲取對應的Mapper例項
            StudentMapper mapper=openSession.getMapper(StudentMapper.class);
            Student student1=mapper.selectStudentByIdAndName(2,"danghh");
            Student student2=mapper.selectStudentByIdAndName(2,"danghh");
            System.out.println(student1);
            openSession.commit();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //最後一定不要忘記關閉 SqlSession ,否則會因為連線沒有關閉導致資料庫連線數過多,造成系統崩旗
            openSession.close();
        }
    }
}
執行結果: 
[DEBUG] - Setting autocommit to false on JDBC Connection[com.mysql.jdbc.JDBC4Connection@dfd3711]
[DEBUG] - ==>  Preparing: select * from student where sid=? and s_name=? 
[DEBUG] - ==> Parameters: 2(Integer), danghh(String)
[DEBUG] - <==      Total: 1
 Student{SID=2, Sname='danghh', Sage=22, Ssex='nv', course=null}
Student{SID=2, Sname='danghh', Sage=22, Ssex='nv', course=null}

  通過結果可以看出,由於程式碼中查詢是在一個SqlSession,且兩次查詢過程中沒有更新資訊,不會導致一級快取失效,所以結果只進行了一次資料庫查詢。

  那如果是在兩個SqlSession中分別進行查詢呢?

 程式碼:

 結果:

[DEBUG] - Opening JDBC Connection
[DEBUG] - Checked out connection 234698513 from pool.
[DEBUG] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@dfd3711]
[DEBUG] - ==>  Preparing: select * from student where sid=? and s_name=? 
[DEBUG] - ==> Parameters: 2(Integer), danghh(String)
[DEBUG] - <==      Total: 1
[DEBUG] - Opening JDBC Connection
[DEBUG] - Created connection 1836797772.
[DEBUG] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@6d7b4f4c]
[DEBUG] - ==>  Preparing: select * from student where sid=? and s_name=? 
[DEBUG] - ==> Parameters: 2(Integer), danghh(String)
[DEBUG] - <==      Total: 1
Student{SID=2, Sname='danghh', Sage=22, Ssex='nv', course=null}
Student{SID=2, Sname='danghh', Sage=22, Ssex='nv', course=null}
  可以看出在使用了兩個SqlSession進行查詢後,在資料庫進行了兩次查詢,也驗證了SqlSession的一級快取是和SqlSession的生命週期繫結的,作用範圍也只在當前SqlSession中。
  另外在演示下如果在進行查詢之前進行了一次update操作會不會使一級快取清空呢?
程式碼:
執行結果:
 [DEBUG] - ==>  Preparing: select * from student where sid=? and s_name=? 
 [DEBUG] - ==> Parameters: 2(Integer), hjj(String)
 [DEBUG] - <==      Total: 1
 [DEBUG] - ==>  Preparing: update student set S_name=?,Sage=?,Ssex=? where Sid=? 
 [DEBUG] - ==> Parameters: hjj(String), 23(Integer), null, 2(Integer)
 [DEBUG] - <==    Updates: 1
 [DEBUG] - ==>  Preparing: select * from student where sid=? and s_name=? 
 [DEBUG] - ==> Parameters: 2(Integer), hjj(String)
 [DEBUG] - <==      Total: 1
  Student{SID=2, Sname='hjj', Sage=23, Ssex='null', course=null}
  Student{SID=2, Sname='hjj', Sage=23, Ssex='null', course=null}
二,MyBatis二級快取(全域性快取)
  MyBatis二級快取非常強大,它不同於一級快取只存在於 SqlSession 的生命週期中,而是可以理解為存在於 SqlSessionFactory 的生命週期中 ,是Mapper(studentMapper)級別的快取,一個Mapper對應一個二級快取,當Mapper中的多個SqlSession共同操作同一個方法時,多個SqlSession是可以共用二級快取的中的資料的,所以二級快取是跨SqlSession的。
  在開啟二級快取時,查出來的資料預設先儲存在一級快取中,當有SqlSession關閉時,它裡面一級快取中的資料就會被儲存到Mapper的二級快取中,這樣該Mapper中的其他會話執行了相同方法時,就會在二級快取中找到匹配的資料,如果沒有找到,才會去資料庫中查詢。注意只有在該會話關閉時,它一級快取中的資料才會被刷到二級快取中。另外如果只是開啟二級快取的全域性(config)開關,而會話(student)沒有開啟二級快取,查詢時也不會在二級快取中查詢。
  一級快取( 也叫本地快取)一般預設會啟開,不需要進行配置,但要使用二級快取就需要進行配置。那如何配置呢?
第一步:在全域性配置檔案中新增

(這個引數是二級快取的全域性開關,預設值是 true ,初始狀態為啟用狀態,所以也可忽略此步的配置)

  

(由於MyBatis二級快取和名稱空間namespace是繫結的 ,即二級快取還需要在 Mapper.xml 對映檔案中配置或者在 Mapper.java 介面中配置。)

第二步:在Sql對映檔案中新增<cache></cache>元素。
        
  上面的配置建立了一個 FIFO 快取,並每隔6秒重新整理一次,儲存集合或物件的1024個引用,而且返回的物件被認為是非只讀的。
  eviction :快取的收回策略
  • LRU (最近最少使用的) 移除最長時間不被使用的物件,這是預設值
  • FIFO (先進先出〉 按物件進入快取的順序來移除它們
  • SOFT (軟引用) 移除基於垃圾回收器狀態和軟引用規則的物件
  • WEAK (弱引用) 更積極地移除基於垃圾收集器狀態和弱引用規則的物件
  flushinterval :重新整理間隔
    設定快取多長時間清空一次,單位為毫秒值,預設不清空。
  readOnly:是否只讀
    true:只讀,設定為true後,mybatis認為所有從快取中獲取資料的操作都是隻讀操作,不會修改資料,因此為了加快獲取速度,一般會直接將資料在快取中的引用交給使用者,雖然速度快,但不安全。
    false:非只讀,設定為false後,mybatis認為獲取的資料可能會被修改,因此會利用序列化和反序列化的技術克隆一份新的資料給你,雖然速度慢,但安全。
       預設是 false
  size :引用數目
    設定快取可以存放的引用數目,可以被設定為任意正整數。預設值是 1024。
第三步:給POJO類實現序列化介面
  

程式碼:

public class MyBatisTest {
    public static void main( String[] args ) {
        SqlSession openSession1 = null;
        SqlSession openSession2 = null;
        try {
            //mybatis配置檔案
            String resourse="mybatis-cfg.xml";
            //通過 Resources 工具類將 ti -config.xm 配置檔案讀入 Reader
            InputStream inputStream=Resources.getResourceAsStream(resourse);
            //通過 SqlSessionFactoryBuilder 建造類使用 Reader 建立 SqlSessionFactory工廠物件
            SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);
            //通過SqlSessionFactory工廠得到SqlSession1
            openSession1 = sqlSessionFactory.openSession();
            StudentMapper mapper1=openSession1.getMapper(StudentMapper.class);
            //通過SqlSessionFactory工廠得到SqlSession2
            openSession2 = sqlSessionFactory.openSession();
            StudentMapper mapper2=openSession2.getMapper(StudentMapper.class);
            //使用會話1進行查詢,此次查詢結果只會儲存在一級快取中
            Student student1=mapper1.selectStudentByIdAndName(2,"hjj");
            System.out.println(student1);
            //使用會話2進行查詢,前面會話未關閉,資料不會被刷到二級快取中,所以本次仍會執行sql
            Student student2=mapper2.selectStudentByIdAndName(2,"hjj");
            System.out.println(student2);
            //使用會話2進行查詢,由於前面已執行過該方法,所以可在一級快取中查到
            Student student3=mapper2.selectStudentByIdAndName(2,"hjj");
            System.out.println(student3);
            openSession1.commit();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //最後一定不要忘記關閉 SqlSession ,否則會因為連線沒有關閉導致資料庫連線數過多,造成系統崩旗
            openSession1.close();
        }
    }
}
執行結果:
[DEBUG] - Cache Hit Ratio [MyBatisDemo.StudentMapper]: 0.0
[DEBUG] - Opening JDBC Connection
[DEBUG] - Checked out connection 234698513 from pool.
[DEBUG] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@dfd3711]
[DEBUG] - ==>  Preparing: select * from student where sid=? and s_name=? 
[DEBUG] - ==> Parameters: 2(Integer), hjj(String)
[DEBUG] - <==      Total: 1
     Student{SID=2, Sname='hjj', Sage=23, Ssex='null', course=null}
[DEBUG] - Cache Hit Ratio [MyBatisDemo.StudentMapper]: 0.0
[DEBUG] - Opening JDBC Connection
[DEBUG] - Created connection 1843368112.
[DEBUG] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@6ddf90b0]
[DEBUG] - ==>  Preparing: select * from student where sid=? and s_name=? 
[DEBUG] - ==> Parameters: 2(Integer), hjj(String)
[DEBUG] - <==      Total: 1
     Student{SID=2, Sname='hjj', Sage=23, Ssex='null', course=null}
[DEBUG] - Cache Hit Ratio [MyBatisDemo.StudentMapper]: 0.0
      Student{SID=2, Sname='hjj', Sage=23, Ssex='null', course=null}
[DEBUG] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@dfd3711]
[DEBUG] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@dfd3711]
[DEBUG] - Returned connection 234698513 to pool.

該顏色:表示會話1第一次查詢的結果,由於第一次查詢,一級快取和二級快取中都沒有資料,所以Mapper命中率為0.0,且進行了資料庫查詢,並將結果儲存到會話1一級快取中。

該顏色:表示會話2第一次查詢的結果,由於會話1沒有關閉,所以會話1的一級快取不會刷到Mapper的二級快取中,並且是在會話2中第一次查詢該方法,所以Mapper命中率為0.0,且進行了資料庫查詢,並將結果儲存到會話2的一級快取中。

該顏色:表示會話2第二次查詢的結果,雖然會話1沒有關閉,會話1的一級快取不會刷到Mapper的二級快取中,但是在會話2中查詢過該方法,在會話2的一級快取中已存在該資料,所以Mapper命中率為0.0,沒有進行資料庫查詢。
  接下來就驗證下會話中一級快取的資料是不是隻有在該會話關閉後才會被重新整理到mapper的二級快取!
程式碼:(僅擷取部分)
 

 執行結果:

[DEBUG] - Cache Hit Ratio [MyBatisDemo.StudentMapper]: 0.0
[DEBUG] - Opening JDBC Connection
[DEBUG] - Checked out connection 234698513 from pool.
[DEBUG] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@dfd3711]
[DEBUG] - ==>  Preparing: select * from student where sid=? and s_name=? 
[DEBUG] - ==> Parameters: 2(Integer), hjj(String)
[DEBUG] - <==      Total: 1
  Student{SID=2, Sname='hjj', Sage=23, Ssex='null', course=null}
[DEBUG] - Cache Hit Ratio [MyBatisDemo.StudentMapper]: 0.0
  Student{SID=2, Sname='hjj', Sage=23, Ssex='null', course=null}
[DEBUG] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@dfd3711]
[DEBUG] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@dfd3711]
[DEBUG] - Returned connection 234698513 to pool.
[DEBUG] - Cache Hit Ratio [MyBatisDemo.StudentMapper]: 0.3333333333333333
  Student{SID=2, Sname='hjj', Sage=23, Ssex='null', course=null}
[DEBUG] - Cache Hit Ratio [MyBatisDemo.StudentMapper]: 0.5
  Student{SID=2, Sname='hjj', Sage=23, Ssex='null', course=null}

 

 

相關文章