欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一文搞懂MyBatis一級(jí)緩存和二級(jí)緩存

 更新時(shí)間:2023年05月11日 10:16:44   作者:半夏之沫  
本文主要介紹了一文搞懂MyBatis一級(jí)緩存和二級(jí)緩存,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧

前言

在本篇文章中,將結(jié)合示例與源碼,對(duì)MyBatis中的一級(jí)緩存二級(jí)緩存進(jìn)行說明。

MyBatis版本:3.5.6

正文

一. 一級(jí)緩存機(jī)制展示

MyBatis中如果多次執(zhí)行完全相同的SQL語句時(shí),MyBatis提供了一級(jí)緩存機(jī)制用于提高查詢效率。一級(jí)緩存是默認(rèn)開啟的,如果想要手動(dòng)配置,需要在MyBatis配置文件中加入如下配置。

<settings>
    <setting name="localCacheScope" value="SESSION"/>
</settings>

其中localCacheScope可以配置為SESSION(默認(rèn)) 或者STATEMENT,含義如下所示。

屬性值含義
SESSION一級(jí)緩存在一個(gè)會(huì)話中生效。即在一個(gè)會(huì)話中的所有查詢語句,均會(huì)共享同一份一級(jí)緩存,不同會(huì)話中的一級(jí)緩存不共享。
STATEMENT一級(jí)緩存僅針對(duì)當(dāng)前執(zhí)行的SQL語句生效。當(dāng)前執(zhí)行的SQL語句執(zhí)行完畢后,對(duì)應(yīng)的一級(jí)緩存會(huì)被清空。

下面以一個(gè)例子對(duì)MyBatis的一級(jí)緩存機(jī)制進(jìn)行演示和說明。首先開啟日志打印,然后關(guān)閉二級(jí)緩存,并將一級(jí)緩存作用范圍設(shè)置為SESSION,配置如下。

<settings>
    <setting name="logImpl" value="STDOUT_LOGGING" />
    <setting name="cacheEnabled" value="false"/>
    <setting name="localCacheScope" value="SESSION"/>
</settings>

映射接口如下所示。

public interface BookMapper {
    Book selectBookById(int id);
}

映射文件如下所示。

<?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.mybatis.learn.dao.BookMapper">
    <resultMap id="bookResultMap" type="com.mybatis.learn.entity.Book">
        <result property="bookName" column="b_name"/>
        <result property="bookPrice" column="b_price"/>
    </resultMap>
    <select id="selectBookById" resultMap="bookResultMap">
        SELECT
            b.id, 
            b.b_name, 
            b.b_price
        FROM book b
        WHERE b.id=#{id}
    </select>
</mapper>

MyBatis的執(zhí)行代碼如下所示。

public class MybatisTest {
    public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(Resources.getResourceAsStream(resource));
        SqlSession sqlSession = sqlSessionFactory.openSession(false);
        BookMapper bookMapper = sqlSession.getMapper(BookMapper.class);
        System.out.println(bookMapper.selectBookById(1));
        System.out.println(bookMapper.selectBookById(1));
        System.out.println(bookMapper.selectBookById(1));
    }
}

在執(zhí)行代碼中,連續(xù)執(zhí)行了三次查詢操作,看一下日志打印,如下所示。

一級(jí)緩存展示-連續(xù)三次查詢操作

可以知道,只有第一次查詢時(shí)和數(shù)據(jù)庫(kù)進(jìn)行了交互,后面兩次查詢均是從一級(jí)緩存中查詢的數(shù)據(jù)?,F(xiàn)在往映射接口和映射文件中加入更改數(shù)據(jù)的邏輯,如下所示。

public interface BookMapper {
    Book selectBookById(int id);
    // 根據(jù)id更改圖書價(jià)格
    void updateBookPriceById(@Param("id") int id, @Param("bookPrice") float bookPrice);
}
<?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.mybatis.learn.dao.BookMapper">
    <resultMap id="bookResultMap" type="com.mybatis.learn.entity.Book">
        <result property="bookName" column="b_name"/>
        <result property="bookPrice" column="b_price"/>
    </resultMap>
    <select id="selectBookById" resultMap="bookResultMap">
        SELECT
            b.id, 
            b.b_name, 
            b.b_price
        FROM book b
        WHERE b.id=#{id}
    </select>
    <update id="updateBookPriceById">
        UPDATE book SET b_price=#{bookPrice}
        WHERE id=#{id}
    </update>
</mapper>

執(zhí)行的操作為先執(zhí)行一次查詢操作,然后執(zhí)行一次更新操作并提交事務(wù),最后再執(zhí)行一次查詢操作,執(zhí)行代碼如下所示。

public class MybatisTest {
    public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(Resources.getResourceAsStream(resource));
        SqlSession sqlSession = sqlSessionFactory.openSession(false);
        BookMapper bookMapper = sqlSession.getMapper(BookMapper.class);
        System.out.println(bookMapper.selectBookById(1));
        System.out.println("Change database.");
        bookMapper.updateBookPriceById(1, 22.5f);
        sqlSession.commit();
        System.out.println(bookMapper.selectBookById(1));
    }
}

執(zhí)行結(jié)果如下所示。

一級(jí)緩存展示-查一次改一次再查一次

通過上述結(jié)果可以知道,在執(zhí)行更新操作之后,再執(zhí)行查詢操作時(shí),是直接從數(shù)據(jù)庫(kù)查詢的數(shù)據(jù),并未使用一級(jí)緩存,即在一個(gè)會(huì)話中,對(duì)數(shù)據(jù)庫(kù)的,操作,均會(huì)使一級(jí)緩存失效。

現(xiàn)在在執(zhí)行代碼中創(chuàng)建兩個(gè)會(huì)話,先讓會(huì)話1執(zhí)行一次查詢操作,然后讓會(huì)話2執(zhí)行一次更新操作并提交事務(wù),最后讓會(huì)話1再執(zhí)行一次相同的查詢。執(zhí)行代碼如下所示。

public class MybatisTest {
    public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(Resources.getResourceAsStream(resource));
        SqlSession sqlSession1 = sqlSessionFactory.openSession(false);
        SqlSession sqlSession2 = sqlSessionFactory.openSession(false);
        BookMapper bookMapper1 = sqlSession1.getMapper(BookMapper.class);
        BookMapper bookMapper2 = sqlSession2.getMapper(BookMapper.class);
        System.out.println(bookMapper1.selectBookById(1));
        System.out.println("Change database.");
        bookMapper2.updateBookPriceById(1, 22.5f);
        sqlSession2.commit();
        System.out.println(bookMapper1.selectBookById(1));
    }
}

執(zhí)行結(jié)果如下所示。

一級(jí)緩存展示-會(huì)話1查詢會(huì)化2更新會(huì)話1再查詢

