MyBatis緩存使用及源碼分析

MyBatis緩存的使用以源碼分析

機(jī)緣巧合看到2018美團(tuán)技術(shù)年貨中介紹了一些MyBatis關(guān)于緩存的文章,正好上篇文章對MyBatis的使用以及源碼進(jìn)行了詳盡的分析,對于緩存的設(shè)計以及使用一筆帶過;本文將對MyBatis的緩存進(jìn)行補(bǔ)充說明。

一級緩存

一級緩存介紹

在應(yīng)用運(yùn)行的過程中,一次數(shù)據(jù)庫連接會話可能會多次執(zhí)行相同的SQL;而MyBatis針對此場景做了優(yōu)化,優(yōu)化的方案便是一級緩存;當(dāng)查詢相同的SQL,會優(yōu)先命中一級緩存,避免了對數(shù)據(jù)庫的多次訪問,提高了性能。執(zhí)行過程如圖所示:

一級緩存.png

(上圖來自:美團(tuán)技術(shù)年貨)

還記得上篇文章中介紹的SqlSession提供對數(shù)據(jù)庫的操作,而具體的職責(zé)是由Executor完成的,那么緩存的是在哪里完成的呢?

一級緩存類圖關(guān)系.png

當(dāng)用戶執(zhí)行查詢時,MyBatis根據(jù)當(dāng)前語句生成的MapperStatement,在localCache(BaseExecutor的成員變量)中查詢,如果命中則直接返回,否則查詢數(shù)據(jù)庫,并將結(jié)果緩存,最后將數(shù)據(jù)返回給用戶。

一級緩存有兩種使用選項,SESSION 或者 STATEMENT,默認(rèn)的級別是SESSION;SESSION表示再一次數(shù)據(jù)庫會話中執(zhí)行的所有語句共享一個緩存;而STATEMENT則可以理解為緩存只對當(dāng)前語句有效。配置方式,在MyBatis配置文件中添加如下:

<settings>
    <setting name="localCacheScope" value="SESSION"/>
</settings>
一級緩存實(shí)驗(yàn)一

我們用以下代碼分別對SESSION,STATEMENT做演示

//演示代碼
@Test
public void test1() {
    SqlSession sqlSession = sessionFactory.openSession(true);
    CustomerDao customerDao;
    try {
        customerDao = sqlSession.getMapper(CustomerDao.class);
        LOGGER.debug(customerDao.selectByPrimaryKey(1L));
        LOGGER.debug(customerDao.selectByPrimaryKey(1L));
        LOGGER.debug(customerDao.selectByPrimaryKey(1L));
    } finally {
        sqlSession.close();
    }
}

SESSION級別演示效果

DEBUG [main] - Created connection 525968792.
DEBUG [main] - ==>  Preparing: select id, optimistic, name, phone from customer where id = ? 
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <==      Total: 1
DEBUG [main] - Customer{id=1, optimistic=null, name='ce.sun', phone='null'}
DEBUG [main] - Customer{id=1, optimistic=null, name='ce.sun', phone='null'}
DEBUG [main] - Customer{id=1, optimistic=null, name='ce.sun', phone='null'}
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1f59a598]
DEBUG [main] - Returned connection 525968792 to pool.

我們發(fā)現(xiàn)只有第一次查詢從數(shù)據(jù)庫取得結(jié)果,后面的都使用了一級緩存

STATEMENT級別演示效果

DEBUG [main] - Created connection 525968792.
DEBUG [main] - ==>  Preparing: select id, optimistic, name, phone from customer where id = ? 
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <==      Total: 1
DEBUG [main] - Customer{id=1, optimistic=null, name='ce.sun', phone='null'}
DEBUG [main] - ==>  Preparing: select id, optimistic, name, phone from customer where id = ? 
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <==      Total: 1
DEBUG [main] - Customer{id=1, optimistic=null, name='ce.sun', phone='null'}
DEBUG [main] - ==>  Preparing: select id, optimistic, name, phone from customer where id = ? 
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <==      Total: 1
DEBUG [main] - Customer{id=1, optimistic=null, name='ce.sun', phone='null'}
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1f59a598]
DEBUG [main] - Returned connection 525968792 to pool.

