天天看點

給我五分鐘,帶你徹底掌握 MyBatis 緩存的工作原理前言為什麼要緩存MyBatis緩存總結

前言

在計算機的世界中,緩存無處不在,作業系統有作業系統的緩存,資料庫也會有資料庫的緩存,各種中間件如Redis也是用來充當緩存的作用,程式設計語言中又可以利用記憶體來作為緩存。自然的,作為一款優秀的ORM架構,MyBatis中又豈能少得了緩存,那麼本文的目的就是帶領大家一起探究一下MyBatis的緩存是如何實作的,隻需給我五分鐘,帶你徹底掌握MyBatis的緩存工作原理。

為什麼要緩存

在計算機的世界中,CPU的處理速度可謂是一馬當先,遠遠甩開了其他操作,尤其是I/O操作,除了那種CPU密集型的系統,其餘大部分的業務系統性能瓶頸最後或多或少都會出現在I/O操作上,是以為了減少磁盤的I/O次數,那麼緩存是必不可少的,通過緩存的使用我們可以大大減少I/O操作次數,進而在一定程度上彌補了I/O操作和CPU處理速度之間的鴻溝。而在我們ORM架構中引入緩存的目的就是為了減少讀取資料庫的次數,進而提升查詢的效率。

MyBatis緩存

MyBatis中的緩存相關類都在cache包下面,而且定義了一個頂級接口Cache,預設隻有一個實作類PerpetualCache,PerpetualCache中是内部維護了一個HashMap來實作緩存。

下圖就是MyBatis中緩存相關類:

需要注意的是decorators包下面的所有類也實作了Cache接口,那麼為什麼我還是要說Cache隻有一個實作類呢?其實看名字就知道了,這個包裡面全部是裝飾器,也就是說這其實是裝飾器模式的一種實作。

我們随意打開一個裝飾器:

可以看到,最終都是調用了delegate來實作,隻是将部分功能做了增強,其本身都需要依賴Cache的唯一實作類PerpetualCache(因為裝飾器内需要傳入Cache對象,故而隻能傳入PerpetualCache對象,因為接口是無法直接new出來傳進去的)。

在MyBatis中存在兩種緩存,即一級緩存和二級緩存。

一級緩存

一級緩存也叫本地緩存,在MyBatis中,一級緩存是在會話(SqlSession)層面實作的,這就說明一級緩存作用範圍隻能在同一個SqlSession中,跨SqlSession是無效的。

MyBatis中一級緩存是預設開啟的,不需要任何配置。我們先來看一個例子驗證一下一級緩存是不是真的存在,作用範圍又是不是真的隻是對同一個SqlSession有效。

一級緩存真的存在嗎

package com.lonelyWolf.mybatis;

import com.lonelyWolf.mybatis.mapper.UserAddressMapper;
import com.lonelyWolf.mybatis.mapper.UserMapper;
import com.lonelyWolf.mybatis.model.LwUser;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

public class TestMyBatisCache {
    public static void main(String[] args) throws IOException {
        String resource = "mybatis-config.xml";
        //讀取mybatis-config配置檔案
        InputStream inputStream = Resources.getResourceAsStream(resource);
        //建立SqlSessionFactory對象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        //建立SqlSession對象
        SqlSession session = sqlSessionFactory.openSession();

        UserMapper userMapper = session.getMapper(UserMapper.class);
        List<LwUser> userList =  userMapper.selectUserAndJob();
        List<LwUser> userList2 =  userMapper.selectUserAndJob();
    }
}           

執行後,輸出結果如下:

我們可以看到,sql語句隻列印了一次,這就說明第2次用到了緩存,這也足以證明一級緩存确實是存在的而且預設就是是開啟的。

一級緩存作用範圍

現在我們再來驗證一下一級緩存是否真的隻對同一個SqlSession有效,我們對上面的示例代碼進行如下改變:

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

 UserMapper userMapper1 = session1.getMapper(UserMapper.class);
 UserMapper userMapper2 = session2.getMapper(UserMapper.class);
 List<LwUser> userList =  userMapper1.selectUserAndJob();
 List<LwUser> userList2 =  userMapper2.selectUserAndJob();           

