MyBatis緩存

一級緩存

Mybatis對緩存提供支持,但是在沒有配置的默認(rèn)情況下,它只開啟一級緩存,一級緩存只是相對于同一個SqlSession而言。所以在參數(shù)和SQL完全一樣的情況下,我們使用同一個SqlSession對象調(diào)用一個Mapper方法,往往只執(zhí)行一次SQL,因?yàn)槭褂肧elSession第一次查詢后,MyBatis會將其放在緩存中,以后再查詢的時候,如果沒有聲明需要刷新,并且緩存沒有超時的情況下,SqlSession都會取出當(dāng)前緩存的數(shù)據(jù),而不會再次發(fā)送SQL到數(shù)據(jù)庫。

image

為什么要使用一級緩存,不用多說也知道個大概。但是還有幾個問題我們要注意一下。

1、一級緩存的生命周期有多長?

a、MyBatis在開啟一個數(shù)據(jù)庫會話時,會 創(chuàng)建一個新的SqlSession對象,SqlSession對象中會有一個新的Executor對象。Executor對象中持有一個新的PerpetualCache對象;當(dāng)會話結(jié)束時,SqlSession對象及其內(nèi)部的Executor對象還有PerpetualCache對象也一并釋放掉。

b、如果SqlSession調(diào)用了close()方法,會釋放掉一級緩存PerpetualCache對象,一級緩存將不可用。

c、如果SqlSession調(diào)用了clearCache(),會清空PerpetualCache對象中的數(shù)據(jù),但是該對象仍可使用。

d、SqlSession中執(zhí)行了任何一個update操作(update()、delete()、insert()) ,都會清空PerpetualCache對象的數(shù)據(jù),但是該對象可以繼續(xù)使用

2、怎么判斷某兩次查詢是完全相同的查詢?

mybatis認(rèn)為,對于兩次查詢,如果以下條件都完全一樣,那么就認(rèn)為它們是完全相同的兩次查詢。

2.1 傳入的statementId

2.2 查詢時要求的結(jié)果集中的結(jié)果范圍

2.3. 這次查詢所產(chǎn)生的最終要傳遞給JDBC java.sql.Preparedstatement的Sql語句字符串(boundSql.getSql() )

2.4 傳遞給java.sql.Statement要設(shè)置的參數(shù)值

二級緩存:

MyBatis的二級緩存是Application級別的緩存,它可以提高對數(shù)據(jù)庫查詢的效率,以提高應(yīng)用的性能。

MyBatis的緩存機(jī)制整體設(shè)計(jì)以及二級緩存的工作模式

image

SqlSessionFactory層面上的二級緩存默認(rèn)是不開啟的,二級緩存的開啟需要進(jìn)行配置,實(shí)現(xiàn)二級緩存的時候,MyBatis要求返回的POJO必須是可序列化的。 也就是要求實(shí)現(xiàn)Serializable接口,配置方法很簡單,只需要在映射XML文件配置就可以開啟緩存了<cache/>,如果我們配置了二級緩存就意味著:

  • 映射語句文件中的所有select語句將會被緩存。
  • 映射語句文件中的所欲insert、update和delete語句會刷新緩存。
  • 緩存會使用默認(rèn)的Least Recently Used(LRU,最近最少使用的)算法來收回。
  • 根據(jù)時間表,比如No Flush Interval,(CNFI沒有刷新間隔),緩存不會以任何時間順序來刷新。
  • 緩存會存儲列表集合或?qū)ο?無論查詢方法返回什么)的1024個引用
  • 緩存會被視為是read/write(可讀/可寫)的緩存,意味著對象檢索不是共享的,而且可以安全的被調(diào)用者修改,不干擾其他調(diào)用者或線程所做的潛在修改。

實(shí)踐:

一、創(chuàng)建一個POJO Bean并序列化

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

import java.io.Serializable; 
import java.util.List; 
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();        
}

二、在映射文件中開啟二級緩存

cache配置

