1. 程式人生 > >mybatis-3.4.x 從原始碼看快取的使用[筆記三]

mybatis-3.4.x 從原始碼看快取的使用[筆記三]

從原始碼看mybatis快取

  1. 簡單看下SqlSession的建立
  //DefaultSqlSessionFactory.java
  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      //事務管理器
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      //執行器 由Executor處理快取,見下文
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

通過裝飾器模式,包裝Executor,豐富Executor的功能

  /*詳見Configuration.java*/
  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;

    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    //預設為true,包裝成快取執行器
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    //成為攔截器代理物件
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

CachingExecutor對查詢的處理,處理二級快取

  /*詳見CachingExecutor.java*/
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    //獲取mapper對應的快取
    Cache cache = ms.getCache();
    if (cache != null) {
      //如果需要重新整理快取就清掉二級快取
      flushCacheIfRequired(ms);
      //如果使用快取,且沒有resultHandler則先試著從快取讀取結果
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          //沒有快取,則由代理繼續執行後續步驟
          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
  
  

基類 BaseExecutor 對查詢的處理【處理一級快取】

 /*詳見BaseExecutor.java**/
  @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.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      //從一級快取讀取查詢結果
      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();
      //如果LocalCacheScope為STATEMENT,則不快取
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

快取的的key CacheKey

/*預設實現*/
public class PerpetualCache implements Cache {

  private final String id;
  //存放快取的資料
  private Map<Object, Object> cache = new HashMap<Object, Object>();
  ...

hashMap判斷key是否相等

 if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
 ...
 hash值相等 並且 記憶體地址相等 或者 equals返回true

mybatis CacheKey 實現

package org.apache.ibatis.cache;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import org.apache.ibatis.reflection.ArrayUtil;

/**
 * @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;
  // 8/21/2017 - Sonarlint flags this as needing to be marked transient.  While true if content is not serializable, this is not always true and thus should not be marked transient.
  private List<Object> 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) {
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); 

    count++;
    checksum += baseHashCode;
    baseHashCode *= count;

    hashcode = multiplier * hashcode + baseHashCode;

    updateList.add(object);
  }

  public void updateAll(Object[] objects) {
    for (Object o : objects) {
      update(o);
    }
  }

 /*重寫equals*/
  @Override
  public boolean equals(Object object) {
    if (this == object) {
      return true;
    }
    if (!(object instanceof CacheKey)) {
      return false;
    }

    final CacheKey cacheKey = (CacheKey) object;

    if (hashcode != cacheKey.hashcode) {
      return false;
    }
    if (checksum != cacheKey.checksum) {
      return false;
    }
    if (count != cacheKey.count) {
      return false;
    }

    for (int i = 0; i < updateList.size(); i++) {
      Object thisObject = updateList.get(i);
      Object thatObject = cacheKey.updateList.get(i);
      if (!ArrayUtil.equals(thisObject, thatObject)) {
        return false;
      }
    }
    return true;
  }

  /*重寫hashCode*/
  @Override
  public int hashCode() {
    return hashcode;
  }

  @Override
  public String toString() {
    StringBuilder returnValue = new StringBuilder().append(hashcode).append(':').append(checksum);
    for (Object object : updateList) {
      returnValue.append(':').append(ArrayUtil.toString(object));
    }
    return returnValue.toString();
  }

  @Override
  public CacheKey clone() throws CloneNotSupportedException {
    CacheKey clonedCacheKey = (CacheKey) super.clone();
    clonedCacheKey.updateList = new ArrayList<Object>(updateList);
    return clonedCacheKey;
  }

}

 /**詳見BaseExecutor.java*/
  @Override
  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
    //sql的編號
    cacheKey.update(ms.getId());
    //獲取的資料位置
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    //查詢的sql
    cacheKey.update(boundSql.getSql());
    //查詢的引數
    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);
        }
        cacheKey.update(value);
      }
    }
    if (configuration.getEnvironment() != null) {
      // issue #176
      //查詢的環境
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }
  1. 從上面的原始碼中簡單看下一級快取,二級快取的區別

作用域

executor 由sqlSession持有,所以localCache是在session內共享的

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;
  ...

從上文中 【CachingExecutor對查詢的處理,處理二級快取】可以發現二級快取來源於MappedStatement,這個物件只跟mapper相關,必須位於同一個名稱空間或者指定一個引用的名稱空間的快取

所以二級快取的作用域會比一級快取的小,在mapper範圍內

啟用方式

一級快取

public class Configuration {

  ...
  //一級快取 預設作用域SESSION範圍 
  protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
  ...

如果設定為 localCacheScope = LocalCacheScope.STATEMENT;一級快取就會失效,從上文的【基類 BaseExecutor 對查詢的處理【處理一級快取】】中可以看到處理的原始碼

二級快取

public class Configuration {

  ...
  //二級快取預設開啟
  protected boolean cacheEnabled = true;
  ...

從上文【通過裝飾器模式,包裝Executor,豐富Executor的功能】中看到只有cacheEnabled為true時才會使用二級快取的包裝類

3.簡單使用示例

一級快取

/*公共測試類**/
public class BaseTest {

    protected SqlSessionFactory sqlSessionFactory;
    protected SqlSession sqlSession;

    @Before
    public void init(){
        InputStream inputStream;
        try {
            System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
            inputStream = Resources.getResourceAsStream("mybatis.xml");
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            sqlSession = sqlSessionFactory.openSession();
        } catch (IOException e) {
            //nothing to do
        }
    }

