Mybatis的快取——一級快取和原始碼分析

IsDxh發表於2020-11-11

什麼是快取?

快取就是存在記憶體中的資料,而記憶體讀取都是非常快的 ,通常我們會把更新變動不太頻繁查詢頻繁的資料,在第一次從資料庫查詢出後,存放在快取中,這樣就可以避免之後多次的與資料庫進行互動,從而提升響應速度。

mybatis 也提供了對快取的支援,分為:

  • 一級快取
  • 二級快取

image-20201110221457670

  1. 一級快取:
    每個sqlSeesion物件都有一個一級快取,我們在運算元據庫時需要構造sqlSeesion物件,在物件中有一個HashMap用於儲存快取資料。不同的sqlSession之間的快取資料區域(HashMap)是互不影響的。
  2. 二級快取:
    二級快取是mapper級別(或稱為namespace級別)的快取,多個sqlSession去操作同一個Mapper的sql語句,多個SqlSession可以共用二級快取,二級快取是跨sqlSession的。

一級快取

首先我們來開一級快取,一級快取是預設開啟的,所以我們可以很方便來體驗一下一級快取。

測試一、

準備一張表,有兩個欄位id和username
image-20201110232032148

在測試類中:

public class TestCache {
    private SqlSession sqlSession;
    private UserMapper mapper;
    @Before
    public void before() throws IOException {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);
        sqlSession = build.openSession();
        mapper = sqlSession.getMapper(UserMapper.class);
    }

    @Test
    public void testFirst(){
        //第一次查詢————首先去一級快取中查詢
        User user1 = mapper.findById(1);
        System.out.println("======"+user1);
		//第二次查詢
        User user2 = mapper.findById(1);
        System.out.println("======"+user2);
        
        System.out.println(user1==user2);
    }
}

我們用同一個sqlSession分別根據id來查詢使用者,id都為1,之後再比較它們的地址值。來看一下結果:

23:16:25,818 DEBUG findById:159 - ==>  Preparing: select * from user where id=? 
23:16:25,862 DEBUG findById:159 - ==> Parameters: 1(Integer)
23:16:25,894 DEBUG findById:159 - <==      Total: 1
======User{id=1, username='lucy'}
======User{id=1, username='lucy'}
true

我們發現只列印了一條SQL,同時它們的地址值一致。

說明第一次查詢,快取中沒有,然後從資料庫中查詢——執行SQL,然後存入快取,第二次查詢時發現快取中有了,所以直接從快取中取出,不再執行SQL了。
image-20201110233551326

我們剛才提到,一級快取的資料結構是一個hashmap,也就是說有key有value。
value就是我們查詢出的結果,key是由多個值組成的:

  • statementid :namespace.id組成
  • params:查詢時傳入的引數
  • boundsql:mybatis底層的物件,它封裝著我們要執行的sql
  • rowbounds:分頁物件
  • ...還有一些會在原始碼分析中道明

測試二、

我們現在修改一下,我們在查詢第一次結果後,修改一下資料庫的值,然後再進行第二次查詢,我們來看一下查詢結果。id=1 的username為lucy
image-20201110232032148

    @Test
    public void testFirst(){
        //第一次查詢
        User user1 = mapper.findById(1);
        System.out.println("======"+user1);

        //修改id為1的username
        User updateUser = new User();
        updateUser.setId(1);
        updateUser.setUsername("李思");
        mapper.updateUser(updateUser);
        //手動提交事務
        sqlSession.commit();

        //第二次查詢
        User user2 = mapper.findById(1);
        System.out.println("======"+user2);

        System.out.println(user1==user2);
    }

image-20201110235221440

在提交事務的地方打一個斷點,可以看到執行了兩條sql,一個是查詢id為1,一個是修改id為1的username

最終結果:

23:50:15,933 DEBUG findById:159 - ==>  Preparing: select * from user where id=? 
23:50:15,976 DEBUG findById:159 - ==> Parameters: 1(Integer)
23:50:16,002 DEBUG findById:159 - <==      Total: 1
======User{id=1, username='lucy', roleList=null, orderList=null}
23:50:16,003 DEBUG updateUser:159 - ==>  Preparing: update user set username=? where id =? 
23:50:16,005 DEBUG updateUser:159 - ==> Parameters: 李思(String), 1(Integer)
23:50:16,016 DEBUG updateUser:159 - <==    Updates: 1
23:53:18,316 DEBUG JdbcTransaction:70 - Committing JDBC Connection [com.mysql.jdbc.JDBC4Connection@421e361]
23:53:22,306 DEBUG findById:159 - ==>  Preparing: select * from user where id=? 
23:53:22,306 DEBUG findById:159 - ==> Parameters: 1(Integer)
23:53:22,307 DEBUG findById:159 - <==      Total: 1
======User{id=1, username='李思', roleList=null, orderList=null}

我們看到,最終列印了3條sql,再進行修改後的第二次查詢也列印了。

說明在第二次查詢時在快取中找不到所對應的key了。在進行修改操作時,會重新整理快取

我們也可以通過sqlSession.clearCache();手動重新整理一級快取

總結:

  • 一級快取的資料結構時HashMap
  • 不同的SqlSession的一級快取互不影響
  • 一級快取的key是由多個值組成的,value就是其查詢結果
  • 增刪改操作會重新整理一級快取
  • 通過sqlSession.clearCache()手動重新整理一級快取

一級快取原始碼分析:

我們在分析一級快取之前帶著一些疑問來讀程式碼

  1. 一級快取是什麼? 真的是上面說的HashMap嗎?

  2. 一級快取什麼時候被建立?

  3. 一級快取的工作流程是怎麼樣的?

