【MyBatis】關於MyBatis的一級緩存和二級緩存,你應該瞭解這些

目錄

1、一級緩存

1.1 我們在一個 sqlSession 中,對 User 表根據id進行兩次查詢,查看他們發出sql語句的情況。

1.2 同樣是對user表進行兩次查詢,只不過兩次查詢之間進行了一次update操作。

1.3 一級緩存查詢過程

1.4 Mybatis與Spring整合導致一級緩存失效

1.5 總結

2、二級緩存

2.1 二級緩存的使用

2.1.1 創建一個POJO Bean並序列化

2.1.2 在映射文件中開啓二級緩存

2.1.3 在 mybatis-config.xml中開啓二級緩存

2.1.4 測試

2.1.5 使用spring整合Mybatis時使用二級緩存

2.2 二級緩存的缺陷


Mybatis 爲我們提供了一級緩存和二級緩存,可以通過下圖來理解:

  • 一級緩存SqlSession級別的緩存。在操作數據庫時需要構造sqlSession對象,在對象中有一個數據結構(HashMap)用於存儲緩存數據。不同的sqlSession之間的緩存數據區域(HashMap)是互相不影響的。
  • 二級緩存mapper級別的緩存,多個SqlSession去操作同一個Mapper的sql語句,多個SqlSession可以共用二級緩存,二級緩存是跨SqlSession的。二級緩存的底層也是HashMap。(同一個Mapper本質就是同一個命名空間)

 

1、一級緩存

我們在一個 sqlSession 中,對 User 表根據id進行兩次查詢,查看他們發出sql語句的情況。

 

1.1 我們在一個 sqlSession 中,對 User 表根據id進行兩次查詢,查看他們發出sql語句的情況。

@Test
public void testSelectOrderAndUserByOrderId(){
    //根據 sqlSessionFactory 產生 sqlsession
    SqlSession sqlSession = sessionFactory.openSession();
    String statement = "one.to.one.mapper.OrdersMapper.selectOrderAndUserByOrderID";
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

    //第一次查詢,發出sql語句,並將查詢的結果放入緩存中
    User u1 = userMapper.selectUserByUserId(1);
    System.out.println(u1);

    //第二次查詢,由於是同一個sqlSession,會在緩存中查找查詢結果
    //如果有,則直接從緩存中取出來,不和數據庫進行交互
    User u2 = userMapper.selectUserByUserId(1);
    System.out.println(u2);

    sqlSession.close();
}

查看控制檯打印情況:

 

1.2 同樣是對user表進行兩次查詢,只不過兩次查詢之間進行了一次update操作。

@Test
public void testSelectOrderAndUserByOrderId(){
    //根據 sqlSessionFactory 產生 sqlsession
    SqlSession sqlSession = sessionFactory.openSession();
    String statement = "one.to.one.mapper.OrdersMapper.selectOrderAndUserByOrderID";
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

    //第一次查詢,發出sql語句,並將查詢的結果放入緩存中
    User u1 = userMapper.selectUserByUserId(1);
    System.out.println(u1);

    //第二步進行了一次更新操作,sqlSession.commit()
    u1.setSex("女");
    userMapper.updateUserByUserId(u1);
    sqlSession.commit();

    //第二次查詢,由於是同一個sqlSession.commit(),會清空緩存信息
    //則此次查詢也會發出 sql 語句
    User u2 = userMapper.selectUserByUserId(1);
    System.out.println(u2);

    sqlSession.close();
}

 控制檯打印情況:

 

1.3 一級緩存查詢過程

1、第一次發起查詢用戶id爲1的用戶信息,先去找緩存中是否有id爲1的用戶信息,如果沒有,從數據庫查詢用戶信息。得到用戶信息,將用戶信息存儲到一級緩存中。

2、如果中間sqlSession去執行commit操作(執行插入、更新、刪除),則會清空SqlSession中的一級緩存,這樣做的目的爲了讓緩存中存儲的是最新的信息,避免髒讀。

3、第二次發起查詢用戶id爲1的用戶信息,先去找緩存中是否有id爲1的用戶信息,緩存中有,直接從緩存中獲取用戶信息。

 

1.4 MybatisSpring整合導致一級緩存失效

我們使用Spring整合MyBatis的時候,就會發現我們的一級緩存失效,即如果1.1的代碼是運行在spring項目下的,那麼兩次查詢都會執行sql語句。

這是因爲spring整合Mybatis之後,sqlSession是要交給spring容器去管理的,在每一次執行sql之後,spring會將sqlSession關閉,所以一級緩存進而失效。

 

1.5 總結

一級緩存與數據庫同一次會話(sqlSession)期間查詢到的數據會放在本地緩存中,如果以後要獲取相同的數據直接從緩存中獲取,不會再次向數據庫查詢數據

一個SqlSession擁有一個一級緩存,這個一級緩存是一個本地緩存。

 

myBatis一直開啓一級緩存,不同SqlSession的緩存,數據不可以共享

 