上述結(jié)果表明,會(huì)話1的第一次查詢是直接查詢的數(shù)據(jù)庫(kù),然后會(huì)話2執(zhí)行了一次更新操作并提交了事務(wù),此時(shí)數(shù)據(jù)庫(kù)中id為1的圖書的價(jià)格已經(jīng)變更為了22.5,緊接著會(huì)話1又做了一次查詢,但查詢結(jié)果中的圖書價(jià)格為20.5,說明會(huì)話1的第二次查詢是從緩存獲取的查詢結(jié)果。所以在這里可以知道,MyBatis中每個(gè)會(huì)話均會(huì)維護(hù)一份一級(jí)緩存,不同會(huì)話之間的一級(jí)緩存各不影響。

在本小節(jié)最后,對(duì)MyBatis的一級(jí)緩存機(jī)制做一個(gè)總結(jié),如下所示。

  • MyBatis的一級(jí)緩存默認(rèn)開啟,且默認(rèn)作用范圍為SESSION,即一級(jí)緩存在一個(gè)會(huì)話中生效,也可以通過配置將作用范圍設(shè)置為STATEMENT,讓一級(jí)緩存僅針對(duì)當(dāng)前執(zhí)行的SQL語句生效;
  • 在同一個(gè)會(huì)話中,執(zhí)行,操作會(huì)使本會(huì)話中的一級(jí)緩存失效;
  • 不同會(huì)話持有不同的一級(jí)緩存,本會(huì)話內(nèi)的操作不會(huì)影響其它會(huì)話內(nèi)的一級(jí)緩存。

二. 一級(jí)緩存源碼分析

本小節(jié)將對(duì)一級(jí)緩存對(duì)應(yīng)的MyBatis源碼進(jìn)行討論。

已知,禁用二級(jí)緩存的情況下,執(zhí)行查詢操作時(shí),調(diào)用鏈如下所示。

Mybaits-禁用二級(jí)緩存時(shí)查詢執(zhí)行鏈路圖

BaseExecutor中有兩個(gè)重載的query() 方法,下面先看第一個(gè)query() 方法的實(shí)現(xiàn),如下所示。

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, 
                       ResultHandler resultHandler) throws SQLException {
    // 獲取Sql語句
    BoundSql boundSql = ms.getBoundSql(parameter);
    // 生成CacheKey
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    // 調(diào)用重載的query()方法
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

在上述query() 方法中,先會(huì)在MappedStatement中獲取SQL語句,然后生成CacheKey,這個(gè)CacheKey實(shí)際就是本會(huì)話一級(jí)緩存中緩存的唯一標(biāo)識(shí),CacheKey類圖如下所示。

CacheKey類圖

CacheKey中的multiplierhashcode,checksum,countupdateList字段用于判斷CacheKey之間是否相等,這些字段會(huì)在CacheKey的構(gòu)造函數(shù)中進(jìn)行初始化,如下所示。

public CacheKey() {
    this.hashcode = DEFAULT_HASHCODE;
    this.multiplier = DEFAULT_MULTIPLIER;
    this.count = 0;
    this.updateList = new ArrayList<>();
}

同時(shí)hashcodechecksum,countupdateList字段會(huì)在CacheKeyupdate() 方法中被更新,如下所示。

public void update(Object object) {
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
    count++;
    checksum += baseHashCode;
    baseHashCode *= count;
    hashcode = multiplier * hashcode + baseHashCode;
    updateList.add(object);
}

主要邏輯就是基于update() 方法的入?yún)⒂?jì)算并更新hashcode,checksumcount的值,然后再將入?yún)⑻砑拥?strong>updateList集合中。同時(shí),在CacheKey重寫的equals() 方法中,只有當(dāng)hashcode相等,checksum相等,count相等,以及updateList集合中的元素也全都相等時(shí),才算做兩個(gè)CacheKey是相等。

回到上述的BaseExecutor中的query() 方法,在其中會(huì)調(diào)用createCacheKey() 方法生成CacheKey,其部分源碼如下所示。

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, 
                               RowBounds rowBounds, BoundSql boundSql) {
    // ......
    // 創(chuàng)建CacheKey
    CacheKey cacheKey = new CacheKey();
    // 基于MappedStatement的id更新CacheKey
    cacheKey.update(ms.getId());
    // 基于RowBounds的offset更新CacheKey
    cacheKey.update(rowBounds.getOffset());
    // 基于RowBounds的limit更新CacheKey
    cacheKey.update(rowBounds.getLimit());
    // 基于Sql語句更新CacheKey
    cacheKey.update(boundSql.getSql());
    // ......
    // 基于查詢參數(shù)更新CacheKey
    cacheKey.update(value);
    // ......
    // 基于Environment的id更新CacheKey
    cacheKey.update(configuration.getEnvironment().getId());
    return cacheKey; 
}

所以可以得出結(jié)論,判斷CacheKey是否相等的依據(jù)就是MappedStatement id + RowBounds offset + RowBounds limit + SQL + Parameter + Environment id相等。

獲取到CacheKey后,會(huì)調(diào)用BaseExecutor中重載的query() 方法,如下所示。

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, 
                         CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
        throw new ExecutorException("Executor was closed.");
    }
    // queryStack是BaseExecutor的成員變量
    // queryStack主要用于遞歸調(diào)用query()方法時(shí)防止一級(jí)緩存被清空
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
        clearLocalCache();
    }
    List<E> list;
    try {
        queryStack++;
        // 先從一級(jí)緩存中根據(jù)CacheKey命中查詢結(jié)果
        list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
        if (list != null) {
            // 處理存儲(chǔ)過程相關(guān)邏輯
            handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
        } else {
            // 未命中,則直接查數(shù)據(jù)庫(kù)
            list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
        }
    } finally {
        queryStack--;
    }
    if (queryStack == 0) {
        for (BaseExecutor.DeferredLoad deferredLoad : deferredLoads) {
            deferredLoad.load();
        }
        deferredLoads.clear();
        // 如果一級(jí)緩存作用范圍是STATEMENT時(shí),每次query()執(zhí)行完畢就需要清空一級(jí)緩存
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            clearLocalCache();
        }
    }
    return list;
}

上述query() 方法中,會(huì)先根據(jù)CacheKey去緩存中命中查詢結(jié)果,如果命中到查詢結(jié)果并且映射文件中CURD標(biāo)簽上的statementTypeCALLABLE,則會(huì)先在handleLocallyCachedOutputParameters() 方法中處理存儲(chǔ)過程相關(guān)邏輯然后再將命中的查詢結(jié)果返回,如果未命中到查詢結(jié)果,則會(huì)直接查詢數(shù)據(jù)庫(kù)。

上述query() 方法中還使用到了BaseExecutorqueryStack字段,主要防止一級(jí)緩存作用范圍是STATEMENT并且還存在遞歸調(diào)用query() 方法時(shí),在遞歸尚未終止時(shí)就將一級(jí)緩存刪除,如果不存在遞歸調(diào)用,那么一級(jí)緩存作用范圍是STATEMENT時(shí),每次查詢結(jié)束后,都會(huì)清空緩存。

下面看一下BaseExecutor中的一級(jí)緩存localCache,其實(shí)際是PerpetualCache,類圖如下所示。

PerpetualCache類圖

所以PerpetualCache的內(nèi)部主要是基于一個(gè)Map(實(shí)際為HashMap)用于數(shù)據(jù)存儲(chǔ)。