1. 一級快取到底是什麼?

之前說不同的SqlSession的一級快取互不影響,所以我從SqlSession這個類入手
image-20201111005238379

可以看到,org.apache.ibatis.session.SqlSession中有一個和快取有關的方法——clearCache()重新整理快取的方法,點進去,找到它的實現類DefaultSqlSession

  @Override
  public void clearCache() {
    executor.clearLocalCache();
  }

再次點進去executor.clearLocalCache(),再次點進去並找到其實現類BaseExecutor

  @Override
  public void clearLocalCache() {
    if (!closed) {
      localCache.clear();
      localOutputParameterCache.clear();
    }
  }

進入localCache.clear()方法。進入到了org.apache.ibatis.cache.impl.PerpetualCache類中

package org.apache.ibatis.cache.impl;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;
/**
 * @author Clinton Begin
 */
public class PerpetualCache implements Cache {
  private final String id;

  private Map<Object, Object> cache = new HashMap<Object, Object>();

  public PerpetualCache(String id) {
    this.id = id;
  }

  //省略部分...
  @Override
  public void clear() {
    cache.clear();
  }
  //省略部分...
}

我們看到了PerpetualCache類中有一個屬性 private Map<Object, Object> cache = new HashMap<Object, Object>(),很明顯它是一個HashMap,我們所呼叫的.clear()方法,實際上就是呼叫的Map的clear方法
image-20201111010052591

得出結論:

一級快取的資料結構確實是HashMap
image-20201111011712449

2. 一級快取什麼時候被建立?

我們進入到org.apache.ibatis.executor.Executor
看到一個方法CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) ,見名思意是一個建立CacheKey的方法
找到它的實現類和方法org.apache.ibatis.executor.BaseExecuto.createCacheKey

image-20201111012213242

我們分析一下建立CacheKey的這塊程式碼:

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    //初始化CacheKey
    CacheKey cacheKey = new CacheKey();
    //存入statementId
    cacheKey.update(ms.getId());
    //分別存入分頁需要的Offset和Limit
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    //把從BoundSql中封裝的sql取出並存入到cacheKey物件中
    cacheKey.update(boundSql.getSql());
    //下面這一塊就是封裝引數
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();

    for (ParameterMapping parameterMapping : parameterMappings) {
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) {
          value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
          value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          value = parameterObject;
        } else {
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        cacheKey.update(value);
      }
    }
    //從configuration物件中(也就是載入配置檔案後存放的物件)把EnvironmentId存入
        /**
     *     <environments default="development">
     *         <environment id="development"> //就是這個id
     *             <!--當前事務交由JDBC進行管理-->
     *             <transactionManager type="JDBC"></transactionManager>
     *             <!--當前使用mybatis提供的連線池-->
     *             <dataSource type="POOLED">
     *                 <property name="driver" value="${jdbc.driver}"/>
     *                 <property name="url" value="${jdbc.url}"/>
     *                 <property name="username" value="${jdbc.username}"/>
     *                 <property name="password" value="${jdbc.password}"/>
     *             </dataSource>
     *         </environment>
     *     </environments>
     */
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
    //返回
    return cacheKey;
  }

我們再點進去cacheKey.update()方法看一看

/**
 * @author Clinton Begin
 */
public class CacheKey implements Cloneable, Serializable {
  private static final long serialVersionUID = 1146682552656046210L;
  public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();
  private static final int DEFAULT_MULTIPLYER = 37;
  private static final int DEFAULT_HASHCODE = 17;

  private final int multiplier;
  private int hashcode;
  private long checksum;
  private int count;
  //值存入的地方
  private transient List<Object> updateList;
  //省略部分方法......
  //省略部分方法......
  public void update(Object object) {
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); 
    count++;
    checksum += baseHashCode;
    baseHashCode *= count;
    hashcode = multiplier * hashcode + baseHashCode;
    //看到把值傳入到了一個list中
    updateList.add(object);
  }
 
  //省略部分方法......
}

我們知道了那些資料是在CacheKey物件中如何儲存的了。下面我們返回createCacheKey()方法。
image-20201111013322287

Ctrl+滑鼠左鍵 點選方法名,查詢有哪些地方呼叫了此方法

我們進入BaseExecutor,可以看到一個query()方法:
image-20201111013443089

這裡我們很清楚的看到,在執行query()方法前,CacheKey方法被建立了

3. 一級快取的執行流程

我們可以看到,建立CacheKey後呼叫了query()方法,我們再次點進去:

image-20201111014034187

在執行SQL前如何在一級快取中找不到Key,那麼將會執行sql,我們來看一下執行sql前後會做些什麼,進入list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
image-20201111014639468

分析一下:

  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    //1. 把key存入快取,value放一個佔位符
	localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      //2. 與資料庫互動
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      //3. 如果第2步出了什麼異常,把第1步存入的key刪除
      localCache.removeObject(key);
    }
      //4. 把結果存入快取
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

至此,我們思路就非常的清晰了。

結論:

在執行sql前,會首先根據CacheKey查詢快取中有沒有,如果有,就處理快取中的引數,如果沒有,就執行sql,執行sql後把結果存入快取。

一級快取原始碼分析結論:

  1. 一級快取的資料結構是一個HashMap<Object,Object>,它的value就是查詢結果,它的key是CacheKeyCacheKey中有一個list屬性,statementId,params,rowbounds,sql等引數都存入到了這個list
  2. 一級快取在呼叫query()方法前被建立。並傳入到query()方法中
  3. 會首先根據CacheKey查詢快取中有沒有,如果有,就處理快取中的引數,如果沒有,就執行sql,執行sql後把結果存入快取。

相關文章