一級緩存還有失效的情況,一級緩存失效後,第二次查詢相同的語句就還需向數據庫發送sql

一級緩存失效情況:

  1. sqlSession不同
  2. 當sqlSession對象相同的時候,查詢的條件不同,原因是第一次查詢時候一級緩存中沒有第二次查詢所需要的數據
  3. 當sqlSession對象相同,兩次查詢之間進行了插入的操作
  4. 當sqlSession對象相同,手動清除了一級緩存中的數據

 

一級緩存的生命週期有多長?

  1. MyBatis在開啓一個數據庫會話時,會創建一個新的SqlSession對象,SqlSession對象中會有一個新的Executor對象。Executor對象中持有一個新的PerpetualCache對象;當會話結束時,SqlSession對象及其內部的Executor對象還有PerpetualCache對象也一併釋放掉。
  2. 如果SqlSession調用了close()方法,會釋放掉一級緩存PerpetualCache對象,使PerpetualCache對象失效,以致一級緩存將不可用。
  3. 如果SqlSession調用了clearCache(),會清空PerpetualCache對象中的數據,但是該對象仍可使用。
  4. SqlSession中執行了任何一個update操作(update()delete()insert()) ,都會清空PerpetualCache對象的數據,但是該對象可以繼續使用

 

2、二級緩存

二級緩存的原理和一級緩存原理一樣,第一次查詢,會將數據放入緩存中,然後第二次查詢則會直接去緩存中取。但是一級緩存是基於 sqlSession 的,而 二級緩存是基於 mapper文件的namespace的,也就是說多個sqlSession可以共享一個mapper中的二級緩存區域,並且如果兩個mapper的namespace相同,即使是兩個mapper,那麼這兩個mapper中執行sql查詢到的數據也將存在相同的二級緩存區域中。

 

namespace默認就是Mapper的類的全限定名。所以同一個Mapper也就保證了是同一個namespace

 

 

2.1 二級緩存的使用

2.1.1 創建一個POJO Bean並序列化

由於二級緩存的數據不一定都是存儲到內存中,它的存儲介質多種多樣,所以需要給緩存的對象執行序列化。(如果存儲在內存中的話,實測不序列化也可以的。)

public class Student implements Serializable{
    private static final long serialVersionUID = 735655488285535299L;
    private String id;
    private String name;
    private int age;
    private Gender gender;
    private List<Teacher> teachers;
    setters&getters()....;
    toString();        
}

 

2.1.2 在映射文件中開啓二級緩存

<?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="com.yihaomen.mybatis.dao.StudentMapper">
    <!--開啓本mapper的namespace下的二級緩存-->
    <!--
        eviction:代表的是緩存回收策略,目前MyBatis提供以下策略。
        (1) LRU,最近最少使用的,一處最長時間不用的對象
        (2) FIFO,先進先出,按對象進入緩存的順序來移除他們
        (3) SOFT,軟引用,移除基於垃圾回收器狀態和軟引用規則的對象
        (4) WEAK,弱引用,更積極的移除基於垃圾收集器狀態和弱引用規則的對象。這裏採用的是LRU,
                移除最長時間不用的對形象
        flushInterval:刷新間隔時間,單位爲毫秒,這裏配置的是100秒刷新,如果你不配置它,那麼當
        SQL被執行的時候纔會去刷新緩存。
        size:引用數目,一個正整數,代表緩存最多可以存儲多少個對象,不宜設置過大。設置過大會導致內存溢出。
        這裏配置的是1024個對象
        readOnly:只讀,意味着緩存數據只能讀取而不能修改,這樣設置的好處是我們可以快速讀取緩存,缺點是我們沒有
        辦法修改緩存,他的默認值是false,不允許我們修改
    -->
    <cache eviction="LRU" flushInterval="100000" readOnly="true" size="1024"/>
    <resultMap id="studentMap" type="Student">
        <id property="id" column="id" />
        <result property="name" column="name" />
        <result property="age" column="age" />
        <result property="gender" column="gender" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler" />
    </resultMap>
    <resultMap id="collectionMap" type="Student" extends="studentMap">
        <collection property="teachers" ofType="Teacher">
            <id property="id" column="teach_id" />
            <result property="name" column="tname"/>
            <result property="gender" column="tgender" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
            <result property="subject" column="tsubject" typeHandler="org.apache.ibatis.type.EnumTypeHandler"/>
            <result property="degree" column="tdegree" javaType="string" jdbcType="VARCHAR"/>
        </collection>
    </resultMap>
    <select id="selectStudents" resultMap="collectionMap">
        SELECT
            s.id, s.name, s.gender, t.id teach_id, t.name tname, t.gender tgender, t.subject tsubject, t.degree tdegree
        FROM
            student s
        LEFT JOIN
            stu_teach_rel str
        ON
            s.id = str.stu_id
        LEFT JOIN
            teacher t
        ON
            t.id = str.teach_id
    </select>
    <!--可以通過設置useCache來規定這個sql是否開啓緩存,ture是開啓,false是關閉-->
    <select id="selectAllStudents" resultMap="studentMap" useCache="true">
        SELECT id, name, age FROM student
    </select>
    <!--刷新二級緩存
    <select id="selectAllStudents" resultMap="studentMap" flushCache="true">
        SELECT id, name, age FROM student
    </select>
    -->