這時候再次運作,輸出結果如下:

可以看到,列印了2次,沒有用到緩存,也就是不同SqlSession中不能共享一級緩存。

一級緩存原理分析

首先讓我們來想一想,既然一級緩存的作用域隻對同一個SqlSession有效,那麼一級緩存應該存儲在哪裡比較合适是呢?

是的,自然是存儲在SqlSession内是最合适的,那我們來看看SqlSession的唯一實作類DefaultSqlSession:

DefaultSqlSession中隻有5個成員屬性,後面3個不用說,肯定不可能用來存儲緩存,然後Configuration又是一個全局的配置檔案,也不合适存儲一級緩存,這麼看來就隻有Executor比較合适了,因為我們知道,SqlSession隻提供對外接口,實際執行sql的就是Executor。

既然這樣,那我們就進去看看Executor的實作類BaseExecutor:

看到果然有一個localCache。而上面我們有提到PerpetualCache内緩存是用一個HashMap來存儲緩存的,那麼接下來大家肯定就有以下問題:

  • 緩存是什麼時候建立的?
  • 緩存的key是怎麼定義的?
  • 緩存在何時使用
  • 緩存在什麼時候會失效?

接下來就讓我們逐一分析

1、一級緩存CacheKey的構成

既然緩存那麼肯定是針對的查詢語句,一級緩存的建立就是在BaseExecutor中的query方法内建立的:

createCacheKey這個方法的代碼就不貼了,在這裡我總結了一下CacheKey的組成,CacheKey主要是由以下6部分組成

  • 1、将Statement中的id添加到CacheKey對象中的updateList屬性
  • 2、将offset(分頁偏移量)添加到CacheKey對象中的updateList屬性(如果沒有分頁則預設0)
  • 3、将limit(每頁顯示的條數)添加到CacheKey對象中的updateList屬性(如果沒有分頁則預設Integer.MAX_VALUE)
  • 4、将sql語句(包括占位符?)添加到CacheKey對象中的updateList屬性
  • 5、循環使用者傳入的參數,并将每個參數添加到CacheKey對象中的updateList屬性
  • 6、如果有配置Environment,則将Environment中的id添加到CacheKey對象中的updateList屬性

2、一級緩存的使用

建立完CacheKey之後,我們繼續進入query方法:

可以看到,在查詢之前就會去localCache中根據CacheKey對象來擷取緩存,擷取不到才會調用後面的queryFromDatabase方法

3、一級緩存的建立

queryFromDatabase方法中會将查詢得到的結果存儲到localCache中

4、一級緩存什麼時候會被清除

一級緩存的清除主要有以下兩個地方:

  • 1、就是擷取緩存之前會先進行判斷使用者是否配置了flushCache=true屬性(參考一級緩存的建立代碼截圖),如果配置了則會清除一級緩存。
  • 2、MyBatis全局配置屬性localCacheScope配置為Statement時,那麼完成一次查詢就會清除緩存。
  • 3、在執行commit,rollback,update方法時會清空一級緩存。

PS:利用插件我們也可以自己去将緩存清除,後面我們會介紹插件相關知識。

二級緩存

一級緩存因為隻能在同一個SqlSession中共享,是以會存在一個問題,在分布式或者多線程的環境下,不同會話之間對于相同的資料可能會産生不同的結果,因為跨會話修改了資料是不能互相感覺的,是以就有可能存在髒資料的問題,正因為一級緩存存在這種不足,是以我們需要一種作用域更大的緩存,這就是二級緩存。

二級緩存的作用範圍

一級緩存作用域是SqlSession級别,是以它存儲的SqlSession中的BaseExecutor之中,但是二級緩存目的就是要實作作用範圍更廣,那肯定是要實作跨會話共享的,在MyBatis中二級緩存的作用域是namespace,也就是作用範圍是同一個命名空間,是以很顯然二級緩存是需要存儲在SqlSession之外的,那麼二級緩存應該存儲在哪裡合适呢?