當(dāng)我們將一級緩存的選項調(diào)整為STATEMENT時,發(fā)現(xiàn)三次都是從數(shù)據(jù)庫取得結(jié)果。

一級緩存實(shí)驗(yàn)二

使用SESSION一級緩存,當(dāng)我們在一次會話中對改數(shù)據(jù)進(jìn)行修改,會對緩存進(jìn)行清除么?

//演示代碼
@Test
public void test2() {
    SqlSession sqlSession = sessionFactory.openSession(true);
    CustomerDao customerDao;
    try {
        customerDao = sqlSession.getMapper(CustomerDao.class);
        Customer customer = customerDao.selectByPrimaryKey(1L);
        LOGGER.debug(customer);
        customer.setName("wang.er");
        customerDao.updateByPrimaryKey(customer);
        LOGGER.debug(customerDao.selectByPrimaryKey(1L));
    } finally {
        sqlSession.close();
    }
}

實(shí)驗(yàn)結(jié)果如下:

DEBUG [main] - Created connection 525968792.
DEBUG [main] - ==>  Preparing: select id, optimistic, name, phone from customer where id = ? 
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <==      Total: 1
DEBUG [main] - Customer{id=1, optimistic=null, name='wang.er', phone='null'}
DEBUG [main] - ==>  Preparing: update customer set optimistic = ?, name = ?, phone = ? where id = ? 
DEBUG [main] - ==> Parameters: null, wang.er(String), null, 1(Long)
DEBUG [main] - <==    Updates: 1
DEBUG [main] - ==>  Preparing: select id, optimistic, name, phone from customer where id = ? 
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <==      Total: 1
DEBUG [main] - Customer{id=1, optimistic=null, name='wang.er', phone='null'}
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1f59a598]
DEBUG [main] - Returned connection 525968792 to pool.

一級緩存失效,重新從數(shù)據(jù)庫獲取數(shù)據(jù)。

一級緩存實(shí)驗(yàn)三

驗(yàn)證一級緩存SESSION配置,是只在會話內(nèi)部共享;我們開啟兩個會話,一個會話查詢兩次,另一個會話在兩次查詢期間對查詢數(shù)據(jù)進(jìn)行修改,會有什么樣的結(jié)果呢?

@Test
public void test3() {
    SqlSession sqlSession1 = sessionFactory.openSession(true);
    SqlSession sqlSession2 = sessionFactory.openSession(true);
    try {
        CustomerDao customerDao1 = sqlSession1.getMapper(CustomerDao.class);
        CustomerDao customerDao2 = sqlSession2.getMapper(CustomerDao.class);
        LOGGER.info("會話一:" + customerDao1.selectByPrimaryKey(1L));
        LOGGER.info("會話一:" + customerDao1.selectByPrimaryKey(1L));
        Customer customer = new Customer();
        customer.setId(1L);
        customer.setName("wang.er");
        customerDao2.updateByPrimaryKey(customer);
        LOGGER.info("會話二:" + customerDao2.selectByPrimaryKey(1L));
        LOGGER.info("會話一:" + customerDao1.selectByPrimaryKey(1L));
    } finally {
        sqlSession1.close();
        sqlSession2.close();
    }
}

演示結(jié)果