</mapper>

 

2.1.3 mybatis-config.xml中開啓二級緩存

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <!--這個配置使全局的映射器(二級緩存)啓用或禁用緩存-->
        <setting name="cacheEnabled" value="true" />
        .....
    </settings>
    ....
</configuration>

 

2.1.4 測試

測試二級緩存和sqlSession 無關

@Test
public void testTwoCache(){
    //根據 sqlSessionFactory 產生 session
    SqlSession sqlSession1 = sessionFactory.openSession();
    SqlSession sqlSession2 = sessionFactory.openSession();
    String statement = "com.ys.twocache.UserMapper.selectUserByUserId";

    UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
    UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);

    //第一次查詢,發出sql語句,並將查詢的結果放入緩存中
    User u1 = userMapper1.selectUserByUserId(1);
    System.out.println(u1);
    sqlSession1.close();//第一次查詢完後關閉sqlSession

    //第二次查詢,即使sqlSession1已經關閉了,這次查詢依然不發出sql語句
    User u2 = userMapper2.selectUserByUserId(1);
    System.out.println(u2);
    sqlSession2.close();
}

可以看出上面兩個不同的sqlSession,第一個關閉了,第二次查詢依然不發出sql查詢語句。

 

測試執行 commit() 操作,二級緩存數據清空

@Test
public void testTwoCache(){
    //根據 sqlSessionFactory 產生 session
    SqlSession sqlSession1 = sessionFactory.openSession();
    SqlSession sqlSession2 = sessionFactory.openSession();
    SqlSession sqlSession3 = sessionFactory.openSession();
    String statement = "com.ys.twocache.UserMapper.selectUserByUserId";

    UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
    UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
    UserMapper userMapper3 = sqlSession2.getMapper(UserMapper.class);

    //第一次查詢,發出sql語句,並將查詢的結果放入緩存中
    User u1 = userMapper1.selectUserByUserId(1);
    System.out.println(u1);
    sqlSession1.close();//第一次查詢完後關閉sqlSession

    //執行更新操作,commit()
    u1.setUsername("aaa");
    userMapper3.updateUserByUserId(u1);
    sqlSession3.commit();

    //第二次查詢,由於上次更新操作,緩存數據已經清空(防止數據髒讀),這裏必須再次發出sql語句
    User u2 = userMapper2.selectUserByUserId(1);
    System.out.println(u2);
    sqlSession2.close();
}

查看控制檯情況:

 

 

2.1.5 使用spring整合Mybatis時使用二級緩存

只需要在Mapper接口類上加一個@CacheNamespace註解就開啓二級緩存了。

@CacheNamespace
public interface demoMapper {
    @Select("select * from demo")
    public List<Map<String, Object>> list();
}

 

2.2 二級緩存的缺陷

MyBatis二級緩存使用的在某些場景下會出問題,來看一下爲什麼這麼說。

假設我有一條select語句(開啓了二級緩存):

select a.col1, a.col2, a.col3, b.col1, b.col2, b.col3 from tableA a, tableB b where a.id = b.id;

對於tableA與tableB的操作定義在兩個Mapper中,分別叫做MapperA與MapperB,即它們屬於兩個命名空間,如果此時啓用緩存:

  1. MapperA中執行上述sql語句查詢這6個字段
  2. tableB更新了col1與col2兩個字段
  3. MapperA再次執行上述sql語句查詢這6個字段(前提是沒有執行過任何insert、delete、update操作)

此時問題就來了,即使第(2)步tableB更新了col1與col2兩個字段,第(3)步MapperA走二級緩存查詢到的這6個字段依然是原來的這6個字段的值,因爲我們從CacheKey的3組條件來看:

  1. <select>標籤所在的Mapper的Namespace+<select>標籤的id屬性
  2. RowBounds的offset和limit屬性,RowBounds是MyBatis用於處理分頁的一個類,offset默認爲0,limit默認爲Integer.MAX_VALUE
  3. <select>標籤中定義的sql語句

對於MapperA來說,其中的任何一個條件都沒有變化,自然會將緩存中的原結果返回。

這個問題對於MyBatis的二級緩存來說是一個無解的問題,因此使用MyBatis二級緩存有一個前提:必須保證所有的增刪改查都在同一個命名空間下才行

 

最後說一下,MyBatis支持三種類型的二級緩存:

  • MyBatis默認的緩存,type爲空,Cache爲PerpetualCache
  • 自定義緩存
  • 第三方緩存

彙總自https://www.jianshu.com/p/2be932206c59
              https://www.cnblogs.com/xrq730/p/6991655.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章