在MyBatis中為了實作二級緩存,專門用了一個裝飾器來維護,這就是我們上一篇文章介紹Executor時還留下的沒有介紹的一個對象:CachingExecutor。

如何開啟二級緩存

二級緩存相關的配置有三個地方:1、mybatis-config中有一個全局配置屬性,這個不配置也行,因為預設就是true。

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

想詳細了解mybatis-config的可以點選這裡。2、在Mapper映射檔案内需要配置緩存标簽:

<cache/>
或
<cache-ref namespace="com.lonelyWolf.mybatis.mapper.UserAddressMapper"/>           

想詳細了解Mapper映射的所有标簽屬性配置可以點選這裡。3、在select查詢語句标簽上配置useCache屬性,如下:

<select id="selectUserAndJob" resultMap="JobResultMap2" useCache="true">
        select * from lw_user
    </select>           

以上配置第1點是預設開啟的,也就是說我們隻要配置第2點就可以打開二級緩存了,而第3點是當我們需要針對某一條語句來配置二級緩存時候則可以使用。

不過開啟二級緩存的時候有兩點需要注意:1、需要commit事務之後才會生效 2、如果使用的是預設緩存,那麼結果集對象需要實作序列化接口(Serializable)

如果不實作序列化接口則會報如下錯誤:

接下來我們通過一個例子來驗證一下二級緩存的存在,還是用上面一級緩存的例子進行如下改造:

SqlSession session1 = sqlSessionFactory.openSession();
        UserMapper userMapper1 = session1.getMapper(UserMapper.class);
        List<LwUser> userList =  userMapper1.selectUserAndJob();
        session1.commit();//注意這裡需要commit,否則緩存不會生效

        SqlSession session2 = sqlSessionFactory.openSession();
        UserMapper userMapper2 = session2.getMapper(UserMapper.class);
        List<LwUser> userList2 =  userMapper2.selectUserAndJob();           

然後UserMapper.xml映射檔案中,新增如下配置:

<cache/>           

運作代碼,輸出如下結果:

上面輸出結果中隻輸出了一次sql,說明用到了緩存,而因為我們是跨會話的,是以肯定就是二級緩存生效了。

二級緩存原理分析

上面我們提到二級緩存是通過CachingExecutor對象來實作的,那麼就讓我們先來看看這個對象:

我們看到CachingExecutor中隻有2個屬性,第1個屬性不用說了,因為CachingExecutor本身就是Executor的包裝器,是以屬性TransactionalCacheManager肯定就是用來管理二級緩存的,我們再進去看看TransactionalCacheManager對象是如何管理緩存的:

TransactionalCacheManager内部非常簡單,也是維護了一個HashMap來存儲緩存。HashMap中的value是一個TransactionalCache對象,繼承了Cache。

注意上面有一個屬性是臨時存儲二級緩存的,為什麼要有這個屬性,我們下面會解釋。

1、二級緩存的建立和使用

我們在讀取mybatis-config全局配置檔案的時候會根據我們配置的Executor類型來建立對應的三種Executor中的一種,然後如果我們開啟了二級緩存之後,隻要開啟(全局配置檔案中配置為true)就會使用CachingExecutor來對我們的三種基本Executor進行包裝,即使Mapper.xml映射檔案沒有開啟也會進行包裝。

接下來我們看看CachingExecutor中的query方法:

上面方法大緻經過如下流程:

  • 1、建立一級緩存的CacheKey
  • 2、擷取二級緩存
  • 3、如果沒有擷取到二級緩存則執行被包裝的Executor對象中的query方法,此時會走一級緩存中的流程。
  • 4、查詢到結果之後将結果進行緩存。

需要注意的是在事務送出之前,并不會真正存儲到二級緩存,而是先存儲到一個臨時屬性,等事務送出之後才會真正存儲到二級緩存。這麼做的目的就是防止髒讀。因為假如你在一個事務中修改了資料,然後去查詢,這時候直接緩存了,那麼假如事務復原了呢?是以這裡會先臨時存儲一下。是以我們看一下commit方法:

二級緩存如何進行包裝