DEBUG [main] - Created connection 525968792.
DEBUG [main] - ==>  Preparing: select id, optimistic, name, phone from customer where id = ? 
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <==      Total: 1
 INFO [main] - 會話一:Customer{id=1, optimistic=null, name='sun.ce', phone='null'}
 INFO [main] - 會話一:Customer{id=1, optimistic=null, name='sun.ce', phone='null'}
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 1431467659.
DEBUG [main] - ==>  Preparing: update customer set optimistic = ?, name = ?, phone = ? where id = ? 
DEBUG [main] - ==> Parameters: null, wang.er(String), null, 1(Long)
DEBUG [main] - <==    Updates: 1
DEBUG [main] - ==>  Preparing: select id, optimistic, name, phone from customer where id = ? 
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <==      Total: 1
 INFO [main] - 會話二:Customer{id=1, optimistic=null, name='wang.er', phone='null'}
 INFO [main] - 會話一:Customer{id=1, optimistic=null, name='sun.ce', phone='null'}
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1f59a598]
DEBUG [main] - Returned connection 525968792 to pool.
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5552768b]
DEBUG [main] - Returned connection 1431467659 to pool.

我們發(fā)現(xiàn),會話一的結(jié)果,沒有因?yàn)闀挾男薷亩淖?,還是使用了一級緩存的結(jié)果;發(fā)生了數(shù)據(jù)臟讀。

一級緩存源碼分析
一級緩存執(zhí)行流程.png

從上圖中我們能夠發(fā)現(xiàn),緩存的重點(diǎn)關(guān)注對象是Executor類,我們再來一起回顧一下Executor的繼承關(guān)系圖。

ExecutorType.png

我們先來看看Executor的抽象實(shí)現(xiàn)類BaseExecutor

//創(chuàng)建CacheKey
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLEx
ception {
 BoundSql boundSql = ms.getBoundSql(parameter);
 CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
 return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

具體CacheKey生成規(guī)則如下:

//CacheKey生成規(guī)則
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
//后面是update了sql中帶的參數(shù)
cacheKey.update(value);

以上代碼使用MappedStatement的id,SQL的offset、SQL的limit,sql,以及參數(shù)生成了CacheKey;我們緊接著看生成CacheKey之后的操作;

list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
//如果緩存結(jié)果不為空則處理緩存結(jié)果
if (list != null) {
    this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//否則從數(shù)據(jù)庫查詢
    list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

我們看到進(jìn)入query(ms, parameter, rowBounds, resultHandler, key, boundSql)方法后,會先取緩存結(jié)果,不存在則從數(shù)據(jù)庫中查詢。

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);

    List list;
    try {
        list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
        this.localCache.removeObject(key);
    }
    
    //將數(shù)據(jù)庫中查詢的結(jié)果緩存起來
    this.localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
        this.localOutputParameterCache.putObject(key, parameter);
    }

    return list;
}

驗(yàn)證完查詢的緩存流程,我們再來驗(yàn)證之前實(shí)驗(yàn)二中的更新清空緩存,代碼如下:

public int update(MappedStatement ms, Object parameter) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (this.closed) {
        throw new ExecutorException("Executor was closed.");
    } else {
        //每次更新之前會清空緩存
        this.clearLocalCache();
        return this.doUpdate(ms, parameter);
    }
}

我們發(fā)現(xiàn)每次更新之前,會先清空緩存,隨后在進(jìn)行更新操作。我們再來仔細(xì)看看Cache的具體實(shí)現(xiàn);它是Executor的一個成員變量

public abstract class BaseExecutor implements Executor {
    ....
    protected PerpetualCache localCache;
}

PerpetualCache 具體代碼如下:

public class PerpetualCache implements Cache {

    private Map<Object, Object> cache = new HashMap();
    
     public int getSize() {
        return this.cache.size();
    }
    //添加緩存對象
    public void putObject(Object key, Object value) {
        this.cache.put(key, value);
    }
    //獲取緩存對象
    public Object getObject(Object key) {
        return this.cache.get(key);
    }
    //移除緩存對象
    public Object removeObject(Object key) {
        return this.cache.remove(key);
    }
    //清空緩存
    public void clear() {
        this.cache.clear();
    }
}    
一級緩存總結(jié)
  • 一級緩存的生命周期跟SqlSession一樣
  • 一級緩存的實(shí)現(xiàn)比較簡單通過HashMap
  • 一級緩存的最大范圍是SESSION,但是在分布式或者多個SqlSession的情況下,有可能會出現(xiàn)數(shù)據(jù)臟讀, 建議使用STATEMENT