現(xiàn)在回到上面的BaseExecutorquery() 方法中,如果沒有在一級(jí)緩存中命中查詢結(jié)果,則會(huì)直接查詢數(shù)據(jù)庫(kù),queryFromDatabase() 方法如下所示。

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, 
                    ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
        // 調(diào)用doQuery()進(jìn)行查詢操作
        list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
        localCache.removeObject(key);
    }
    // 將查詢結(jié)果添加到一級(jí)緩存中
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
        localOutputParameterCache.putObject(key, parameter);
    }
    // 返回查詢結(jié)果
    return list;
}

queryFromDatabase() 方法中和一級(jí)緩存相關(guān)的邏輯就是在查詢完數(shù)據(jù)庫(kù)后,會(huì)將查詢結(jié)果以CacheKey作為唯一標(biāo)識(shí)緩存到一級(jí)緩存中。

MyBatis中如果是執(zhí)行,操作,并且在禁用二級(jí)緩存的情況下,均會(huì)調(diào)用到BaseExecutorupdate() 方法,如下所示。

@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource())
            .activity("executing an update").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    // 執(zhí)行操作前先清空緩存
    clearLocalCache();
    return doUpdate(ms, parameter);
}

所以MyBatis中的一級(jí)緩存在執(zhí)行了,操作后,會(huì)被清空即失效。

最后,一級(jí)緩存的使用流程可以用下圖進(jìn)行概括。

Mybatis-一級(jí)緩存查詢執(zhí)行總結(jié)圖

三. 二級(jí)緩存機(jī)制展示

MyBatis的一級(jí)緩存僅在一個(gè)會(huì)話中被共享,會(huì)話之間的一級(jí)緩存互不影響,而MyBatis的二級(jí)緩存可以被多個(gè)會(huì)話共享,本小節(jié)將結(jié)合例子,對(duì)MyBatis中的二級(jí)緩存的使用機(jī)制進(jìn)行分析。要使用二級(jí)緩存,需要對(duì)MyBatis配置文件進(jìn)行更改以開啟二級(jí)緩存,如下所示。

<settings>
    <setting name="logImpl" value="STDOUT_LOGGING" />
    <setting name="cacheEnabled" value="true"/>
    <setting name="localCacheScope" value="STATEMENT"/>
</settings>

上述配置文件中還將一級(jí)緩存的作用范圍設(shè)置為了STATEMENT,目的是為了在例子中屏蔽一級(jí)緩存對(duì)查詢結(jié)果的干擾。映射接口如下所示。

public interface BookMapper {
    Book selectBookById(int id);
    void updateBookPriceById(@Param("id") int id, @Param("bookPrice") float bookPrice);
}

要使用二級(jí)緩存,還需要在映射文件中加入二級(jí)緩存相關(guān)的設(shè)置,如下所示。

<?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.mybatis.learn.dao.BookMapper">
    <!-- 二級(jí)緩存相關(guān)設(shè)置 -->
    <cache eviction="LRU"
           type="org.apache.ibatis.cache.impl.PerpetualCache"
           flushInterval="600000"
           size="1024"
           readOnly="true"
           blocking="false"/>
    <resultMap id="bookResultMap" type="com.mybatis.learn.entity.Book">
        <result property="bookName" column="b_name"/>
        <result property="bookPrice" column="b_price"/>
    </resultMap>
    <select id="selectBookById" resultMap="bookResultMap">
        SELECT
            b.id, 
            b.b_name, 
            b.b_price
        FROM book b
        WHERE b.id=#{id}
    </select>
    <update id="updateBookPriceById">
        UPDATE book SET b_price=#{bookPrice}
        WHERE id=#{id}
    </update>
</mapper>

二級(jí)緩存相關(guān)設(shè)置的每一項(xiàng)的含義,會(huì)在本小節(jié)末尾進(jìn)行說明。

1. 場(chǎng)景一

場(chǎng)景一:創(chuàng)建兩個(gè)會(huì)話,會(huì)話1以相同SQL語句連續(xù)執(zhí)行兩次查詢,會(huì)話2以相同SQL語句執(zhí)行一次查詢。執(zhí)行代碼如下所示。

public class MybatisTest {
    public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(Resources.getResourceAsStream(resource));
        SqlSession sqlSession1 = sqlSessionFactory.openSession(false);
        SqlSession sqlSession2 = sqlSessionFactory.openSession(false);
        BookMapper bookMapper1 = sqlSession1.getMapper(BookMapper.class);
        BookMapper bookMapper2 = sqlSession2.getMapper(BookMapper.class);
        System.out.println(bookMapper1.selectBookById(1));
        System.out.println(bookMapper1.selectBookById(1));
        System.out.println(bookMapper2.selectBookById(1));
    }
}

執(zhí)行結(jié)果如下所示。

二級(jí)緩存展示-場(chǎng)景1

MyBatis中的二級(jí)緩存開啟時(shí),每次查詢會(huì)先去二級(jí)緩存中命中查詢結(jié)果,未命中時(shí)才會(huì)使用一級(jí)緩存以及直接去查詢數(shù)據(jù)庫(kù)。上述結(jié)果截圖表明,場(chǎng)景一中,SQL語句相同時(shí),無論是同一會(huì)話的連續(xù)兩次查詢還是另一會(huì)話的一次查詢,均是查詢的數(shù)據(jù)庫(kù),仿佛二級(jí)緩存沒有生效,實(shí)際上,將查詢結(jié)果緩存到二級(jí)緩存中需要事務(wù)提交,場(chǎng)景一中并沒有事務(wù)提交,所以二級(jí)緩存中是沒有內(nèi)容的,最終導(dǎo)致三次查詢均是直接查詢的數(shù)據(jù)庫(kù)。此外,如果是增刪改操作,只要沒有事務(wù)提交,那么就不會(huì)影響二級(jí)緩存。

2. 場(chǎng)景二

場(chǎng)景二:創(chuàng)建兩個(gè)會(huì)話,會(huì)話1執(zhí)行一次查詢并提交事務(wù),然后會(huì)話1以相同SQL語句再執(zhí)行一次查詢,接著會(huì)話2以相同SQL語句執(zhí)行一次查詢。執(zhí)行代碼如下所示。

public class MybatisTest {
    public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(Resources.getResourceAsStream(resource));
        SqlSession sqlSession1 = sqlSessionFactory.openSession(false);
        SqlSession sqlSession2 = sqlSessionFactory.openSession(false);
        BookMapper bookMapper1 = sqlSession1.getMapper(BookMapper.class);
        BookMapper bookMapper2 = sqlSession2.getMapper(BookMapper.class);
        System.out.println(bookMapper1.selectBookById(1));
        sqlSession1.commit();
        System.out.println(bookMapper1.selectBookById(1));
        System.out.println(bookMapper2.selectBookById(1));
    }
}

執(zhí)行結(jié)果如下所示。

二級(jí)緩存展示-場(chǎng)景2

