MyBatis拾遺(三)——MyBatis的緩存

前言

緩存是一般的ORM框架都會提供的功能,目的就是提升查詢的效率,和Hibernate一樣,MyBatis也有一級緩存和二級緩存,並且我們還可以通過實現MyBatis的緩存接口來採用第三方緩存。

一級緩存

一級緩存也稱爲本地緩存,MyBatis的一級緩存是在會話級別(SqlSession),MyBatis的一級緩存是默認開啓的,我們不需要任何配置。

SqlSession其實在Mybatis中有一個默認的實現——DefaultSqlSession。

public class DefaultSqlSession implements SqlSession {

  private final Configuration configuration;
  private final Executor executor;

  private final boolean autoCommit;
  private boolean dirty;
  private List<Cursor<?>> cursorList;
}

上述屬性是DefaultSqlSession的一些屬性,我們走到現在可以很明確的一點是,緩存應該是維護在Session級別,那麼針對DefaultSqlSession中的兩個屬性,哪一個和緩存有關呢?Configuration是一個全局配置屬性,緩存固然不會在裏面維護,因此最有可能的是在Executor屬性中進行維護,Executor也是一個接口,其也有一個默認的實現——BaseExecutor(常見的Executor有三種——SimpleExecutor,ReuseExecutor,BatchExecutor)

public abstract class BaseExecutor implements Executor {

  protected Transaction transaction;
  protected Executor wrapper;

  protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
  protected PerpetualCache localCache;
  protected PerpetualCache localOutputParameterCache;
  protected Configuration configuration;

  protected int queryStack;
  private boolean closed;
  
}

可以看到這裏有兩個PerpetualCache的屬性,這個就是我們的緩存。

在同一個會話裏,多次執行相同的SQL語句,會直接從內存取到緩存中的結果,不會再發送SQL到數據庫查詢,但是在不同的會話裏頭,即使執行的SQL一樣(也是通過同一個Mapper的同一個方法調用)也不能命中一級緩存,畢竟一級緩存只存在會話級別。

在這裏插入圖片描述

測試代碼:

@Slf4j
public class TestCache {

    @Test
    public void testFirstLevleCache() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession session1 = sqlSessionFactory.openSession();
        SqlSession session2 = sqlSessionFactory.openSession();

        try{
            BlogMapper blogMapper01 = session1.getMapper(BlogMapper.class);
            BlogMapper blogMapper02 = session2.getMapper(BlogMapper.class);

            Blog blog = blogMapper01.selectBlogById(1);
            log.info("第一次查詢,通過BlogMapper01查詢:{}",blog);
            log.info("第二次查詢,依舊通過blogMapper01查詢:{}",blogMapper01.selectBlogById(1));
            log.info("第三次查詢,通過BlogMapper02查詢:{}",blogMapper02.selectBlogById(1));
        }catch (Exception e){
            log.info("出現異常,異常信息:{}",e);
        }finally {
            session1.close();
            session2.close();
        }
    }
}

運行結果

在這裏插入圖片描述

ps:在測試一級緩存的時候,建議關閉二級緩存

需要說一下的是,如果數據被修改或者刪除了,一級緩存會失效

如下代碼

/**
 * 測試一級緩存失效的情況
 */
@Test
public void testFirstLevelCacheInvalid() throws IOException {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    SqlSession session = sqlSessionFactory.openSession();
    try{
        BlogMapper blogMapper = session.getMapper(BlogMapper.class);
        log.info("第一次查詢,通過BlogMapper查詢:{}",blogMapper.selectBlogById(1));
        Blog blog = new Blog();
        blog.setBid(1);
        blog.setName(new Date().toString());
        blogMapper.updateByPrimaryKey(blog);
        //update之後,更新操作
        session.commit();

        log.info("更新之後再次查詢,查詢記錄爲:{}",blogMapper.selectBlogById(1));
    }catch (Exception e){
        log.info("出現異常,異常信息:{}",e);
    }finally {
        session.close();
    }
}

這段代碼會出現兩條SQL查詢語句。

ps:select標籤中有個flushCache的屬性,默認是false,也就是說查詢的時候是不會清空緩存的,如果有必要可以將這個屬性置爲true,查詢的時候也會更新緩存。

一級緩存的不足之處

不能跨會話共享,不同的會話之間對於相同的數據可能有不一樣的緩存。在有多個會話或者分佈式環境下,會存在髒數據的問題。如果要解決這個問題,就要用到二級緩存。

二級緩存

先來看看下面一段代碼

