天天看點

mybais (一級緩存實作詳解 + 源碼分析) (十)

文章目錄

  • ​​1.一級緩存​​
  • ​​1.1 一級緩存的概述​​
  • ​​1.2 MyBatis中的一級緩存是怎樣組織的​​
  • ​​1.3 一級緩存的生命周期​​
  • ​​2.SqlSession 一級緩存的工作流程​​
  • ​​3.Cache接口的設計以及CacheKey的定義​​
  • ​​3.1 CacheKey的定義​​
  • ​​3.2 CacheKey的建立​​
  • ​​4.關閉一級緩存​​
  • ​​4.1 什麼場景下必須需要關閉一級緩存​​
  • ​​4.2 關閉一級緩存方法​​

1.一級緩存

1.1 一級緩存的概述

每當我們使用MyBatis開啟一次和資料庫的會話,MyBatis會建立出一個SqlSession對象表示一次資料庫會話。

MyBatis會在一次會話的表示----一個SqlSession對象中建立一個本地緩存(local cache),對于每一次查詢,都會嘗試根據查詢的條件去本地緩存中查找是否在緩存中,如果在緩存中,就直接從緩存中取出,然後傳回給使用者;否則,從資料庫讀取資料,将查詢結果存入緩存并傳回給使用者。

mybais (一級緩存實作詳解 + 源碼分析) (十)

對于會話(Session)級别的資料緩存,我們稱之為一級資料緩存,簡稱一級緩存。

mybais (一級緩存實作詳解 + 源碼分析) (十)

1.2 MyBatis中的一級緩存是怎樣組織的

SqlSession、Executor、Cache之間的關系:

mybais (一級緩存實作詳解 + 源碼分析) (十)

Executor接口的實作類BaseExecutor中擁有一個Cache接口的實作類PerpetualCache,則對于BaseExecutor對象而言,它将使用PerpetualCache對象維護緩存。

mybais (一級緩存實作詳解 + 源碼分析) (十)

SqlSession對象、Executor對象、Cache對象之間的關系:

mybais (一級緩存實作詳解 + 源碼分析) (十)

PerpetualCache實作原理其實很簡單,其内部就是通過一個簡單的HashMap<k,v> 來實作的,沒有其他的任何限制。

public class PerpetualCache implements Cache {

  private final String id;

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

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

  @Override
  public String getId() {
    return id;
  }

  @Override
  public int getSize() {
    return cache.size();
  }

  @Override
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }

  @Override
  public Object getObject(Object key) {
    return cache.get(key);
  }

  @Override
  public Object removeObject(Object key) {
    return cache.remove(key);
  }

  @Override
  public void clear() {
    cache.clear();
  }

  @Override
  public boolean equals(Object o) {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    if (this == o) {
      return true;
    }
    if (!(o instanceof Cache)) {
      return false;
    }

    Cache otherCache = (Cache) o;
    return getId().equals(otherCache.getId());
  }

  @Override
  public int hashCode() {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    return getId().hashCode();
  }

}      

1.3 一級緩存的生命周期

  1. MyBatis在開啟一個資料庫會話時,會 建立一個新的​

    ​SqlSession​

    ​對象,​

    ​SqlSession​

    ​對象中會有一個新的​

    ​Executor​

    ​對象,​

    ​Executor​

    ​對象中持有一個新的​

    ​PerpetualCache​

    ​對象;當會話結束時,​

    ​SqlSession​

    ​對象及其内部的​

    ​Executor​

    ​對象還有​

    ​PerpetualCache​

    ​對象也一并釋放掉。
  2. 如果​

    ​SqlSession​

    ​調用了​

    ​close()​

    ​方法,會釋放掉一級緩存​

    ​PerpetualCache​

    ​對象,一級緩存将不可用;
  3. 如果SqlSession調用了clearCache(),會​

    ​清空PerpetualCache對象​

    ​中的資料,但是該對象仍可使用;
  4. SqlSession中執行了任何一個update操作(update()、delete()、insert()) ,都會清空​

    ​PerpetualCache​

    ​對象的資料,但是該對象可以​

    ​繼續使用​

    ​;
mybais (一級緩存實作詳解 + 源碼分析) (十)

2.SqlSession 一級緩存的工作流程

  1. 對于某個查詢,根據statementId,params,rowBounds來建構一個key值,根據這個key值去緩存Cache中取出對應的key值存儲的緩存結果;
  2. 判斷從Cache中根據特定的key值取的資料資料是否為空,即是否命中;
  3. 如果命中,則直接将緩存結果傳回;
  4. 如果沒命中: 去資料庫中查詢資料,得到查詢結果;将key和查詢到的結果分别作為key,value對存儲到Cache中;将查詢結果傳回;