場(chǎng)景二中第一次查詢后提交了事務(wù),此時(shí)將查詢結(jié)果緩存到了二級(jí)緩存,所以后續(xù)的查詢?nèi)吭诙?jí)緩存中命中了查詢結(jié)果。

3. 場(chǎng)景三

場(chǎng)景三:創(chuàng)建兩個(gè)會(huì)話,會(huì)話1執(zhí)行一次查詢并提交事務(wù),然后會(huì)話2執(zhí)行一次更新并提交事務(wù),接著會(huì)話1再執(zhí)行一次相同的查詢。執(zhí)行代碼如下所示。

public class MybatisTest {
    public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(Resources.getResourceAsStream(resource));
	// 將事務(wù)隔離級(jí)別設(shè)置為讀已提交
        SqlSession sqlSession1 = sqlSessionFactory.openSession(
			TransactionIsolationLevel.READ_COMMITTED);
        SqlSession sqlSession2 = sqlSessionFactory.openSession(
			TransactionIsolationLevel.READ_COMMITTED);
        BookMapper bookMapper1 = sqlSession1.getMapper(BookMapper.class);
        BookMapper bookMapper2 = sqlSession2.getMapper(BookMapper.class);
        System.out.println(bookMapper1.selectBookById(1));
        sqlSession1.commit();
        System.out.println("Change database.");
        bookMapper2.updateBookPriceById(1, 20.5f);
        sqlSession2.commit();
        System.out.println(bookMapper1.selectBookById(1));
    }
}

執(zhí)行結(jié)果如下所示。

二級(jí)緩存展示-場(chǎng)景3

場(chǎng)景三的執(zhí)行結(jié)果表明,執(zhí)行更新操作并且提交事務(wù)后,會(huì)清空二級(jí)緩存,執(zhí)行新增和刪除操作也是同理。

4. 場(chǎng)景四

場(chǎng)景四:創(chuàng)建兩個(gè)會(huì)話,創(chuàng)建兩張表,會(huì)話1首先執(zhí)行一次多表查詢并提交事務(wù),然后會(huì)話2執(zhí)行一次更新操作以更新表2的數(shù)據(jù)并提交事務(wù),接著會(huì)話1再執(zhí)行一次相同的多表查詢。創(chuàng)表語句如下所示。

CREATE TABLE book(
    id INT(11) PRIMARY KEY AUTO_INCREMENT,
    b_name VARCHAR(255) NOT NULL,
    b_price FLOAT NOT NULL,
    bs_id INT(11) NOT NULL,
    FOREIGN KEY book(bs_id) REFERENCES bookstore(id)
);
CREATE TABLE bookstore(
    id INT(11) PRIMARY KEY AUTO_INCREMENT,
    bs_name VARCHAR(255) NOT NULL
)

book表和bookstore表中添加如下數(shù)據(jù)。

INSERT INTO book (b_name, b_price, bs_id) VALUES ("Math", 20.5, 1);
INSERT INTO book (b_name, b_price, bs_id) VALUES ("English", 21.5, 1);
INSERT INTO book (b_name, b_price, bs_id) VALUES ("Water Margin", 30.5, 2);
INSERT INTO bookstore (bs_name) VALUES ("XinHua");
INSERT INTO bookstore (bs_name) VALUES ("SanYou")

創(chuàng)建BookStore類,如下所示。

@Data
public class BookStore {
    private String id;
    private String bookStoreName;
}

創(chuàng)建BookDetail類,如下所示。

@Data
public class BookDetail {
    private long id;
    private String bookName;
    private float bookPrice;
    private BookStore bookStore;
}

BookMapper映射接口添加selectBookDetailById() 方法,如下所示。

public interface BookMapper {
    Book selectBookById(int id);
    void updateBookPriceById(@Param("id") int id, @Param("bookPrice") float bookPrice);
    BookDetail selectBookDetailById(int id);
}

BookMapper.xml映射文件如下所示。

<?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.mybatis.learn.dao.BookMapper">
    <cache eviction="LRU"
           type="org.apache.ibatis.cache.impl.PerpetualCache"
           flushInterval="600000"
           size="1024"
           readOnly="true"
           blocking="false"/>
    <resultMap id="bookResultMap" type="com.mybatis.learn.entity.Book">
        <result property="bookName" column="b_name"/>
        <result property="bookPrice" column="b_price"/>
    </resultMap>
    <resultMap id="bookDetailResultMap" type="com.mybatis.learn.entity.BookDetail">
        <id property="id" column="id"/>
        <result property="bookName" column="b_name"/>
        <result property="bookPrice" column="b_price"/>
        <association property="bookStore">
            <id property="id" column="id"/>
            <result property="bookStoreName" column="bs_name"/>
        </association>
    </resultMap>
    <select id="selectBookById" resultMap="bookResultMap">
        SELECT
            b.id, 
            b.b_name, 
            b.b_price
        FROM book b
        WHERE b.id=#{id}
    </select>
    <update id="updateBookPriceById">
        UPDATE book SET b_price=#{bookPrice}
        WHERE id=#{id}
    </update>
    <select id="selectBookDetailById" resultMap="bookDetailResultMap">
        SELECT
            b.id, 
            b.b_name, 
            b.b_price, 
            bs.id, 
            bs.bs_name
        FROM book b, bookstore bs
        WHERE b.id=#{id}
            AND b.bs_id = bs.id
    </select>
</mapper>

還需要添加BookStoreMapper映射接口,如下所示。

public interface BookStoreMapper {
    void updateBookPriceById(@Param("id") int id, @Param("bookStoreName") String bookStoreName);
}

還需要添加BookStoreMapper.xml映射文件,如下所示。

<?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.mybatis.learn.dao.BookStoreMapper">
    <cache eviction="LRU"
           type="org.apache.ibatis.cache.impl.PerpetualCache"
           flushInterval="600000"
           size="1024"
           readOnly="true"
           blocking="false"/>
    <update id="updateBookPriceById">
        UPDATE bookstore SET bs_name=#{bookStoreName}
        WHERE id=#{id}
    </update>
</mapper>

進(jìn)行完上述更改之后,進(jìn)行場(chǎng)景四的測(cè)試,執(zhí)行代碼如下所示。

public class MybatisTest {
    public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(Resources.getResourceAsStream(resource));
	// 將事務(wù)隔離級(jí)別設(shè)置為讀已提交
        SqlSession sqlSession1 = sqlSessionFactory.openSession(
                TransactionIsolationLevel.READ_COMMITTED);
        SqlSession sqlSession2 = sqlSessionFactory.openSession(
                TransactionIsolationLevel.READ_COMMITTED);
        BookMapper bookMapper1 = sqlSession1.getMapper(BookMapper.class);
        BookStoreMapper bookStoreMapper = sqlSession2.getMapper(BookStoreMapper.class);
        System.out.println(bookMapper1.selectBookDetailById(1));
        sqlSession1.commit();
        System.out.println("Change database.");
        bookStoreMapper.updateBookStoreById(1, "ShuXiang");
        sqlSession2.commit();
        System.out.println(bookMapper1.selectBookDetailById(1));
    }
}

執(zhí)行結(jié)果如下所示。