二級緩存

二級緩存介紹

我們通過一級緩存了解到,一級緩存的最大范圍是SESSION內(nèi)部,那么在多個SqlSeesion中,如何實(shí)現(xiàn)緩存呢?這就需要二級緩存了,回想Executor有兩個子類,BaseExecutor跟CachingExecutor;二級緩存就是通過CachingExecutor實(shí)現(xiàn)的,如下是二級緩存的工作原理。

二級緩存.png

二級緩存開啟后,同一個namespace下的所有SqlSession共享一個Cache;此時Sql的查詢流程是
二級緩存 > 一級緩存 > 數(shù)據(jù)庫

通過在配置文件中,新增如下配置可以打開二級緩存

<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>    

XML 文件中新增<Cache/>

<?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="xin.sunce.mybatis.dao.StudentDao">
  <cache/>

  <select id="getStudentById" parameterType="int" resultType="xin.sunce.mybatis.entity.Student">
    SELECT id,name,age FROM student WHERE id = #{id}
  </select>
</mapper>  
二級緩存實(shí)驗(yàn)一

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

//演示代碼
@Test
public void test1() {
    SqlSession sqlSession1 = sessionFactory.openSession(true);
    SqlSession sqlSession2 = sessionFactory.openSession(true);
    try {
        StudentDao studentDao1 = sqlSession1.getMapper(StudentDao.class);
        StudentDao studentDao2 = sqlSession2.getMapper(StudentDao.class);
        LOGGER.info(studentDao1.getStudentById(1));
        LOGGER.info(studentDao2.getStudentById(1));
    } finally {
        sqlSession1.close();
        sqlSession2.close();
    }
}

實(shí)驗(yàn)結(jié)果:

DEBUG [main] - Created connection 1552999801.
DEBUG [main] - ==>  Preparing: SELECT id,name,age FROM student WHERE id = ? 
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <==      Total: 1
 INFO [main] - xin.sunce.mybatis.entity.Student@152aa092
DEBUG [main] - Cache Hit Ratio [xin.sunce.mybatis.dao.StudentDao]: 0.0
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 1324578393.
DEBUG [main] - ==>  Preparing: SELECT id,name,age FROM student WHERE id = ? 
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <==      Total: 1
 INFO [main] - xin.sunce.mybatis.entity.Student@37858383
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5c90e579]
DEBUG [main] - Returned connection 1552999801 to pool.
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4ef37659]
DEBUG [main] - Returned connection 1324578393 to pool.

我們發(fā)現(xiàn)沒有命中緩存,會話二重新從數(shù)據(jù)庫中讀取

二級緩存實(shí)驗(yàn)二

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

//演示代碼
@Test
public void test2() {
    SqlSession sqlSession1 = sessionFactory.openSession(true);
    SqlSession sqlSession2 = sessionFactory.openSession(true);
    try {
        StudentDao studentDao1 = sqlSession1.getMapper(StudentDao.class);
        StudentDao studentDao2 = sqlSession2.getMapper(StudentDao.class);
        LOGGER.info(studentDao1.getStudentById(1));
        sqlSession1.commit();
        LOGGER.info(studentDao2.getStudentById(1));
    } finally {
        sqlSession1.close();
        sqlSession2.close();
    }
}

實(shí)驗(yàn)結(jié)果

DEBUG [main] - Created connection 1552999801.
DEBUG [main] - ==>  Preparing: SELECT id,name,age FROM student WHERE id = ? 
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <==      Total: 1
 INFO [main] - xin.sunce.mybatis.entity.Student@152aa092