屬性 說明 默認(rèn)值 可選值
eviction 回收內(nèi)存策略 LRU LRU FIFO SOFT WEAK
flushInterval 刷新間隔 沒設(shè)置 大于0 (單位:ms)
size 緩存對象的數(shù)量 1024 大于0
readOnly 緩存數(shù)據(jù)只能讀取而不能修改 false true false
type 可以指定自定義緩存,但是該類必須實(shí)現(xiàn)org.apache.ibatis.cache.Cache接口 com....class

自定義緩存

該屬性會調(diào)用setCacheFile方法(setter),將屬性值注入。
<cache type="com.domain.something.MyCustomCache">
<property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>
</cache>

開啟本mapper的namespace下的二級緩存
eviction:代表的是緩存回收策略

  • LRU,最近最少使用的,一處最長時間不用的對象
  • FIFO,先進(jìn)先出,按對象進(jìn)入緩存的順序來移除他們
  • SOFT,軟引用,移除基于垃圾回收器狀態(tài)和軟引用規(guī)則的對象
  • WEAK,弱引用,更積極的移除基于垃圾收集器狀態(tài)和弱引用規(guī)則的對象。

例子采用LRU,移除最長時間不用的對形象 flushInterval:刷新間隔時間,單位為毫秒,這里配置的是100秒刷新,如果你不配置它,那么當(dāng)SQL被執(zhí)行的時候才會去刷新緩存。

size:引用數(shù)目,一個正整數(shù),代表緩存最多可以存儲多少個對象,不宜設(shè)置過大。設(shè)置過大會導(dǎo)致內(nèi)存溢出,這里配置的是1024個對象。

readOnly:只讀,意味著緩存數(shù)據(jù)只能讀取而不能修改,這樣設(shè)置的好處是我們可以快速讀取緩存,缺點(diǎn)是我們沒有辦法修改緩存,他的默認(rèn)值是false,不允許我們修改。

<?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.demo.mybatis.dao.StudentMapper">
  
  <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
      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>
    
  <!--可以通過設(shè)置useCache來規(guī)定這個sql是否開啟緩存,ture是開啟,false是關(guān)閉-->
  <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>

三、在 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>

四、測試

import org.apache.ibatis.session.SqlSession; 
import org.apache.ibatis.session.SqlSessionFactory; 
import java.util.List; 
public class TestStudent extends BaseTest { 
  public static void selectAllStudent() {
        SqlSessionFactory sqlSessionFactory = getSession();
        SqlSession session = sqlSessionFactory.openSession();
        StudentMapper mapper = session.getMapper(StudentMapper.class);
        List<Student> list = mapper.selectAllStudents();
        System.out.println(list);
        System.out.println("第二次執(zhí)行");

        List<Student> list2 = mapper.selectAllStudents();
        System.out.println(list2);
        session.commit();
        System.out.println("二級緩存觀測點(diǎn)");

        SqlSession session2 = sqlSessionFactory.openSession();
        StudentMapper mapper2 = session2.getMapper(StudentMapper.class);
        List<Student> list3 = mapper2.selectAllStudents();
        System.out.println(list3);

        System.out.println("第二次執(zhí)行");
        List<Student> list4 = mapper2.selectAllStudents();
        System.out.println(list4);
        session2.commit();
    } 

    public static void main(String[] args) {
        selectAllStudent();
    }
}

結(jié)果:
我們可以從結(jié)果看到,sql只執(zhí)行了一次,證明我們的二級緩存生效了。

探究二級緩存

我們繼續(xù)以 MyBatis 一級緩存文章中的例子為基礎(chǔ),搭建一個滿足二級緩存的例子,來對二級緩存進(jìn)行探究,例子如下(對 一級緩存的例子部分源碼進(jìn)行修改):

Dept.java
存放在共享緩存中數(shù)據(jù)進(jìn)行序列化操作和反序列化操作,實(shí)體類必須實(shí)現(xiàn)序列化接口。

public class Dept implements Serializable {

    private Integer deptNo;
    private String  dname;
    private String  loc;

    public Dept() {}
    public Dept(Integer deptNo, String dname, String loc) {
        this.deptNo = deptNo;
        this.dname = dname;
        this.loc = loc;
    }

   get and set...
}

對應(yīng)的二級緩存測試類如下:

public class MyBatisSecondCacheTest {