二級(jí)緩存展示-場(chǎng)景4

會(huì)話1第一次執(zhí)行多表查詢并提交事務(wù)時(shí),將查詢結(jié)果緩存到了二級(jí)緩存中,然后會(huì)話2對(duì)bookstore表執(zhí)行了更新操作并提交了事務(wù),但是最后會(huì)話1第二次執(zhí)行相同的多表查詢時(shí),卻從二級(jí)緩存中命中了查詢結(jié)果,最終導(dǎo)致查詢出來了臟數(shù)據(jù)。

實(shí)際上,二級(jí)緩存的作用范圍是同一命名空間下的多個(gè)會(huì)話共享,這里的命名空間就是映射文件的namespace,可以理解為每一個(gè)映射文件持有一份二級(jí)緩存,所有會(huì)話在這個(gè)映射文件中的所有操作,都會(huì)共享這個(gè)二級(jí)緩存。所以場(chǎng)景四的例子中,會(huì)話2對(duì)bookstore表執(zhí)行更新操作并提交事務(wù)時(shí),清空的是BookStoreMapper.xml持有的二級(jí)緩存,BookMapper.xml持有的二級(jí)緩存沒有感知到bookstore表的數(shù)據(jù)發(fā)生了變化,最終導(dǎo)致會(huì)話1第二次執(zhí)行相同的多表查詢時(shí)從二級(jí)緩存中命中了臟數(shù)據(jù)。

5. 場(chǎng)景五

場(chǎng)景五:執(zhí)行的操作和場(chǎng)景四一致,但是在BookStoreMapper.xml文件中進(jìn)行如下更改。

<?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.mybatis.learn.dao.BookStoreMapper">
    <cache-ref namespace="com.mybatis.learn.dao.BookMapper"/>
    <update id="updateBookStoreById">
        UPDATE bookstore SET bs_name=#{bookStoreName}
        WHERE id=#{id}
    </update>
</mapper>

執(zhí)行代碼如下所示。

public class MybatisTest {
    public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(Resources.getResourceAsStream(resource));
	// 將事務(wù)隔離級(jí)別設(shè)置為讀已提交
        SqlSession sqlSession1 = sqlSessionFactory.openSession(
                TransactionIsolationLevel.READ_COMMITTED);
        SqlSession sqlSession2 = sqlSessionFactory.openSession(
                TransactionIsolationLevel.READ_COMMITTED);
        BookMapper bookMapper1 = sqlSession1.getMapper(BookMapper.class);
        BookStoreMapper bookStoreMapper = sqlSession2.getMapper(BookStoreMapper.class);
        System.out.println(bookMapper1.selectBookDetailById(1));
        sqlSession1.commit();
        System.out.println("Change database.");
        bookStoreMapper.updateBookStoreById(1, "ShuXiang");
        sqlSession2.commit();
        System.out.println(bookMapper1.selectBookDetailById(1));
    }
}

執(zhí)行結(jié)果如下所示。

二級(jí)緩存展示-場(chǎng)景5

BookStoreMapper.xml中使用<cache-ref>標(biāo)簽引用了命名空間為com.mybatis.learn.dao.BookMapper的映射文件使用的二級(jí)緩存,因此相當(dāng)于BookMapper.xml映射文件與BookStoreMapper.xml映射文件持有同一份二級(jí)緩存,會(huì)話2在BookStoreMapper.xml映射文件中執(zhí)行更新操作并提交事務(wù)后,會(huì)導(dǎo)致二級(jí)緩存被清空,從而會(huì)話1第二次執(zhí)行相同的多表查詢時(shí)會(huì)從數(shù)據(jù)庫(kù)查詢數(shù)據(jù)。

現(xiàn)在對(duì)MyBatis的二級(jí)緩存機(jī)制進(jìn)行一個(gè)總結(jié),如下所示。

  • MyBatis中的二級(jí)緩存默認(rèn)開啟,可以在MyBatis配置文件中的<settings>中添加<setting name="cacheEnabled" value="false"/>將二級(jí)緩存關(guān)閉;
  • MyBatis中的二級(jí)緩存作用范圍是同一命名空間下的多個(gè)會(huì)話共享,這里的命名空間就是映射文件的namespace,即不同會(huì)話使用同一映射文件中的SQL語句對(duì)數(shù)據(jù)庫(kù)執(zhí)行操作并提交事務(wù)后,均會(huì)影響這個(gè)映射文件持有的二級(jí)緩存;
  • MyBatis中執(zhí)行查詢操作后,需要提交事務(wù)才能將查詢結(jié)果緩存到二級(jí)緩存中;
  • MyBatis中執(zhí)行增,刪或改操作并提交事務(wù)后,會(huì)清空對(duì)應(yīng)的二級(jí)緩存;
  • MyBatis中需要在映射文件中添加<cache>標(biāo)簽來為映射文件配置二級(jí)緩存,也可以在映射文件中添加<cache-ref>標(biāo)簽來引用其它映射文件的二級(jí)緩存以達(dá)到多個(gè)映射文件持有同一份二級(jí)緩存的效果。

最后,對(duì)<cache>標(biāo)簽和<cache-ref>標(biāo)簽進(jìn)行說明。

<cache>標(biāo)簽如下所示。

屬性含義默認(rèn)值
eviction緩存淘汰策略。LRU表示最近使用頻次最少的優(yōu)先被淘汰;FIFO表示先被緩存的會(huì)先被淘汰;SOFT表示基于軟引用規(guī)則來淘汰;WEAK表示基于弱引用規(guī)則來淘汰LRU
flushInterval緩存刷新間隔。單位毫秒空,表示永不過期
type緩存的類型PerpetualCache
size最多緩存的對(duì)象個(gè)數(shù)1024
blocking緩存未命中時(shí)是否阻塞false
readOnly緩存中的對(duì)象是否只讀。配置為true時(shí),表示緩存對(duì)象只讀,命中緩存時(shí)會(huì)直接將緩存的對(duì)象返回,性能更快,但是線程不安全;配置為false時(shí),表示緩存對(duì)象可讀寫,命中緩存時(shí)會(huì)將緩存的對(duì)象克隆然后返回克隆的對(duì)象,性能更慢,但是線程安全false

<cache-ref>標(biāo)簽如下所示。

屬性含義
namespace其它映射文件的命名空間,設(shè)置之后則當(dāng)前映射文件將和其它映射文件將持有同一份二級(jí)緩存

四. 二級(jí)緩存的創(chuàng)建

詳解MyBatis加載映射文件和動(dòng)態(tài)代理中已經(jīng)知道,XMLMapperBuilderconfigurationElement() 方法會(huì)解析映射文件的內(nèi)容并豐富到Configuration中,但在詳解MyBatis加載映射文件和動(dòng)態(tài)代理中并未對(duì)解析映射文件的<cache>標(biāo)簽和<cache-ref>標(biāo)簽進(jìn)行說明,因此本小節(jié)將對(duì)這部分內(nèi)容進(jìn)行補(bǔ)充。

configurationElement() 方法如下所示。