DEBUG [main] - Cache Hit Ratio [xin.sunce.mybatis.dao.StudentDao]: 0.5
 INFO [main] - xin.sunce.mybatis.entity.Student@62e7f11d
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5c90e579]
DEBUG [main] - Returned connection 1552999801 to pool.

會話一事務(wù)提交以后,會話二查詢結(jié)果命中緩存

二級緩存實(shí)驗(yàn)三

其他會話更新,會不會清空緩存

//演示代碼:
@Test
public void test3() {
    SqlSession sqlSession1 = sessionFactory.openSession(true);
    SqlSession sqlSession2 = sessionFactory.openSession(true);
    SqlSession sqlSession3 = sessionFactory.openSession(true);
    try {
        StudentDao studentDao1 = sqlSession1.getMapper(StudentDao.class);
        StudentDao studentDao2 = sqlSession2.getMapper(StudentDao.class);
        StudentDao studentDao3 = sqlSession3.getMapper(StudentDao.class);
        LOGGER.info("會話一:" + studentDao1.getStudentById(1));
        sqlSession1.commit();
        LOGGER.info("會話二,studentDao3更新之前:" + studentDao2.getStudentById(1));
        studentDao3.updateStudentName("測測", 1);
        sqlSession3.commit();
        LOGGER.info("會話二,studentDao3更新之后:" + studentDao2.getStudentById(1));
    } finally {
        sqlSession1.close();
        sqlSession2.close();
        sqlSession3.close();
    }
}

演示結(jié)果:

DEBUG [main] - Created connection 1479909053.
DEBUG [main] - ==>  Preparing: SELECT id,name,age FROM student WHERE id = ? 
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <==      Total: 1
 INFO [main] - 會話一:Student{id=1, name='試試', age=16}
DEBUG [main] - Cache Hit Ratio [xin.sunce.mybatis.dao.StudentDao]: 0.5
 INFO [main] - 會話二,studentDao3更新之前:Student{id=1, name='試試', age=16}
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 1061448687.
DEBUG [main] - ==>  Preparing: UPDATE student SET name = ? WHERE id = ? 
DEBUG [main] - ==> Parameters: 測測(String), 1(Integer)
DEBUG [main] - <==    Updates: 1
DEBUG [main] - Cache Hit Ratio [xin.sunce.mybatis.dao.StudentDao]: 0.3333333333333333
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 1533330615.
DEBUG [main] - ==>  Preparing: SELECT id,name,age FROM student WHERE id = ? 
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <==      Total: 1
 INFO [main] - 會話二,studentDao3更新之后:Student{id=1, name='測測', age=16}
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@58359ebd]
DEBUG [main] - Returned connection 1479909053 to pool.
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5b64c4b7]
DEBUG [main] - Returned connection 1533330615 to pool.
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3f446bef]
DEBUG [main] - Returned connection 1061448687 to pool.

sqlSession3事務(wù)提交之后,sqlSession2沒有走Cache,而是通過數(shù)據(jù)庫查詢到結(jié)果。

二級緩存實(shí)驗(yàn)四

驗(yàn)證緩存范圍namespace,我們通常會為一個表建立一個namespace;一個namespace無法感知到另一個namespace的數(shù)據(jù)變化;關(guān)聯(lián)查詢學(xué)生所在的班級名稱,單獨(dú)修改班級名稱,緩存結(jié)果會怎樣呢?