最開始我們提到了一些緩存的包裝類,這些都到底有什麼用呢?在回答這個問題之前,我們先斷點一下看看擷取到的二級緩存長啥樣:

從上面可以看到,經過了層層包裝,從内到外一次經過如下包裝:

  • 1、PerpetualCache:第一層緩存,這個是緩存的唯一實作類,肯定需要。
  • 2、LruCache:二級緩存淘汰機制之一。因為我們配置的預設機制,而預設就是LRU算法淘汰機制。淘汰機制總共有4中,我們可以自己進行手動配置。
  • 3、SerializedCache:序列化緩存。這就是為什麼開啟了預設二級緩存我們的結果集對象需要實作序列化接口。
  • 4、LoggingCache:日志緩存。
  • 5、SynchronizedCache:同步緩存機制。這個是為了保證多線程機制下的線程安全性。

下面就是MyBatis中所有緩存的包裝彙總:

二級緩存應該開啟嗎

既然一級緩存預設是開啟的,而二級緩存是需要我們手動開啟的,那麼我們什麼時候應該開啟二級緩存呢?

1、因為所有的update操作(insert,delete,uptede)都會觸發緩存的重新整理,進而導緻二級緩存失效,是以二級緩存适合在讀多寫少的場景中開啟。

2、因為二級緩存針對的是同一個namespace,是以建議是在單表操作的Mapper中使用,或者是在相關表的Mapper檔案中共享同一個緩存。

自定義緩存

一級緩存可能存在髒讀情況,那麼二級緩存是否也可能存在呢?

是的,預設的二級緩存畢竟也是存儲在本地緩存,是以對于微服務下是可能出現髒讀的情況的,是以這時候我們可能會需要自定義緩存,比如利用redis來存儲緩存,而不是存儲在本地記憶體當中。

MyBatis官方提供的第三方緩存

MyBatis官方也提供了一些第三方緩存的支援,如:encache和redis。下面我們以redis為例來示範一下:引入pom檔案:

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

然後緩存配置如下:

<cache type="org.mybatis.caches.redis.RedisCache"></cache>           

然後在預設的resource路徑下建立一個redis.properties檔案:

host=localhost
port=6379
12           

然後執行上面的示例,檢視Cache,已經被Redis包裝:

自己實作二級緩存

如果要實作一個自己的緩存的話,那麼我們隻需要建立一個類實作Cache接口就好了,然後重寫其中的方法,如下:

package com.lonelyWolf.mybatis.cache;

import org.apache.ibatis.cache.Cache;

public class MyCache implements Cache {
    @Override
    public String getId() {
        return null;
    }
    @Override
    public void putObject(Object o, Object o1) {

    }
    @Override
    public Object getObject(Object o) {
        return null;
    }

    @Override
    public Object removeObject(Object o) {
        return null;
    }

    @Override
    public void clear() {
    }

    @Override
    public int getSize() {
        return 0;
    }
}           

上面自定義的緩存中,我們隻需要在對應方法,如putObject方法,我們把緩存存到我們想存的地方就行了,方法全部重寫之後,然後配置的時候type配上我們自己的類就可以實作了,在這裡我們就不做示範了

總結

本文主要分析了MyBatis的緩存是如何實作的,并且分别示範了一級緩存和二級緩存,并分析了一級緩存和二級緩存所存在的問題,最後也介紹了如何使用第三方緩存和如何自定義我們自己的緩存,通過本文,我想大家應該可以徹底掌握MyBatis的緩存工作原理了。

Java 的知識面非常廣,面試問的涉及也非常廣泛,重點包括:Java 基礎、Java 并發,JVM、MySQL、資料結構、算法、Spring、微服務、MQ 等等,涉及的知識點何其龐大,是以我們在複習的時候也往往無從下手,今天小編給大家帶來一套 Java 面試題,題庫非常全面,包括 Java 基礎、Java 集合、JVM、Java 并發、Spring全家桶、Redis、MySQL、Dubbo、Netty、MQ 等等,包含 Java 後端知識點 2000 +

資料擷取方式:關注公種浩:“程式員白楠楠”擷取上述資料