private void configurationElement(XNode context) {
    try {
        String namespace = context.getStringAttribute("namespace");
        if (namespace == null || namespace.isEmpty()) {
            throw new BuilderException("Mapper's namespace cannot be empty");
        }
        builderAssistant.setCurrentNamespace(namespace);
	// 解析<cache-ref>標(biāo)簽
        cacheRefElement(context.evalNode("cache-ref"));
	// 解析<cache>標(biāo)簽
        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. The XML location is '" 
                + resource + "'. Cause: " + e, e);
    }
}

configurationElement() 方法中會(huì)先解析<cache-ref>標(biāo)簽,然后再解析<cache>標(biāo)簽,因此在這里先進(jìn)行一個(gè)推測(cè):如果映射文件中同時(shí)存在<cache-ref>和<cache>標(biāo)簽,那么<cache>標(biāo)簽配置的二級(jí)緩存會(huì)覆蓋<cache-ref>引用的二級(jí)緩存。

下面先分析<cache>標(biāo)簽的解析,cacheElement() 方法如下所示。

private void cacheElement(XNode context) {
    if (context != null) {
        // 獲取<cache>標(biāo)簽的type屬性值
        String type = context.getStringAttribute("type", "PERPETUAL");
        Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
        // 獲取<cache>標(biāo)簽的eviction屬性值
        String eviction = context.getStringAttribute("eviction", "LRU");
        Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
        // 獲取<cache>標(biāo)簽的flushInterval屬性值
        Long flushInterval = context.getLongAttribute("flushInterval");
        // 獲取<cache>標(biāo)簽的size屬性值
        Integer size = context.getIntAttribute("size");
        // 獲取<cache>標(biāo)簽的readOnly屬性值并取反
        boolean readWrite = !context.getBooleanAttribute("readOnly", false);
        // 獲取<cache>標(biāo)簽的blocking屬性值
        boolean blocking = context.getBooleanAttribute("blocking", false);
        Properties props = context.getChildrenAsProperties();
        builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
}

單步跟蹤cacheElement() 方法,每個(gè)屬性解析出來的內(nèi)容可以參照下圖。

單步跟蹤解析cache標(biāo)簽

Cache的實(shí)際創(chuàng)建是在MapperBuilderAssistantuseNewCache() 方法中,實(shí)現(xiàn)如下所示。

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;
}

MapperBuilderAssistantuseNewCache() 方法中會(huì)先創(chuàng)建CacheBuilder,然后調(diào)用CacheBuilderbuild() 方法構(gòu)建CacheCacheBuilder類圖如下所示。

CacheBuilder類圖

CacheBuilder的構(gòu)造函數(shù)如下所示。

public CacheBuilder(String id) {
    this.id = id;
    this.decorators = new ArrayList<>();
}

所以可以知道,CacheBuilderid字段實(shí)際就是當(dāng)前映射文件的namespace,其實(shí)到這里已經(jīng)大致可以猜到,CacheBuilder構(gòu)建出來的二級(jí)緩存CacheConfiguration中的唯一標(biāo)識(shí)就是映射文件的namespace。此外,CacheBuilder中的implementationPerpetualCacheClass對(duì)象,decorators集合中包含有LruCacheClass對(duì)象。下面看一下CacheBuilderbuild() 方法,如下所示。

public Cache build() {
    setDefaultImplementations();
    // 創(chuàng)建PerpetualCache,作為基礎(chǔ)Cache對(duì)象
    Cache cache = newBaseCacheInstance(implementation, id);
    setCacheProperties(cache);
    if (PerpetualCache.class.equals(cache.getClass())) {
        // 為基礎(chǔ)Cache對(duì)象添加緩存淘汰策略相關(guān)的裝飾器
        for (Class<? extends Cache> decorator : decorators) {
            cache = newCacheDecoratorInstance(decorator, cache);
            setCacheProperties(cache);
        }
        // 繼續(xù)添加裝飾器
        cache = setStandardDecorators(cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
        cache = new LoggingCache(cache);
    }
    return cache;
}

CacheBuilderbuild() 方法首先會(huì)創(chuàng)建PerpetualCache對(duì)象,作為基礎(chǔ)緩存對(duì)象,然后還會(huì)為基礎(chǔ)緩存對(duì)象根據(jù)緩存淘汰策略添加對(duì)應(yīng)的裝飾器,比如<cache>標(biāo)簽中eviction屬性值為LRU,那么對(duì)應(yīng)的裝飾器為LruCache,根據(jù)eviction屬性值的不同,對(duì)應(yīng)的裝飾器就不同,下圖是MyBatis為緩存淘汰策略提供的所有裝飾器。

MyBatis提供的緩存裝飾器

CacheBuilderbuild() 方法中,為PerpetualCache添加完緩存淘汰策略添裝飾器后,還會(huì)繼續(xù)添加標(biāo)準(zhǔn)裝飾器,MyBatis中定義的標(biāo)準(zhǔn)裝飾器有ScheduledCache,SerializedCache,LoggingCache,SynchronizedCacheBlockingCache,含義如下表所示。

裝飾器含義
ScheduledCache提供緩存定時(shí)刷新功能,<cache>標(biāo)簽設(shè)置了flushInterval屬性值時(shí)會(huì)添加該裝飾器
SerializedCache提供緩存序列化功能,<cache>標(biāo)簽的readOnly屬性設(shè)置為false時(shí)會(huì)添加該裝飾器
LoggingCache提供日志功能,默認(rèn)會(huì)添加該裝飾器
SynchronizedCache提供同步功能,默認(rèn)會(huì)添加該裝飾器
BlockingCache提供阻塞功能,<cache>標(biāo)簽的blocking屬性設(shè)置為true時(shí)會(huì)添加該裝飾器

如下是一個(gè)<cache>標(biāo)簽的示例。

<cache eviction="LRU"
       type="org.apache.ibatis.cache.impl.PerpetualCache"
       flushInterval="600000"
       size="1024"
       readOnly="false"
       blocking="true"/>

那么生成的二級(jí)緩存對(duì)象如下所示。

生成的二級(jí)緩存對(duì)象

整個(gè)裝飾鏈如下圖所示。

Mybatis-二級(jí)緩存裝飾鏈?zhǔn)疽鈭D

現(xiàn)在回到MapperBuilderAssistantuseNewCache() 方法,構(gòu)建好二級(jí)緩存對(duì)象之后,會(huì)將其添加到Configuration中,ConfigurationaddCache() 方法如下所示。

public void addCache(Cache cache) {
    caches.put(cache.getId(), cache);
}

這里就印證了前面的猜想,即二級(jí)緩存CacheConfiguration中的唯一標(biāo)識(shí)就是映射文件的namespace

現(xiàn)在再分析一下XMLMapperBuilder中的configurationElement() 方法對(duì)<cache-ref>標(biāo)簽的解析。cacheRefElement() 方法如下所示。