//演示代碼
@Test
public void test4() {
    SqlSession sqlSession1 = sessionFactory.openSession(true);
    SqlSession sqlSession2 = sessionFactory.openSession(true);
    SqlSession sqlSession3 = sessionFactory.openSession(true);
    try {
        StudentDao studentDao1 = sqlSession1.getMapper(StudentDao.class);
        StudentDao studentDao2 = sqlSession2.getMapper(StudentDao.class);
        ClassDao classDao = sqlSession3.getMapper(ClassDao.class);
        LOGGER.info("會話一:" + studentDao1.getStudentByIdWithClassInfo(1));
        sqlSession1.commit();
        LOGGER.info("會話二,classDao更新之前:" + studentDao2.getStudentByIdWithClassInfo(1));
        classDao.updateClassName("測試一班", 1);
        sqlSession3.commit();
        LOGGER.info("會話二,classDao更新之后:" + studentDao2.getStudentByIdWithClassInfo(1));
    } finally {
        sqlSession1.close();
        sqlSession2.close();
        sqlSession3.close();
    }
}

演示結(jié)果:

DEBUG [main] - Created connection 758119607.
DEBUG [main] - ==>  Preparing: SELECT s.id,s.name,s.age,class.name as className FROM classroom c JOIN student s ON c.student_id = s.id JOIN class ON c.class_id = class.id WHERE s.id = ?; 
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <==      Total: 1
 INFO [main] - 會話一:{name=測測, className=一班, id=1, age=16}
DEBUG [main] - Cache Hit Ratio [xin.sunce.mybatis.dao.StudentDao]: 0.5
 INFO [main] - 會話二,classDao更新之前:{name=測測, className=一班, id=1, age=16}
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 1987169128.
DEBUG [main] - ==>  Preparing: UPDATE class SET name = ? WHERE id = ? 
DEBUG [main] - ==> Parameters: 測試一班(String), 1(Integer)
DEBUG [main] - <==    Updates: 1
DEBUG [main] - Cache Hit Ratio [xin.sunce.mybatis.dao.StudentDao]: 0.6666666666666666
 INFO [main] - 會話二,classDao更新之后:{name=測測, className=一班, id=1, age=16}
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2d2ffcb7]
DEBUG [main] - Returned connection 758119607 to pool.
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7671cb68]
DEBUG [main] - Returned connection 1987169128 to pool.

我們發(fā)現(xiàn)班級名稱已經(jīng)被修改為測試一班,然而會話二的結(jié)果仍為一班;驗(yàn)證了二級緩存的范圍為namespace

二級緩存實(shí)驗(yàn)五

我們注釋掉ClassDao.xml中的<cache/> ,開啟<cache-ref namespace="xin.sunce.mybatis.dao.StudentDao"/>,將class表與student表置于同一namesapce,再看看結(jié)果

測試結(jié)果:

DEBUG [main] - Created connection 758119607.
DEBUG [main] - ==>  Preparing: SELECT s.id,s.name,s.age,class.name as className FROM classroom c JOIN student s ON c.student_id = s.id JOIN class ON c.class_id = class.id WHERE s.id = ?; 
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <==      Total: 1
 INFO [main] - 會話一:{name=測測, className=一班, id=1, age=16}
DEBUG [main] - Cache Hit Ratio [xin.sunce.mybatis.dao.StudentDao]: 0.5
 INFO [main] - 會話二,classDao更新之前:{name=測測, className=一班, id=1, age=16}
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 1944978632.
DEBUG [main] - ==>  Preparing: UPDATE class SET name = ? WHERE id = ? 
DEBUG [main] - ==> Parameters: 特殊一班(String), 1(Integer)
DEBUG [main] - <==    Updates: 1
DEBUG [main] - Cache Hit Ratio [xin.sunce.mybatis.dao.StudentDao]: 0.3333333333333333
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 1804379080.
DEBUG [main] - ==>  Preparing: SELECT s.id,s.name,s.age,class.name as className FROM classroom c JOIN student s ON c.student_id = s.id JOIN class ON c.class_id = class.id WHERE s.id = ?; 
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <==      Total: 1
 INFO [main] - 會話二,classDao更新之后:{name=測測, className=特殊一班, id=1, age=16}