@Test
public void testFirstCacheDirtyRead() throws IOException {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    SqlSession session1 = sqlSessionFactory.openSession();
    SqlSession session2 = sqlSessionFactory.openSession();
    try {
        BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
        log.info("第一次查詢,通過session01,結果爲:{}",mapper1.selectBlogById(1));

        // 會話2更新了數據,會話2的一級緩存更新
        Blog blog = new Blog();
        blog.setBid(1);
        blog.setName("會話二修改"+new Date());
        BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
        mapper2.updateByPrimaryKey(blog);
        session2.commit();

        log.info("第二次查詢,還是通過session01,結果爲:{}",mapper1.selectBlogById(1));
        log.info("通過會話二查詢的真實數據爲:{}",mapper2.selectBlogById(1));
    } finally {
        session1.close();
        session2.close();
    }
}

運行結果如下:

在這裏插入圖片描述

這種不就是所謂的髒讀麼?爲了解決這個問題,於是就有了二級緩存。二級緩存的作用域是命名空間級別(Mapper級別)的,同一個Mapper的同一個方法的調用就能共享我們的二級緩存

我們知道,一級緩存是在DefaultSession的Executor屬性中維護了一個緩存對象,畢竟該緩存是會話級別的,但是針對二級緩存,是Mapper級別的,所以這個緩存結構是不是維護在DefaultSession作用域之外的一個對象中呢?而對應的SqlSession作用域之外的一個對象就是SqlSessionFactory了,但是根據單一職責原則SqlSessionFactory只是單純的產生SqlSession,而不會維護緩存。

要跨越會話共享,SqlSession本身和他裏面的BaseExecutor已經滿足不了要求了,這裏可能就需要擴展一下BaseExecutor的功能了,MyBatis底層通過裝飾器模式,擴展了BaseExecutor對象——CachingExecutor。

CachingExecutor對於查詢請求,會判斷二級緩存是否有緩存結果,如果有就直接返回,如果沒有則會讓被委託的執行器去執行,比如SimpleExecutor來執行查詢,之後再走一次一級緩存的流程。最後會把結果緩存起來返回給用戶。

在這裏插入圖片描述

開啓二級緩存

一級緩存是默認開啓的,開啓二級緩存需要增加幾個配置屬性。

1、全局配置中增加如下屬性,該配置MyBatis默認開啓其實可以不用設置

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

2、每個會話級別的Mapper中開啓緩存配置

在需要開啓二級緩存的Mapper.xml文件中加入表示開始二級緩存

<!-- 聲明這個namespace使用二級緩存 -->
<cache/>

關於cache的各種屬性實例(詳細參數參見MyBatis官方文檔)

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

1、type——指定實現緩存的具體實現類

2、size——指定緩存的大小

3、eviction——指定緩存策略

4、flushInterval——緩存刷新間隔時長

5、readOnly——默認是true(安全),如果改爲false(可讀寫)對象必須實現序列化接口。

Mapper.xml配置了標籤之後,select方法會緩存,update,delete,insert等會刷新緩存。

如果某些查詢方法對數據實時性要求很高,可以單獨針對這個接口關閉二級緩存

<select id="selectBlogById" resultMap="BaseResultMap" useCache="false">

二級緩存測試實例

@Test
public void testSecondLevelCache() throws IOException {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    SqlSession session1 = sqlSessionFactory.openSession();
    SqlSession session2 = sqlSessionFactory.openSession();
    try {
        BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
        log.info("會話一開始查詢,查詢結果爲:{}",mapper1.selectBlogById(1));
        // 事務不提交的情況下,二級緩存並不會寫入
        session1.commit();

        BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
        log.info("會話二開始查詢,查詢結果爲:{}",mapper2.selectBlogById(1));
    } finally {
        session1.close();
    }
}

上述代碼運行結果:

在這裏插入圖片描述

二級緩存的注意事項

一級緩存是默認開啓的,二級緩存是需要配置纔可以開啓,那麼就需要梳理二級緩存在什麼情況下才有必要去開啓

1、所有的刪除都會刷新二級緩存,導致二級緩存失效,所以查詢爲主的應用中可以使用二級緩存

2、如果多個namespace(可以理解一個mapper文件)中有針對於同一張表的操作,如果在一個namespace中刷新了緩存,另一個namespace中沒有刷新,就會出現髒讀的數據,所以建議一個mapper中只操作一張表。

緩存可以在多個namespace中共享,如果AMapper.xml文件中需要共享BMapper.xml文件中的緩存,可以使用來解決。但是多個mapper,只要有其中任意一個操作的修改等操作,都會刷新緩存,這個時候一定程度上二級緩存已經失去了意義了。

<cache-ref namespace = "com.learn.dao.mapper.BMapper"/>

3、可以採用第三方緩存中間件來實現二級緩存,參看以下實例

使用redis作爲MyBatis二級緩存實例:

先引入MyBatis-redis依賴

<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-redis</artifactId>
    <version>1.0.0-beta2</version>
</dependency>

引入redis配置文件

host=localhost
port=6379
connectionTimeout=5000
soTimeout=5000
database=0

運行上面的二級緩存實例代碼即可

可以看到指定的redis緩存中如下所示:

在這裏插入圖片描述

總結

本篇博客總結了MyBatis中緩存的問題。

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