    @After
    public void close(){
        sqlSession.close();
    }
}

測試使用一級快取

關閉二級快取
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
...
    <settings>
        <setting name="cacheEnabled" value="false"/>
    </settings>
   ...
</configuration>
public class CacheTest extends BaseTest {
    /*
    * 測試一級快取
    * */
    @Test
    public void testCache1(){
        CachedAuthorMapper cachedAuthorMapper = sqlSession.getMapper(CachedAuthorMapper.class);
        cachedAuthorMapper.search(1,1);
        cachedAuthorMapper.search(1,1);
    }
}
執行結果
DEBUG [main] - ==>  Preparing: select p.id as post_id,a.id,a.author_id,a.title,r.username,p.`comment` from article a,author r,post p WHERE 1 = 1 and a.author_id = r.id and p.article_id = a.id and p.article_id = ? 
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <==      Total: 2

查詢兩次 只執行了一次資料庫操作

測試關閉一級快取

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="localCacheScope" value="STATEMENT"/>
        <setting name="cacheEnabled" value="false"/>
    </settings>
</configuration>
執行結果
DEBUG [main] - ==>  Preparing: select p.id as post_id,a.id,a.author_id,a.title,r.username,p.`comment` from article a,author r,post p WHERE 1 = 1 and a.author_id = r.id and p.article_id = a.id and p.article_id = ? 
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <==      Total: 2
DEBUG [main] - ==>  Preparing: select p.id as post_id,a.id,a.author_id,a.title,r.username,p.`comment` from article a,author r,post p WHERE 1 = 1 and a.author_id = r.id and p.article_id = a.id and p.article_id = ? 
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <==      Total: 2

查詢了兩次

測試二級快取的使用

關閉一級快取

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="localCacheScope" value="STATEMENT"/>
    </settings>
</configuration>

配置mapper啟用快取
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="test.CachedAuthorMapper">
 ...
  <cache/>
  ...
 </mapper>

使用二級快取稍有區別

public class CacheTest extends BaseTest {
    
    /*
     * 測試二級快取
     * */
    @Test
    public void testCache2(){
        CachedAuthorMapper cachedAuthorMapper = sqlSession.getMapper(CachedAuthorMapper.class);
        cachedAuthorMapper.search(1,1);
        //必須執行,否則二級快取不會生效
        sqlSession.commit();
        cachedAuthorMapper.search(1,1);
    }
}

為什麼需要執行commit快取才會生效,個人理解是避免快取髒資料

package org.apache.ibatis.cache.decorators;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReadWriteLock;

import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;

public class TransactionalCache implements Cache {

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

  //真正的快取物件
  private final Cache delegate;
  //是否提交事務的時候清空快取
  private boolean clearOnCommit;
  //待新增到快取的資料
  private final Map<Object, Object> entriesToAddOnCommit;
  //快取裡沒有的key
  private final Set<Object> entriesMissedInCache;

  public TransactionalCache(Cache delegate) {
    this.delegate = delegate;
    this.clearOnCommit = false;
    this.entriesToAddOnCommit = new HashMap<Object, Object>();
    this.entriesMissedInCache = new HashSet<Object>();
  }

  @Override
  public String getId() {
    return delegate.getId();
  }

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

  @Override
  public Object getObject(Object key) {
    // issue #116
    Object object = delegate.getObject(key);
    if (object == null) {
      entriesMissedInCache.add(key);
    }
    // issue #146
    if (clearOnCommit) {
      return null;
    } else {
      return object;
    }
  }

  @Override
  public ReadWriteLock getReadWriteLock() {
    return null;
  }

  /**
   * 新增到entriesToAddOnCommit集合
   * @param key Can be any object but usually it is a {@link CacheKey}
   * @param object
   */
  @Override
  public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
  }

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

  @Override
  public void clear() {
    clearOnCommit = true;
    entriesToAddOnCommit.clear();
  }

  /**
   * 提交的時候重新整理之前的待快取資料到實際快取中
   */
  public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    flushPendingEntries();
    reset();
  }

  public void rollback() {
    unlockMissedEntries();
    reset();
  }

  private void reset() {
    clearOnCommit = false;
    entriesToAddOnCommit.clear();
    entriesMissedInCache.clear();
  }

  /**
   * 新增到實際快取
   */
  private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
  }

  private void unlockMissedEntries() {
    for (Object entry : entriesMissedInCache) {
      try {
        delegate.removeObject(entry);
      } catch (Exception e) {
        log.warn("Unexpected exception while notifiying a rollback to the cache adapter."
            + "Consider upgrading your cache adapter to the latest version.  Cause: " + e);
      }
    }
  }

}

mapper配置快取有兩種方式 cache-ref,cache

cache 上面使用了,一般都是這種方式,那麼cache-ref有什麼應用場景呢

很多時候我們的操作可能不是那麼單一,也不是唯一一個地方能引起快取的變化,比如有些中間表,可能就會出現在不同的mapper對映中,那麼這時候如果單獨放在自己的名稱空間的快取下勢必會產生一些資料不一致問題【小注:一級快取不會產生這種問題,因為任何的mapper操作資料庫的更新,都會引起快取的重新整理】,那麼這些個有關聯性的mapper對映就可以引用同一個快取,來達到快取一致性,因為無論是哪個mapper的更新操作都會重新整理他們共有的快取