1. 程式人生 > >【Mybatis原始碼】一級快取

【Mybatis原始碼】一級快取

參考:

Mybatis一級快取配置:

<setting name="localCacheScope" value="SESSION"/>

value有兩個值可選:

session:快取對一次會話中所有的執行語句有效,也就是SqlSession級別的。

statement:快取只對當前執行的這一個Statement有效。

在一級快取中對快取的查詢和寫入是在Executor中完成的,以BaseExecutor為例,檢視query方法:

public abstract class BaseExecutor implements Executor {

  private static final Log log = LogFactory.getLog(BaseExecutor.class);

  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;

......
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    //構建CacheKey
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);//呼叫了下面的query方法
 }

  @SuppressWarnings("unchecked")
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    //如果queryStack為0或者並且有必要重新整理快取
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();//清空本地快取
    }
    List<E> list;
    try {
      queryStack++;
      //從快取中獲取資料,key的型別為CacheKey
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        //處理本地快取輸出引數
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {//如果獲取結果為空,從資料庫中查詢
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      //如果是STATEMENT級別的快取
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // 清空快取
        clearLocalCache();
      }
    }
    return list;
  }
}

(1)從BaseExecutor的成員變數中,可以看到有一個型別為PerpetualCache變數名為localCache的欄位,快取就是用它來實現的。PerpetualCache類的成員變數也很簡單,包含一個id和一個HashMap,快取資料就儲存在HashMap中。

public class PerpetualCache implements Cache {

  private final String id;

  private Map<Object, Object> cache = new HashMap<Object, Object>();//使用一個map做儲存

   get set方法省略
   ......
}

(2)在BaseExecutor的quey方法中,有一個構建CacheKey的語句,既然快取資料儲存在HashMap中,那麼資料格式一定是鍵值對的形式,這個CacheKey就是HashMap中的key,value是資料庫返回的資料。

CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);

(3)第二個query方法中,當執行查詢時,首先通過localCache.getObject(key)從快取中獲取資料,如果獲取的資料為空,再從資料庫中查詢。

//從快取中獲取資料,key的型別為CacheKey
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
//如果獲取結果為空,從資料庫中查詢
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

(4)如果開啟了flushcache,將會清空快取

 //如果queryStack為0或者並且有必要重新整理快取
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();//清空本地快取
    }

配置flushcache:

<select id="getStudent" parameterType="String" flushCache="true">  
    ……  
</select>  

(5)如果一級快取的級別為Statement,將會清空快取,這也是如果設定一級快取的級別為Statement時快取只對當前執行的這一個Statement有效的原因。

//如果是STATEMENT級別的快取
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // 清空快取
        clearLocalCache();
      }

配置:

<setting name="localCacheScope" value="STATEMENT"/>

CacheKey如何生存的

(1)在query方法中,呼叫了createCacheKey方法生成CacheKey,然後多次呼叫了cachekey的update方法,將標籤的ID、分頁資訊、SQL語句、引數等資訊作為引數傳入:

@Override
  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
    //設定ID,也就是標籤所在的Mapper的namespace + 標籤的id
    cacheKey.update(ms.getId());
    //偏移量,Mybatis自帶分頁類RowBounds中的屬性
    cacheKey.update(rowBounds.getOffset());
    //每次查詢大小,同樣是Mybatis自帶分頁類RowBounds中的屬性
    cacheKey.update(rowBounds.getLimit());
    //標籤中定義的SQL語句
    cacheKey.update(boundSql.getSql());
    //獲取引數
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // 處理SQL中的引數
    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);
      }
    }
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }

(2)通過原始碼,看一下CacheKey的update方法,update方法中記錄了呼叫update傳入引數的次數、每個傳入引數的hashcode之和checksum、以及計算CacheKey的成員變數hashcode的值。

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;//hashcode
  private long checksum;//update方法中傳入引數的hashcode之和
  private int count; //呼叫update方法向updatelist新增引數的的次數
  private List<Object> updateList;//呼叫update傳入的引數會被放到updateList

  public CacheKey() {
    //初始化
    this.hashcode = DEFAULT_HASHCODE;
    this.multiplier = DEFAULT_MULTIPLYER;
    this.count = 0;
    this.updateList = new ArrayList<Object>();
  }

  public CacheKey(Object[] objects) {
    this();
    updateAll(objects);
  }

  public int getUpdateCount() {
    return updateList.size();
  }

  public void update(Object object) {
    //獲取引數的hash值
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
    //計數
    count++;
    //hash值累加
    checksum += baseHashCode;
    //更新hash,hash=hash*count
    baseHashCode *= count;
    //計算CacheKey的hashcode
    hashcode = multiplier * hashcode + baseHashCode;
    //將引數新增到updateList
    updateList.add(object);
  }
......
}

(3)CacheKey中的成員變數的作用是什麼呢,接下來看一下它的equals方法,CacheKey中重寫了equals方法,CacheKey中的成員變數其實就是為了判斷兩個CacheKey的例項是否相同:

如果滿足以下條件,兩個CacheKey將判為不相同:

1. 要比較的物件不是CacheKey的例項

2. CacheKey物件中的hashcode不相同、count不相同、checksum不相同(它們之間是或的關係)

3. CacheKey物件的updateList成員變數不相同

總結:

如果Statement Id + Offset + Limmit + Sql + Params 都相同將被認為是相同的SQL,第一次將CacheKey作為HashMap中的key,資料庫返回的資料作為value放入到集合中,第二次查詢時由於被認為是相同的SQL,HashMap中已經存在該SQL的CacheKey物件,可直接從localCache中獲取資料來實現mybatis的一級快取。

@Override
  public boolean equals(Object object) {
    //如果物件為空
    if (this == object) {
      return true;
    }
    //如果不是CacheKey的例項
    if (!(object instanceof CacheKey)) {
      return false;
    }

    final CacheKey cacheKey = (CacheKey) object;
    //如果hashcode值不相同
    if (hashcode != cacheKey.hashcode) {
      return false;
    }
    // 如果checksum不相同
    if (checksum != cacheKey.checksum) {
      return false;
    }
    //如果count不相同
    if (count != cacheKey.count) {
      return false;
    }
    //對比兩個物件的updatelist中的值是否相同
    for (int i = 0; i < updateList.size(); i++) {
      Object thisObject = updateList.get(i);
      Object thatObject = cacheKey.updateList.get(i);
      //如果有不相同的,返回false
      if (!ArrayUtil.equals(thisObject, thatObject)) {
        return false;
      }
    }
    return true;
  }

總結:

(1)mybatis的一級快取是SqlSession級別的,不同的SqlSession不共享快取;

(2)mybatis一級快取是通過HashMap實現的,在PerpetualCache中定義,沒有容量控制;

(3)分散式環境下使用一級快取,資料庫寫操作會引起髒資料問題;