    private SqlSession sqlSession;
    SqlSessionFactory factory;
    @Before
    public void start() throws IOException {
        InputStream is = Resources.getResourceAsStream("myBatis-config.xml");
        SqlSessionFactoryBuilder builderObj = new SqlSessionFactoryBuilder();
        factory = builderObj.build(is);
        sqlSession = factory.openSession();
    }
    @After
    public void destory(){
        if(sqlSession!=null){
            sqlSession.close();
        }
    }

    @Test
    public void testSecondCache(){
        //會話過程中第一次發(fā)送請求,從數(shù)據(jù)庫中得到結(jié)果
        //得到結(jié)果之后,mybatis自動將這個查詢結(jié)果放入到當(dāng)前用戶的一級緩存
        DeptDao dao =  sqlSession.getMapper(DeptDao.class);
        Dept dept = dao.findByDeptNo(1);
        System.out.println("第一次查詢得到部門對象 = "+dept);
        //觸發(fā)MyBatis框架從當(dāng)前一級緩存中將Dept對象保存到二級緩存

        sqlSession.commit();
        // 改成 sqlSession.close(); 效果相同

        SqlSession session2 = factory.openSession();
        DeptDao dao2 = session2.getMapper(DeptDao.class);
        Dept dept2 = dao2.findByDeptNo(1);
        System.out.println("第二次查詢得到部門對象 = "+dept2);
    }
}

測試二級緩存效果,提交事務(wù),sqlSession查詢完數(shù)據(jù)后,sqlSession2相同的查詢是否會從緩存中獲取數(shù)據(jù)。

測試結(jié)果如下:

通過結(jié)果可以得知,首次執(zhí)行的SQL語句是從數(shù)據(jù)庫中查詢得到的結(jié)果,然后第一個 SqlSession 執(zhí)行提交,第二個 SqlSession 執(zhí)行相同的查詢后是從緩存中查取的。

用一下這幅圖能夠比較直觀的反映兩次 SqlSession 的緩存命中

image

二級緩存失效的條件

與一級緩存一樣,二級緩存也會存在失效的條件的,下面我們就來探究一下哪些情況會造成二級緩存失效

  • 第一次SqlSession 未提交

SqlSession 在未提交的時候,SQL 語句產(chǎn)生的查詢結(jié)果還沒有放入二級緩存中,這個時候 SqlSession2 在查詢的時候是感受不到二級緩存的存在的,修改對應(yīng)的測試類,結(jié)果如下:

@Test
public void testSqlSessionUnCommit(){
  //會話過程中第一次發(fā)送請求,從數(shù)據(jù)庫中得到結(jié)果
  //得到結(jié)果之后,mybatis自動將這個查詢結(jié)果放入到當(dāng)前用戶的一級緩存
  DeptDao dao =  sqlSession.getMapper(DeptDao.class);
  Dept dept = dao.findByDeptNo(1);
  System.out.println("第一次查詢得到部門對象 = "+dept);
  //觸發(fā)MyBatis框架從當(dāng)前一級緩存中將Dept對象保存到二級緩存

  SqlSession session2 = factory.openSession();
  DeptDao dao2 = session2.getMapper(DeptDao.class);
  Dept dept2 = dao2.findByDeptNo(1);
  System.out.println("第二次查詢得到部門對象 = "+dept2);
}

產(chǎn)生的輸出結(jié)果:

image
  • 更新對二級緩存影響

與一級緩存一樣,更新操作很可能對二級緩存造成影響,下面用三個 SqlSession來進(jìn)行模擬,第一個 SqlSession 只是單純的提交,第二個 SqlSession 用于檢驗(yàn)二級緩存所產(chǎn)生的影響,第三個 SqlSession 用于執(zhí)行更新操作,測試如下:

@Test
public void testSqlSessionUpdate(){
  SqlSession sqlSession = factory.openSession();
  SqlSession sqlSession2 = factory.openSession();
  SqlSession sqlSession3 = factory.openSession();

  // 第一個 SqlSession 執(zhí)行更新操作
  DeptDao deptDao = sqlSession.getMapper(DeptDao.class);
  Dept dept = deptDao.findByDeptNo(1);
  System.out.println("dept = " + dept);
  sqlSession.commit();

  // 判斷第二個 SqlSession 是否從緩存中讀取
  DeptDao deptDao2 = sqlSession2.getMapper(DeptDao.class);
  Dept dept2 = deptDao2.findByDeptNo(1);
  System.out.println("dept2 = " + dept2);

  // 第三個 SqlSession 執(zhí)行更新操作
  DeptDao deptDao3 = sqlSession3.getMapper(DeptDao.class);
  deptDao3.updateDept(new Dept(1,"ali","hz"));
  sqlSession3.commit();

  // 判斷第二個 SqlSession 是否從緩存中讀取
  dept2 = deptDao2.findByDeptNo(1);
  System.out.println("dept2 = " + dept2);
}

對應(yīng)的輸出結(jié)果如下

image

探究多表操作對二級緩存的影響

現(xiàn)有這樣一個場景,有兩個表,部門表dept(deptNo,dname,loc)和 部門數(shù)量表deptNum(id,name,num),其中部門表的名稱和部門數(shù)量表的名稱相同,通過名稱能夠聯(lián)查兩個表可以知道其坐標(biāo)(loc)和數(shù)量(num),現(xiàn)在我要對部門數(shù)量表的 num 進(jìn)行更新,然后我再次關(guān)聯(lián)dept 和 deptNum 進(jìn)行查詢,你認(rèn)為這個 SQL 語句能夠查詢到的 num 的數(shù)量是多少?來看一下代碼探究一下

DeptNum.java

public class DeptNum {

    private int id;
    private String name;
    private int num;

    get and set...
}

DeptVo.java

public class DeptVo {

    private Integer deptNo;
    private String  dname;
    private String  loc;
    private Integer num;

    public DeptVo(Integer deptNo, String dname, String loc, Integer num) {
        this.deptNo = deptNo;
        this.dname = dname;
        this.loc = loc;
        this.num = num;
    }

    public DeptVo(String dname, Integer num) {
        this.dname = dname;
        this.num = num;
    }
}

DeptDao.java

public interface DeptDao {

    ...

    DeptVo selectByDeptVo(String name);

    DeptVo selectByDeptVoName(String name);

    int updateDeptVoNum(DeptVo deptVo);
}

DeptDao.xml

<select id="selectByDeptVo" resultType="com.mybatis.beans.DeptVo">
  select d.deptno,d.dname,d.loc,dn.num 
  from dept d,deptNum dn 
  where dn.name = d.dname
    and d.dname = #{name}
</select>

<select id="selectByDeptVoName" resultType="com.mybatis.beans.DeptVo">
  select * from deptNum where name = #{name}
</select>

<update id="updateDeptVoNum"  parameterType="com.mybatis.beans.DeptVo">
  update deptNum set num = #{num} where name = #{dname}
</update>

DeptNum 數(shù)據(jù)庫初始值:

image

測試類對應(yīng)如下:

/**
 * 探究多表操作對二級緩存的影響
 */
@Test
public void testOtherMapper(){

  // 第一個mapper 先執(zhí)行聯(lián)查操作
  SqlSession sqlSession = factory.openSession();
  DeptDao deptDao = sqlSession.getMapper(DeptDao.class);
  DeptVo deptVo = deptDao.selectByDeptVo("ali");
  System.out.println("deptVo = " + deptVo);

  // 第二個mapper 執(zhí)行更新操作 并提交
  SqlSession sqlSession2 = factory.openSession();
  DeptDao deptDao2 = sqlSession2.getMapper(DeptDao.class);
  deptDao2.updateDeptVoNum(new DeptVo("ali",1000));
  sqlSession2.commit();
  sqlSession2.close();

  // 第一個mapper 再次進(jìn)行查詢,觀察查詢結(jié)果
  deptVo = deptDao.selectByDeptVo("ali");
  System.out.println("deptVo = " + deptVo);
}

測試結(jié)果如下:

image

在對DeptNum 表執(zhí)行了一次更新后,再次進(jìn)行聯(lián)查,發(fā)現(xiàn)數(shù)據(jù)庫中查詢出的還是 num 為 1050 的值,也就是說,實(shí)際上 1050 -> 1000 ,最后一次聯(lián)查實(shí)際上查詢的是第一次查詢結(jié)果的緩存,而不是從數(shù)據(jù)庫中查詢得到的值,這樣就讀到了臟數(shù)據(jù)。

