1. 程式人生 > >MyBatis的Cache機制解析

MyBatis的Cache機制解析

1. MyBatis快取機制的核心構件

1.1 Cache介面

介面簡單明瞭,Cache的基本操作;put/get/remove/clear。

public interface Cache {
  String getId();//分組ID
  void putObject(Object key, Object value);//put
  Object getObject(Object key);//get
  Object removeObject(Object key);//remove
  void clear();//clear
  int getSize();//可選 core不再使用
ReadWriteLock getReadWriteLock();//3.2.6後已廢棄 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

1.2 Cache的基本實現:PerpetualCache

通過HashMap實現快取管理。

public class PerpetualCache implements Cache {
  private String id;//ID進行分組
  private Map<Object, Object> cache = new HashMap<Object, Object>();//資料快取
//...實現介面,就是對HashMap的操作 }
  • 1
  • 2
  • 3
  • 4
  • 5

1.3 CacheKey的構成

核心功能,可以動態更新Key的hash值。因為底層快取資料是基於HashMap實現的,在比對value|key是否存在時,會呼叫hashCode方法和equals。
CacheKey覆寫了hashcode和equals。
比較順序:hashCode–>checksum–>count–>updateList,只要有一個不等則說明不是相同的Key。

public class CacheKey implements
Cloneable, Serializable {
//... private static final int DEFAULT_MULTIPLYER = 37; private static final int DEFAULT_HASHCODE = 17; private int multiplier; private int hashcode; private long checksum; private int count; private List<Object> updateList; public CacheKey() { this.hashcode = DEFAULT_HASHCODE; this.multiplier = DEFAULT_MULTIPLYER; this.count = 0; this.updateList = new ArrayList<Object>(); } //... //hashcode的計算方法 private void doUpdate(Object object) { int baseHashCode = object == null ? 1 : object.hashCode(); count++; checksum += baseHashCode; baseHashCode *= count; hashcode = multiplier * hashcode + baseHashCode;//hashcode的演算法 updateList.add(object); } //... //比較2個CacheKey是否相等 @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 (thisObject == null) { if (thatObject != null) { return false; } } else { if (!thisObject.equals(thatObject)) { return false; } } } return true; } //... }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72

1.4 MyBatis中CacheKey的建立

 @Override
  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
   //...
    CacheKey cacheKey = new CacheKey();
    cacheKey.update(ms.getId());
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    cacheKey.update(boundSql.getSql());
    //...
    for (ParameterMapping parameterMapping : parameterMappings) {
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        //...
        cacheKey.update(value);
      }
    }
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

Key的構成:cacheKey=ID + offset + limit + sql + parameterValues + environmentId

2. MyBatis的一級快取

一級快取,即本地快取,永久快取,Mybatis自行控制快取的讀寫刪,在執行器(BaseExecutor)中使用,執行器是在SqlSessionFactory.openSession()後建立的SqlSession中執行SQL語句時建立的,所以一級快取的生命週期等同於SqlSession,也就是說是Session級的快取。

2.1 一級快取的建立、清空和銷燬

MyBatis在commit和rollback時都會清空一級快取,在SqlSession關閉時也會主動清空快取,可以被GC掉。

 protected BaseExecutor(Configuration configuration, Transaction transaction) {
   //...
    this.localCache = new PerpetualCache("LocalCache");
    this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
   //...
  }

   @Override
  public void close(boolean forceRollback) {
    try {
     //...
    } finally {
     //...
      localCache = null;
      localOutputParameterCache = null;
      //...
    }
  }

   @Override
  public void commit(boolean required) throws SQLException {
   //...
    clearLocalCache();
    //...
    if (required) {
      transaction.commit();
    }
  }

  @Override
  public void rollback(boolean required) throws SQLException {
    if (!closed) {
      try {
        clearLocalCache();
       //...
      } finally {
        if (required) {
          transaction.rollback();
        }
      }
    }
  }


  @Override
  public void clearLocalCache() {
    if (!closed) {
      localCache.clear();
      localOutputParameterCache.clear();
    }
  }
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

2.2 一級快取的重新整理

快取必然是針對Select查詢操作提高查詢效率設計的,在Mapper XML的配置檔案中可以設定 flushCache=”ture”(預設為false)來重新整理一級快取(也包括二級快取),任何時候只要語句被呼叫,都會清空一級快取和二級快取。

2.3 一級快取的配置

mybatis-config.xml

<settings>
    //...
    <setting name="localCacheScope" value="SESSION | STATEMENT"/>
    //...
</settings>
   