private void cacheRefElement(XNode context) {
    if (context != null) {
        // 在Configuration的cacheRefMap中將當(dāng)前映射文件命名空間與引用的映射文件命名空間建立映射關(guān)系
        configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
        CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
        try {
            // CacheRefResolver會(huì)將引用的映射文件的二級(jí)緩存從Configuration中獲取出來并賦值給MapperBuilderAssistant的currentCache
            cacheRefResolver.resolveCacheRef();
        } catch (IncompleteElementException e) {
            configuration.addIncompleteCacheRef(cacheRefResolver);
        }
    }
}

cacheRefElement() 方法會(huì)首先在ConfigurationcacheRefMap中將當(dāng)前映射文件命名空間與引用的映射文件命名空間建立映射關(guān)系,然后會(huì)通過CacheRefResolver將引用的映射文件的二級(jí)緩存從Configuration中獲取出來并賦值給MapperBuilderAssistantcurrentCachecurrentCache這個(gè)字段后續(xù)會(huì)在MapperBuilderAssistant構(gòu)建MappedStatement時(shí)傳遞給MappedStatement,以及如果映射文件中還存在<cache>標(biāo)簽,那么MapperBuilderAssistant會(huì)將<cache>標(biāo)簽配置的二級(jí)緩存重新賦值給currentCache以覆蓋<cache-ref>標(biāo)簽引用的二級(jí)緩存,所以映射文件中同時(shí)有<cache-ref>標(biāo)簽和<cache>標(biāo)簽時(shí),只有<cache>標(biāo)簽配置的二級(jí)緩存會(huì)生效。

五. 二級(jí)緩存的源碼分析

本小節(jié)將對(duì)二級(jí)緩存對(duì)應(yīng)的MyBatis源碼進(jìn)行討論。MyBatis中開啟二級(jí)緩存之后,執(zhí)行查詢操作時(shí),調(diào)用鏈如下所示。

Mybatis-開啟二級(jí)緩存時(shí)查詢執(zhí)行鏈路圖

CachingExecutor中有兩個(gè)重載的query() 方法,下面先看第一個(gè)query() 方法,如下所示。

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, 
        RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // 獲取Sql語句
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // 創(chuàng)建CacheKey
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

繼續(xù)看重載的query() 方法,如下所示。

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, 
              ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    // 從MappedStatement中將二級(jí)緩存獲取出來
    Cache cache = ms.getCache();
    if (cache != null) {
        // 清空二級(jí)緩存(如果需要的話)
        flushCacheIfRequired(ms);
        if (ms.isUseCache() && resultHandler == null) {
            // 處理存儲(chǔ)過程相關(guān)邏輯
            ensureNoOutParams(ms, boundSql);
            // 從二級(jí)緩存中根據(jù)CacheKey命中查詢結(jié)果
            List<E> list = (List<E>) tcm.getObject(cache, key);
            if (list == null) {
                // 未命中緩存,則查數(shù)據(jù)庫(kù)
                list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                // 將從數(shù)據(jù)庫(kù)查詢到的結(jié)果緩存到二級(jí)緩存中
                tcm.putObject(cache, key, list);
            }
            // 返回查詢結(jié)果
            return list;
        }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

上述query() 方法整體執(zhí)行流程比較簡(jiǎn)單,概括下來就是:

  • 先從緩存中命中查詢結(jié)果;
  • 命中到查詢結(jié)果則返回;
  • 未命中到查詢結(jié)果則直接查詢數(shù)據(jù)庫(kù)并把查詢結(jié)果緩存到二級(jí)緩存中。

但是從二級(jí)緩存中根據(jù)CacheKey命中查詢結(jié)果時(shí),并沒有直接通過CachegetObject() 方法,而是通過tcmgetObject() 方法,合理進(jìn)行推測(cè)的話,應(yīng)該就是tcm持有二級(jí)緩存的引用,當(dāng)需要從二級(jí)緩存中命中查詢結(jié)果時(shí),由tcm將請(qǐng)求轉(zhuǎn)發(fā)給二級(jí)緩存。

實(shí)際上,tcmCachingExecutor持有的TransactionalCacheManager對(duì)象,從二級(jí)緩存中命中查詢結(jié)果這一請(qǐng)求之所以需要通過TransactionalCacheManager轉(zhuǎn)發(fā)給二級(jí)緩存,是因?yàn)樾枰柚?strong>TransactionalCacheManager實(shí)現(xiàn)只有當(dāng)事務(wù)提交時(shí),二級(jí)緩存才會(huì)被更新這一功能。聯(lián)想到第三小節(jié)中的場(chǎng)景一和場(chǎng)景二的示例,將查詢結(jié)果緩存到二級(jí)緩存中需要事務(wù)提交這一功能,其實(shí)就是借助TransactionalCacheManager實(shí)現(xiàn)的,所以下面對(duì)TransactionalCacheManager進(jìn)行一個(gè)說明。首先TransactionalCacheManager的類圖如下所示。

TransactionalCacheManager類圖

TransactionalCacheManager中持有一個(gè)Map,該Map的鍵為Cache,值為TransactionalCache,即一個(gè)二級(jí)緩存對(duì)應(yīng)一個(gè)TransactionalCache。繼續(xù)看TransactionalCacheManagergetObject() 方法,如下所示。

public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
}
private TransactionalCache getTransactionalCache(Cache cache) {
    return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}

通過上述代碼可以知道,一個(gè)二級(jí)緩存對(duì)應(yīng)一個(gè)TransactionalCache,且TransactionalCache中持有這個(gè)二級(jí)緩存的引用,當(dāng)調(diào)用TransactionalCacheManagergetObject() 方法時(shí),TransactionalCacheManager會(huì)將調(diào)用請(qǐng)求轉(zhuǎn)發(fā)給TransactionalCache,下面分析一下TransactionalCache,類圖如下所示。

TransactionalCache類圖

繼續(xù)看TransactionalCachegetObject() 方法,如下所示。

@Override
public Object getObject(Object key) {
    // 在二級(jí)緩存中命中查詢結(jié)果
    Object object = delegate.getObject(key);
    if (object == null) {
        // 未命中則將CacheKey添加到entriesMissedInCache中
        // 用于統(tǒng)計(jì)命中率
        entriesMissedInCache.add(key);
    }
    if (clearOnCommit) {
        return null;
    } else {
        return object;
    }
}

到這里就可以知道了,在CachingExecutor中通過CacheKey命中查詢結(jié)果時(shí),步驟如下。

  • CachingExecutor將請(qǐng)求發(fā)送給TransactionalCacheManager;
  • TransactionalCacheManager將請(qǐng)求轉(zhuǎn)發(fā)給二級(jí)緩存對(duì)應(yīng)的TransactionalCache
  • 最后再由TransactionalCache將請(qǐng)求最終傳遞到二級(jí)緩存。

在上述getObject() 方法中,如果clearOnCommittrue,則無論是否在二級(jí)緩存中命中查詢結(jié)果,均返回null,那么clearOnCommit在什么地方會(huì)被置為true呢,其實(shí)就是在CachingExecutorflushCacheIfRequired() 方法中,這個(gè)方法在上面分析的query() 方法中會(huì)被調(diào)用到,看一下flushCacheIfRequired() 的實(shí)現(xiàn),如下所示。