解決辦法

如果是兩個mapper命名空間的話,可以使用 <cache-ref>來把一個命名空間指向另外一個命名空間,從而消除上述的影響,再次執(zhí)行,就可以查詢到正確的數(shù)據(jù)。

二級緩存整體管理結(jié)構(gòu):

MapperA.xml

<mapper namespace="com.jabnih.demo.mapper.MapperA">
    <cache />
</mapper>

MapperB.xml

<mapper namespace="com.jabnih.demo.mapper.MapperB">
    <cache-ref namespace="com.jabnih.demo.mapper.MapperA"/>
</mapper>

MapperC.xml

<mapper namespace="com.jabnih.demo.mapper.MapperC">
    <cache />
</mapper>

如下:


image

二級緩存源碼解析

源碼模塊主要分為兩個部分:二級緩存的創(chuàng)建和二級緩存的使用,首先先對二級緩存的創(chuàng)建進(jìn)行分析:

二級緩存的創(chuàng)建

二級緩存的創(chuàng)建是使用 Resource 讀取 XML 配置文件開始的

InputStream is = Resources.getResourceAsStream("myBatis-config.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
factory = builder.build(is);

讀取配置文件后,需要對XML創(chuàng)建 Configuration并初始化

XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());

調(diào)用 parser.parse() 解析根目錄 /configuration 下面的標(biāo)簽,依次進(jìn)行解析

public Configuration parse() {
  if (parsed) {
    throw new BuilderException("Each XMLConfigBuilder can only be used once.");
  }
  parsed = true;
  parseConfiguration(parser.evalNode("/configuration"));
  return configuration;
}
private void parseConfiguration(XNode root) {
  try {
    //issue #117 read properties first
    propertiesElement(root.evalNode("properties"));
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    loadCustomVfs(settings);
    typeAliasesElement(root.evalNode("typeAliases"));
    pluginElement(root.evalNode("plugins"));
    objectFactoryElement(root.evalNode("objectFactory"));
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    reflectorFactoryElement(root.evalNode("reflectorFactory"));
    settingsElement(settings);
    // read it after objectFactory and objectWrapperFactory issue #631
    environmentsElement(root.evalNode("environments"));
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    typeHandlerElement(root.evalNode("typeHandlers"));
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}

其中有一個二級緩存的解析就是

mapperElement(root.evalNode("mappers"));

然后進(jìn)去 mapperElement 方法中

XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();

繼續(xù)跟 mapperParser.parse() 方法

public void parse() {
  if (!configuration.isResourceLoaded(resource)) {
    configurationElement(parser.evalNode("/mapper"));
    configuration.addLoadedResource(resource);
    bindMapperForNamespace();
  }

  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}

這其中有一個 configurationElement 方法,它是對二級緩存進(jìn)行創(chuàng)建,如下

private void configurationElement(XNode context) {
  try {
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.equals("")) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);
    cacheRefElement(context.evalNode("cache-ref"));
    cacheElement(context.evalNode("cache"));
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    sqlElement(context.evalNodes("/mapper/sql"));
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
  }
}

有兩個二級緩存的關(guān)鍵點(diǎn)

cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));

也就是說,mybatis 首先進(jìn)行解析的是 cache-ref 標(biāo)簽,其次進(jìn)行解析的是 cache 標(biāo)簽。

根據(jù)上面我們的 — 多表操作對二級緩存的影響 一節(jié)中提到的解決辦法,采用 cache-ref 來進(jìn)行命名空間的依賴能夠避免二級緩存,但是總不能每次寫一個 XML 配置都會采用這種方式吧,最有效的方式還是避免多表操作使用二級緩存

然后我們再來看一下cacheElement(context.evalNode("cache")) 這個方法

private void cacheElement(XNode context) throws Exception {
  if (context != null) {
    String type = context.getStringAttribute("type", "PERPETUAL");
    Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
    String eviction = context.getStringAttribute("eviction", "LRU");
    Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
    Long flushInterval = context.getLongAttribute("flushInterval");
    Integer size = context.getIntAttribute("size");
    boolean readWrite = !context.getBooleanAttribute("readOnly", false);
    boolean blocking = context.getBooleanAttribute("blocking", false);
    Properties props = context.getChildrenAsProperties();
    builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
  }
}