發(fā)現(xiàn)結(jié)果沒有走緩存,兩個表共享一個namespace;不過這樣做的后果是,緩存的粒度變粗了,多個 Mapper namespace 下的所有操作都會對緩存使用造成影響。

二級緩存源碼分析
二級緩存.png

如果讀過上篇文章的朋友可能記得,從Configuration獲取Executor時:

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? this.defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Object executor;
    if (ExecutorType.BATCH == executorType) {
        executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
        executor = new ReuseExecutor(this, transaction);
    } else {
        executor = new SimpleExecutor(this, transaction);
    }
    // 當(dāng)配置XML文件中cacheEnabled=true時,返回的的執(zhí)行器則為CachingExecutor
    if (this.cacheEnabled) {
        executor = new CachingExecutor((Executor)executor);
    }

    Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
    return executor;
}

開啟緩存以后返回的Executor為CachingExecutor;而CachingExecutor本質(zhì)即為裝飾器,是對BaseExecutor一次包裝,從構(gòu)造函數(shù)public CachingExecutor(Executor delegate)即可看出;那么跟BaseExecutor一樣,我們還是從query方法展開源碼的閱讀。


public class CachingExecutor implements Executor {

//事務(wù)緩存管理器
private final TransactionalCacheManager tcm = new TransactionalCacheManager();

private final Executor delegate;

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) {
        //判斷是否要清除緩存
        this.flushCacheIfRequired(ms);
        if (ms.isUseCache() && resultHandler == null) {
            this.ensureNoOutParams(ms, boundSql);
            List<E> list = (List)this.tcm.getObject(cache, key);
            if (list == null) {
                list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                this.tcm.putObject(cache, key, list);
            }

            return list;
        }
    }

    return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }
    
....
//在默認(rèn)的設(shè)置中SELECT語句不會刷新緩存,insert/update/delte 會刷新緩存。進(jìn)入該方法。
private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    if (cache != null && ms.isFlushCacheRequired()) {
        this.tcm.clear(cache);
    }

}
       

CachingExecutor對緩存的管理都是由TransactionalCacheManager完成的,我們來看看TransactionalCacheManager的具體代碼

public void clear(Cache cache) {
    this.getTransactionalCache(cache).clear();
}

public Object getObject(Cache cache, CacheKey key) {
    return this.getTransactionalCache(cache).getObject(key);
}

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

Cache 究竟是如何工作的呢?我們可以通過DEBUG,來看看

Cache.png

TransactionalCacheManager管理的是一個Cache的裝飾鏈,裝飾鏈的執(zhí)行過程是SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache ->
PerpetualCache 最終一直到PerpetualCache;以上就是處理緩存的流程。

有的人可能會好奇<cache/> <cache-ref/> 如何處理的?回顧上篇文章,SqlSessionFactoryBuilder 在構(gòu)建Factory可以選擇讀取XML文件的方式,順著這個思路去找,你應(yīng)該可以找到你想要的答案。

二級緩存總結(jié)
  • MyBatis的二級緩存相對于一級緩存來說,實(shí)現(xiàn)了 SqlSession 之間緩存數(shù)據(jù)的共享,同時粒度更加的細(xì),能夠到namespace 級別,通過Cache接口實(shí)現(xiàn)類不同的組合,對Cache的可控性也更強(qiáng)。
  • MyBatis在多表查詢時,極大可能會出現(xiàn)臟數(shù)據(jù),有設(shè)計上的缺陷,安全使用二級緩存的條件比較苛刻。
  • 在分布式環(huán)境下,由于默認(rèn)的MyBatis Cache實(shí)現(xiàn)都是基于本地的,分布式環(huán)境下必然會出現(xiàn)讀取到臟數(shù)據(jù),需要使用集中式緩存將MyBatis的Cache接口實(shí)現(xiàn),有一定的開發(fā)成本,直接使用Redis、Memcached等分布式緩存可能成本更低,安全性也更高。
最后編輯于
?著作權(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ù)。

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