Mybatis系列5-緩存源碼分析

1.裝飾器模式

mybatis緩存模塊用了裝飾器模式,裝飾器模式就是用來在類的原有功能基礎上添加新功能。
裝飾器模式中又四個角色:

  • 組件接口:Component,定義了組件的行爲
  • 組件實現類:ConcreteComponent,也就是被裝飾的對象
  • 裝飾器抽象類:Decorator,也實現了組件接口,並且持有一個被裝飾的對象
  • 裝飾器實現類:ConcreteDecorator,實現了裝飾器
    在這裏插入圖片描述
    是不是感覺和適配器優點類似,都持有了原對象引用,不過兩個設計模式用途不一樣,裝飾器是給原有類增加新功能,有點像代理的增強。適配器是把一個對象封裝適配成另外一個對象。

2.mybatis的緩存

我們知道緩存簡單來說就是個map對象,裏面放着key,value的數據。當然redis等專用緩存複雜的多,要考慮持久化。mybatis的緩存比較簡單,就是個map對象。我們平時使用緩存的時候會先查詢緩存中是否有,如果有,則返回。沒有則查詢數據庫。但是這樣會有緩存雪崩、緩存擊穿、緩存穿透的問題。這些我在redis系列中講過。
我們這裏以mybatis的BlockingCache爲例。這個類在原有緩存基礎上增加了防止緩存擊穿的功能。
(1)BlockingCache
BlockingCache實現類Cache接口,內部持有一個Cache引用。

public class BlockingCache implements Cache {

  //阻塞的超時時長
  private long timeout;
  //被裝飾的底層對象,一般是PerpetualCache
  private final Cache delegate;
  //鎖對象集,粒度到key值
  private final ConcurrentHashMap<Object, ReentrantLock> locks;

  public BlockingCache(Cache delegate) {
    this.delegate = delegate;
    this.locks = new ConcurrentHashMap<>();
  }

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

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

  @Override
  public void putObject(Object key, Object value) {
    try {
      delegate.putObject(key, value);
    } finally {
      releaseLock(key);
    }
  }

  @Override
  public Object getObject(Object key) {
    acquireLock(key);//根據key獲得鎖對象,獲取鎖成功加鎖,獲取鎖失敗阻塞一段時間重試
    Object value = delegate.getObject(key);
    if (value != null) {//獲取數據成功的,要釋放鎖
      releaseLock(key);
    }        
    return value;
  }

  @Override
  public Object removeObject(Object key) {
    // despite of its name, this method is called only to release locks
    releaseLock(key);
    return null;
  }

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

  @Override
  public ReadWriteLock getReadWriteLock() {
    return null;
  }
  
  private ReentrantLock getLockForKey(Object key) {
    ReentrantLock lock = new ReentrantLock();//創建鎖
    ReentrantLock previous = locks.putIfAbsent(key, lock);//把新鎖添加到locks集合中,如果添加成功使用新鎖,如果添加失敗則使用locks集合中的鎖
    return previous == null ? lock : previous;
  }
  
//根據key獲得鎖對象,獲取鎖成功加鎖,獲取鎖失敗阻塞一段時間重試
  private void acquireLock(Object key) {
	//獲得鎖對象
    Lock lock = getLockForKey(key);
    if (timeout > 0) {//使用帶超時時間的鎖
      try {
        boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
        if (!acquired) {//如果超時拋出異常
          throw new CacheException("Couldn't get a lock in " + timeout + " for the key " +  key + " at the cache " + delegate.getId());  
        }
      } catch (InterruptedException e) {
        throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);
      }
    } else {//使用不帶超時時間的鎖
      lock.lock();
    }
  }
  
  private void releaseLock(Object key) {
    ReentrantLock lock = locks.get(key);
    if (lock.isHeldByCurrentThread()) {
      lock.unlock();
    }
  }

  public long getTimeout() {
    return timeout;
  }

  public void setTimeout(long timeout) {
    this.timeout = timeout;
  }  
}

(2)get方法
get方法會首先通過acquireLock獲得鎖
(3)acquireLock

 private void acquireLock(Object key) {
	//獲得鎖對象
    Lock lock = getLockForKey(key);
    if (timeout > 0) {//使用帶超時時間的鎖
      try {
        boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
        if (!acquired) {//如果超時拋出異常
          throw new CacheException("Couldn't get a lock in " + timeout + " for the key " +  key + " at the cache " + delegate.getId());  
        }
      } catch (InterruptedException e) {
        throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);
      }
    } else {//使用不帶超時時間的鎖
      lock.lock();
    }
  }

(4)getLockForKey方法

 private ReentrantLock getLockForKey(Object key) {
    ReentrantLock lock = new ReentrantLock();//創建鎖
    ReentrantLock previous = locks.putIfAbsent(key, lock);//把新鎖添加到locks集合中,如果添加成功使用新鎖,如果添加失敗則使用locks集合中的鎖
    return previous == null ? lock : previous;
  }

這裏的locks是個map對象,key就是傳入的參數key,value就是鎖。也就是說一個key,一把鎖。
這裏只有獲取鎖的線程纔可以拿數據。如果緩存中沒有就可以取數據庫,放回緩存,然後釋放鎖。這樣後續的線程就可以直接從緩存中獲取值,防止緩存擊穿。

3.緩存key

Mybatis中涉及到動態SQL的原因,緩存項的key不能僅僅通過一個String來表示,所以通過CacheKey來封裝緩存的Key值,CacheKey可以封裝多個影響緩存項的因素。
構成CacheKey的對象:

  • mappedStatment的id
  • 分頁信息
  • 查詢所使用的SQL語句
  • 用戶傳遞給SQL語句的實際參數值
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章