認(rèn)真看一下其中的屬性的解析,是不是感覺很熟悉?這不就是對 cache 標(biāo)簽屬性的解析嗎??。?!

上述最后一句代碼

builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
public Cache useNewCache(Class<? extends Cache> typeClass,
      Class<? extends Cache> evictionClass,
      Long flushInterval,
      Integer size,
      boolean readWrite,
      boolean blocking,
      Properties props) {
    Cache cache = new CacheBuilder(currentNamespace)
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
  }

這段代碼使用了構(gòu)建器模式,一步一步構(gòu)建Cache 標(biāo)簽的所有屬性,最終把 cache 返回。

二級緩存的使用

在 mybatis 中,使用 Cache 的地方在 CachingExecutor中,來看一下 CachingExecutor 中緩存做了什么工作,我們以查詢?yōu)槔?/p>

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
  throws SQLException {
  // 得到緩存
  Cache cache = ms.getCache();
  if (cache != null) {
    // 如果需要的話刷新緩存
    flushCacheIfRequired(ms);
    if (ms.isUseCache() && resultHandler == null) {
      ensureNoOutParams(ms, parameterObject, boundSql);
      @SuppressWarnings("unchecked")
      List<E> list = (List<E>) tcm.getObject(cache, key);
      if (list == null) {
        list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      return list;
    }
  }
  // 委托模式,交給SimpleExecutor等實(shí)現(xiàn)類去實(shí)現(xiàn)方法。
  return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

其中,先從 MapperStatement 取出緩存。只有通過<cache/>,<cache-ref/>@CacheNamespace,@CacheNamespaceRef標(biāo)記使用緩存的Mapper.xml或Mapper接口(同一個namespace,不能同時使用)才會有二級緩存。

如果緩存不為空,說明是存在緩存。如果cache存在,那么會根據(jù)sql配置(<insert>,<select>,<update>,<delete>flushCache屬性來確定是否清空緩存。

flushCacheIfRequired(ms);

然后根據(jù)xml配置的屬性useCache來判斷是否使用緩存(resultHandler一般使用的默認(rèn)值,很少會null)。

if (ms.isUseCache() && resultHandler == null)

確保方法沒有Out類型的參數(shù),mybatis不支持存儲過程的緩存,所以如果是存儲過程,這里就會報(bào)錯。

private void ensureNoOutParams(MappedStatement ms, Object parameter, BoundSql boundSql) {
  if (ms.getStatementType() == StatementType.CALLABLE) {
    for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
      if (parameterMapping.getMode() != ParameterMode.IN) {
        throw new ExecutorException("Caching stored procedures with OUT params is not supported.  Please configure useCache=false in " + ms.getId() + " statement.");
      }
    }
  }
}

然后根據(jù)在 TransactionalCacheManager 中根據(jù) key 取出緩存,如果沒有緩存,就會執(zhí)行查詢,并且將查詢結(jié)果放到緩存中并返回取出結(jié)果,否則就執(zhí)行真正的查詢方法。

List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
  list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;

是否應(yīng)該使用二級緩存?

那么究竟應(yīng)該不應(yīng)該使用二級緩存呢?先來看一下二級緩存的注意事項(xiàng):

  1. 緩存是以namespace為單位的,不同namespace下的操作互不影響。
  2. insert,update,delete操作會清空所在namespace下的全部緩存。
  3. 通常使用MyBatis Generator生成的代碼中,都是各個表獨(dú)立的,每個表都有自己的namespace
  4. 多表操作一定不要使用二級緩存,因?yàn)槎啾聿僮鬟M(jìn)行更新操作,一定會產(chǎn)生臟數(shù)據(jù)。

如果你遵守二級緩存的注意事項(xiàng),那么你就可以使用二級緩存。

但是,如果不能使用多表操作,二級緩存不就可以用一級緩存來替換掉嗎?而且二級緩存是表級緩存,開銷大,沒有一級緩存直接使用 HashMap 來存儲的效率更高,所以二級緩存并不推薦使用

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容