  • 1
  • 2
  • 3
  • 4
  • 5

localCacheScope預設為SESSION,快取一個會話中執行的所有的查詢結果,如果設定為STATEMENT,則在同一個會話中的不同的呼叫將不會共享資料,在呼叫完畢後會清理一級快取。

//...
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
//...
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

2.4 巢狀查詢的優化

MyBatis利用一級快取機制加速重複巢狀查詢。

private static class DeferredLoad {

   //...
    public boolean canLoad() {
      return localCache.getObject(key) != null && localCache.getObject(key) != EXECUTION_PLACEHOLDER;
    }

    public void load() {
   //...
      List<Object> list = (List<Object>) localCache.getObject(key);
    //...
    }

  }
//...
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

3. MyBatis的二級快取

實現 org.mybatis.cache.Cache 介面都可以作為MyBatis的二級快取;二級快取的Cache物件由Configuration進行管理,而
每次構建SqlSessionFactory物件時都會建立新的Configuration物件,因此,二級快取的生命週期與SqlSessionFactory是相同的。基於Mapper XML 配置,在建立每個MapperedStatement物件時,都會根據其所屬的namespace名稱空間,給其分配Cache快取例項。

二級快取由CachingExecutor負責管理維護,開啟了二級快取後,Executor將使用CacheExecutor,基於裝飾器模式對配置的Executor進行包裝,擴充套件了快取功能。

 public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
   //...
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
   //...
  }
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

3.1 二級快取的配置

mybatis-config.xml配置,false則不啟用二級快取,ture則自動使用CachingExecutor。

<settings>
    //...
    <setting name="cacheEnabled" value="true|false" />
    //...
</settings>
   
  • 1
  • 2
  • 3
  • 4
  • 5

Mapper XML配置
xxx.mapper.xml

<cache
  type="PERPETUAL"//
  eviction="LRU"//演算法
  flushInterval="60000"//重新整理間隔,間隔60秒清空快取,被動觸發非定時器輪詢
  size="512"//大小
  readOnly="false"//true:返回cache結果的克隆物件
  blocking="false"
 />
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

根據快取器配置建立Cache例項,以namespace為CacheId,一個namespace對應一個Cache例項。
可以通過實現 org.mybatis.cache.Cache 介面建立自定義的快取器,比如基於Redis或者Memcached實現分散式快取。

3.2 MyBatis內建的二級快取演算法

Mybatis的所有Cache演算法都是基於裝飾器模式對PerpetualCache擴充套件增加功能。

FIFO:先入先出,基於LinkedList實現;

LRU:最近最少使用,基於LinkedHashMap實現,在put的時候,自動移除最少使用快取物件;

SOFT:對Cache的value進行SoftReference包裝;當快取物件是Soft reference可達時,gc會向系統申請更多記憶體,而不是直接回收它,當記憶體不足的時候才回收它;

WEAK:對Cache的value進行WeakReference包裝;WeakReference不會強制物件儲存在記憶體中。它擁有比較短暫的生命週期,允許你使用垃圾回收器的能力去權衡一個物件的可達性。在垃圾回收器掃描它所管轄的記憶體區域過程中,一旦gc發現物件是weakReference可達,就會把它放到ReferenceQueue中,等下次gc時回收它。

Mybatis Cache引數

  • flushInterval(重新整理間隔)可以被設定為任意的正整數,而且它們代表一個合理的毫秒形式的時間段。預設情況是不設定,也就是沒有重新整理間隔,快取僅僅呼叫語句時重新整理。
  • size(引用數目)可以被設定為任意正整數,要記住你快取的物件數目和你執行環境的可用記憶體資源數目。預設值是1024。
  • readOnly(只讀)屬性可以被設定為true或false。只讀的快取會給所有呼叫者返回快取物件的相同例項。因此這些物件不能被修改。這提供了很重要的效能優勢。可讀寫的快取會返回快取物件的拷貝(通過序列化)。這會慢一些,但是安全,因此預設是false。

如下例子:

<cache  eviction="FIFO"  flushInterval="60000"  size="512"  readOnly="true"/>
   
  • 1

這個更高階的配置建立了一個 FIFO 快取,並每隔 60 秒重新整理,存數結果物件或列表的 512 個引用,而且返回的物件被認為是隻讀的,因此在不同執行緒中的呼叫者之間修改它們會導致衝突。

可用的收回策略有, 預設的是 LRU:

1.LRU – 最近最少使用的:移除最長時間不被使用的物件。
2.FIFO – 先進先出:按物件進入快取的順序來移除它們。
3.SOFT – 軟引用:移除基於垃圾回收器狀態和軟引用規則的物件。
4.WEAK – 弱引用:更積極地移除基於垃圾收集器狀態和弱引用規則的物件。
   
  • 1
  • 2
  • 3
  • 4