private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    if (cache != null && ms.isFlushCacheRequired()) {
        tcm.clear(cache);
    }
}

調(diào)用TransactionalCacheManagerclear() 方法時(shí),最終會(huì)調(diào)用到TransactionalCacheclear() 方法,如下所示。

@Override
public void clear() {
    clearOnCommit = true;
    entriesToAddOnCommit.clear();
}

現(xiàn)在繼續(xù)分析為什么將查詢結(jié)果緩存到二級(jí)緩存中需要事務(wù)提交。從數(shù)據(jù)庫(kù)中查詢出來結(jié)果后,CachingExecutor會(huì)調(diào)用TransactionalCacheManagerputObject() 方法試圖將查詢結(jié)果緩存到二級(jí)緩存中,我們已經(jīng)知道,如果事務(wù)不提交,那么查詢結(jié)果是無法被緩存到二級(jí)緩存中,那么在事務(wù)提交之前,查詢結(jié)果肯定被暫存到了某個(gè)地方,為了搞清楚這部分邏輯,先看一下TransactionalCacheManagerputObject() 方法,如下所示。

public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
}

繼續(xù)看TransactionalCacheputObject() 方法,如下所示。

@Override
public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
}

到這里就搞明白了,在事務(wù)提交之前,查詢結(jié)果會(huì)被暫存TransactionalCacheentriesToAddOnCommit中。

下面繼續(xù)分析事務(wù)提交時(shí)如何將entriesToAddOnCommit暫存的查詢結(jié)果刷新到二級(jí)緩存中,DefaultSqlSessioncommit() 方法如下所示。

@Override
public void commit() {
    commit(false);
}
@Override
public void commit(boolean force) {
    try {
        executor.commit(isCommitOrRollbackRequired(force));
        dirty = false;
    } catch (Exception e) {
        throw ExceptionFactory.wrapException(
                "Error committing transaction. Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

DefaultSqlSessioncommit() 方法中會(huì)調(diào)用到CachingExecutorcommit() 方法,如下所示。

@Override
public void commit(boolean required) throws SQLException {
    delegate.commit(required);
    // 調(diào)用TransactionalCacheManager的commit()方法
    tcm.commit();
}

CachingExecutorcommit() 方法中,會(huì)調(diào)用TransactionalCacheManagercommit() 方法,如下所示。

public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
        // 調(diào)用TransactionalCache的commit()方法
        txCache.commit();
    }
}

繼續(xù)看TransactionalCachecommit() 方法,如下所示。

public void commit() {
    if (clearOnCommit) {
        delegate.clear();
    }
    flushPendingEntries();
    reset();
}
private void flushPendingEntries() {
    // 將entriesToAddOnCommit中暫存的查詢結(jié)果全部緩存到二級(jí)緩存中
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
        delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
        if (!entriesToAddOnCommit.containsKey(entry)) {
            delegate.putObject(entry, null);
        }
    }
}

至此可以知道,當(dāng)調(diào)用SqlSessioncommit() 方法時(shí),會(huì)一路傳遞到TransactionalCachecommit() 方法,最終調(diào)用TransactionalCacheflushPendingEntries() 方法將暫存的查詢結(jié)果全部刷到二級(jí)緩存中。

當(dāng)執(zhí)行,操作并提交事務(wù)時(shí),二級(jí)緩存會(huì)被清空,這是因?yàn)?strong>增,,操作最終會(huì)調(diào)用到CachingExecutorupdate() 方法,而update() 方法中又會(huì)調(diào)用flushCacheIfRequired() 方法,已經(jīng)知道在flushCacheIfRequired() 方法中如果所執(zhí)行的方法對(duì)應(yīng)的MappedStatementflushCacheRequired字段為true的話,則會(huì)最終將TransactionalCache中的clearOnCommit字段置為true,隨即在事務(wù)提交的時(shí)候,會(huì)將二級(jí)緩存清空。而加載映射文件時(shí),解析CURD標(biāo)簽為MappedStatement時(shí)有如下一行代碼。

boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);

即如果沒有在CURD標(biāo)簽中顯式的設(shè)置flushCache屬性,則會(huì)給flushCache字段一個(gè)默認(rèn)值,且默認(rèn)值為非查詢標(biāo)簽下默認(rèn)為true,所以到這里就可以知道,如果是,,操作,那么TransactionalCache中的clearOnCommit字段會(huì)被置為true,從而在提交事務(wù)時(shí)會(huì)在TransactionalCachecommit() 方法中將二級(jí)緩存清空。

到這里,二級(jí)緩存的源碼分析結(jié)束。二級(jí)緩存的使用流程可以用下圖進(jìn)行概括,如下所示。

Mybatis-二級(jí)緩存查詢執(zhí)行總結(jié)圖

總結(jié)

關(guān)于MyBatis的一級(jí)緩存,總結(jié)如下。

  • MyBatis的一級(jí)緩存默認(rèn)開啟,且默認(rèn)作用范圍為SESSION,即一級(jí)緩存在一個(gè)會(huì)話中生效,也可以通過配置將作用范圍設(shè)置為STATEMENT,讓一級(jí)緩存僅針對(duì)當(dāng)前執(zhí)行的SQL語句生效;
  • 在同一個(gè)會(huì)話中,執(zhí)行,,操作會(huì)使本會(huì)話中的一級(jí)緩存失效;
  • 不同會(huì)話持有不同的一級(jí)緩存,本會(huì)話內(nèi)的操作不會(huì)影響其它會(huì)話內(nèi)的一級(jí)緩存。

關(guān)于MyBatis的二級(jí)緩存,總結(jié)如下。

  • MyBatis中的二級(jí)緩存默認(rèn)開啟,可以在MyBatis配置文件中的<settings>中添加<setting name="cacheEnabled" value="false"/>將二級(jí)緩存關(guān)閉;
  • MyBatis中的二級(jí)緩存作用范圍是同一命名空間下的多個(gè)會(huì)話共享,這里的命名空間就是映射文件的namespace,即不同會(huì)話使用同一映射文件中的SQL語句對(duì)數(shù)據(jù)庫(kù)執(zhí)行操作并提交事務(wù)后,均會(huì)影響這個(gè)映射文件持有的二級(jí)緩存;
  • MyBatis中執(zhí)行查詢操作后,需要提交事務(wù)才能將查詢結(jié)果緩存到二級(jí)緩存中;
  • MyBatis中執(zhí)行增,刪或改操作并提交事務(wù)后,會(huì)清空對(duì)應(yīng)的二級(jí)緩存;
  • MyBatis中需要在映射文件中添加<cache>標(biāo)簽來為映射文件配置二級(jí)緩存,也可以在映射文件中添加<cache-ref>標(biāo)簽來引用其它映射文件的二級(jí)緩存以達(dá)到多個(gè)映射文件持有同一份二級(jí)緩存的效果。

到此這篇關(guān)于一文搞懂MyBatis一級(jí)緩存和二級(jí)緩存的文章就介紹到這了,更多相關(guān)MyBatis一級(jí)緩存和二級(jí)緩存內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論