mybais (一級緩存實作詳解 + 源碼分析) (十)
mybais (一級緩存實作詳解 + 源碼分析) (十)
mybais (一級緩存實作詳解 + 源碼分析) (十)
mybais (一級緩存實作詳解 + 源碼分析) (十)

3.Cache接口的設計以及CacheKey的定義

3.1 CacheKey的定義

mybais (一級緩存實作詳解 + 源碼分析) (十)

Cache最核心的實作其實就是一個Map,将本次查詢使用的特征值作為key,将查詢結果作為value存儲到Map中。

MyBatis認為,對于兩次查詢,如果以下條件都完全一樣,那麼就認為它們是完全相同的兩次查詢

  1. 傳入的 statementId
  2. 查詢時要求的結果集中的結果範圍 (結果的範圍通過rowBounds.offset和rowBounds.limit表示);
  3. 這次查詢所産生的最終要傳遞給JDBC java.sql.Preparedstatement的Sql語句字元串(boundSql.getSql() )
  4. 傳遞給java.sql.Statement要設定的參數值

解釋:

  1. 傳入的statementId,對于MyBatis而言,你要使用它,必須需要一個statementId,它代表着你将執行什麼樣的Sql;
  2. MyBatis自身提供的分頁功能是通過RowBounds來實作的,它通過​

    ​rowBounds.offset​

    ​​和​

    ​rowBounds.limit​

    ​來過濾查詢出來的結果集,這種分頁功能是基于查詢結果的再過濾,而不是進行資料庫的實體分頁;
  3. 第3個條件正是要求保證傳遞給JDBC的SQL語句完全一緻;第4條則是保證傳遞給JDBC的參數也完全一緻;

CacheKey:

statementId + rowBounds + 傳遞給JDBC的SQL + 傳遞給JDBC的參數值

3.2 CacheKey的建立

對于每次的查詢請求,Executor都會根據傳遞的參數資訊以及動态生成的SQL語句, 将上面的條件根據一定的計算規則,建立一個對應的CacheKey對象。

  1. 根據CacheKey作為key,去Cache緩存中查找緩存結果;
  2. 如果查找緩存命中失敗,則通過此CacheKey作為key,将從資料庫查詢到的結果作為value,組成key,value對存儲到Cache緩存中。

BaseExecutor#createCacheKey()

@Override
  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
    // 1.statementId
    cacheKey.update(ms.getId());
    // 2.rowBounds.offset
    cacheKey.update(rowBounds.getOffset());
    // 3.rowBounds.limit
    cacheKey.update(rowBounds.getLimit());
    // 4.Sql語句
    cacheKey.update(boundSql.getSql());
    // 5.将每一個要傳遞給JDBC的參數值也更新到CacheKey中
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    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);
        }
        // 将每一個要傳遞給JDBC的參數值也更新到CacheKey中
        cacheKey.update(value);
      }
    }
    if (configuration.getEnvironment() != null) {
      // 将每一個要傳遞給JDBC的參數值也更新到CacheKey中
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }      

CacheKey的hashcode生成算法:

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

4.關閉一級緩存

4.1 什麼場景下必須需要關閉一級緩存

4.2 關閉一級緩存方法

  1. xml:
<select flushCache="true"></select>      
  1. 注解形式:

    @Options(flushCache = Options.FlushCachePolicy.TRUE)

@Options(flushCache = Options.FlushCachePolicy.TRUE)
@Select("select * from ge_jdbc_datasource where id = #{id,jdbcType=BIGINT} and status = 1")
@ResultMap("resultMap")
JdbcDataSource find(Long id);      
  1. sql動态拼接傳入的随機數

    比如sql傳參random()數值 或者 sql傳入目前時間毫秒數,切記一定要從方法形參傳過去而不要在sql中拼寫,否則無效

select id from ge_jdbc_datasource where id = 1 and STATUS = 1 AND NOW()=NOW()      
  1. 執行SqlSession的commit(執行插入、更新、删除操作後)
  2. 執行SqlSession的close方法
  3. 執行SqlSession的clearCache方法
org.apache.ibatis.executor.BaseExecutor#update

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


 org.apache.ibatis.executor.BaseExecutor#commit
 
 public void commit(boolean required) throws SQLException {
    if (this.closed) {
        throw new ExecutorException("Cannot commit, transaction is already closed");
    } else {
        //删除一級緩存
        this.clearLocalCache();
        this.flushStatements();
        if (required) {
            this.transaction.commit();
        }

    }
}

org.apache.ibatis.executor.BaseExecutor#rollback

public void rollback(boolean required) throws SQLException {
    if (!this.closed) {
        try {
            //删除一級緩存
            this.clearLocalCache();
            this.flushStatements(true);
        } finally {
            if (required) {
                this.transaction.rollback();
            }

        }
    }

}