Myabtisソースコード分析4キャッシュモジュール分析、装飾モードの使用

一緒に書く習慣を作りましょう!「ナゲッツデイリーニュープラン・4月アップデートチャレンジ」に参加して9日目です。[クリックするとイベントの詳細が表示されます]

1.Mybatisキャッシュモジュールの分析

mybatisキャッシュモジュールには次の機能があります。

  1. MyBatisキャッシュの実装はマップに基づいており、キャッシュからのデータの読み取りと書き込みは、キャッシュモジュールのコア基本機能です。
  2. コア機能に加えて、次のような多くの追加機能があります。キャッシュの故障の防止、キャッシュクリア戦略(fifo、lru)の追加、シリアル化機能、ロギング機能、タイミングクリア機能など。
  3. 追加機能は、コアベース機能に任意の組み合わせで追加できます。

では、どうすればコア機能に機能を適切に追加できるでしょうか。

一部の学生は、継承の方法が追加の機能を拡張するために使用されることを知っているかもしれません。継承の方法は静的であり、ユーザーは動作を追加する方法とタイミングを制御できません。さらに、新しい関数の組み合わせは多数あり、継承を使用すると、多数のサブクラスが存在する可能性があります。

Mapのコアキャッシング機能に基づいて、ブロッキング、クリア戦略、シリアル化、ロギングなどを任意の組み合わせでエレガントに強化する機能は、Mybatisキャッシュモジュールの実装における最大の問題です。動的プロキシまたは継承メソッドには次の問題があります。これらのメソッドは静的であり、ユーザーは動作を追加する方法とタイミングを制御できません。さらに、新しい関数の組み合わせが複数あり、継承を使用すると、多数のサブクラス。要約すると、MyBtisキャッシュモジュールはデコレータパターンを使用してキャッシュモジュールを実装します。 

次に、デコレータモード 

1.デコレータパターンの概要

デコレータパターンは、継承の代わりに使用される手法であり、継承によってサブクラスを追加することなく、オブジェクトの新しい機能を拡張します。継承の代わりにオブジェクトの関連付け関係を使用します。これにより、柔軟性が高まり、型システムの急速な拡張が回避されます。デコレータUMLクラス図は次のとおり 
です。

 コンポーネントの意味は次のとおりです。

  • コンポーネント:コンポーネントインターフェイスは、すべてのコンポーネントクラスとデコレータによって実装される動作を定義します。 
  • 组件实现类(ConcreteComponent) :实现 Component 接口,组件实现类就是被装饰器 装饰的原始对象,新功能或者附加功能都是通过装饰器添加到该类的对象上的
  • 装饰器抽象类(Decorator) :实现 Component 接口的抽象类,在其中封装了一个 Component 对象,也就是被装饰的对象;
  • 具体装饰器类(ConcreteDecorator) :该实现类要向被装饰的对象添加某些功能; 

 我们很多人都玩过游戏,以DNF里的职业剑魂为例,装饰器模式图示如下: 

2、装饰器模式优点

装饰器相对于继承,装饰器模式灵活性更强,扩展性更强:  

  • 灵活性:装饰器模式将功能切分成一个个独立的装饰器,在运行期可以根据需要动态的 添加功能,甚至对添加的新功能进行自由的组合;
  • 扩展性:当有新功能要添加的时候,只需要添加新的装饰器实现类,然后通过组合方式 添加这个新装饰器,无需修改已有代码,符合开闭原则;

3、装饰器模式使用举例

  1. IO 中输入流和输出流的设计 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream("c://a.txt"))); 
  2. 对网络爬虫的自定义增强,可增强的功能包括:多线程能力、缓存、自动生成报表、黑 白名单、random 触发等 

三、装饰器在缓存模块的使用 

MyBatis 缓存模块是一个经典的使用装饰器实现的模块,类图如下:

  • Cache:Cache 接口是缓存模块的核 心接口,定义了缓存的基本操作; 
  • PerpetualCache:在缓存模块中扮演 ConcreteComponent 角色,使用 HashMap 来实现 cache 的相关操作; 
  • BlockingCache:阻塞版本的缓存装 饰器,保证只有一个线程到数据库去查 找指定的 key 对应的数据;BlockingCache 是阻塞版本的缓存装饰器,这个装饰器通过 ConcurrentHashMap 对锁的粒度 进行了控制,提高加锁后系统代码运行的效率(注:缓存雪崩的问题可以使用细粒度锁的方 式提升锁性能) 

源码分析:


/**
 * Simple blocking decorator 
 * 
 * Simple and inefficient version of EhCache's BlockingCache decorator.
 * It sets a lock over a cache key when the element is not found in cache.
 * This way, other threads will wait until this element is filled instead of hitting the database.
 * 
 * 阻塞版本的缓存装饰器,保证只有一个线程到数据库去查找指定的key对应的数据
 *
 *
 */
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;
  }  
}
复制代码

除了 BlockingCache 之外,缓存模块还有其他的装饰器如: 

  1. LoggingCache:日志能力的缓存; 
  2. ScheduledCache:定时清空的缓存; 
  3. BlockingCache:阻塞式缓存; 
  4. SerializedCache:序列化能力的缓存; 
  5. SynchronizedCache:进行同步控制的缓存; 

那么问题来了,我们知道HashMap是线程不安全的,那么Mybatis 的缓存功能使用 HashMap 实现会不会出现并发安全的问题呢? 

MyBatis 的缓存分为一级缓存、二级缓存。二级缓存是多个会话共享的缓存,确实会出 现并发安全的问题,因此 MyBatis 在初始化二级缓存时,会给二级缓存默认加上 SynchronizedCache 装饰器的增强,在对共享数据 HashMap 操作时进行同步控制,所以二级 缓存不会出现并发安全问题;而一级缓存是会话独享的,不会出现多个线程同时操作缓存数 据的场景,因此一级缓存也不会出现并发安全的问题; 

四、缓存的唯一标识 CacheKey 

MyBatis 中涉及到动态 SQL 的原因,缓存项的 key 不能仅仅通过一个 String 来表示,所以通 过 CacheKey 来封装缓存的 Key 值,CacheKey 可以封装多个影响缓存项的因素;
判断两个 CacheKey是否相同关键是比较两个对象的hash值是否一致;构成CacheKey对象的要素包括:

  1. mappedStatment 的 id 
  2. 指定查询结果集的范围(分页信息) 
  3. 查询所使用的 SQL 语句 
  4. 用户传递给 SQL 语句的实际参数值

CacheKey 中 update 方法和 equals 方法是进行比较时非常重要的两个方法:

  • update 方法:用于添加构成 CacheKey 对象的要素,每添加一个元素会对 hashcode、checksum、count 以及 updateList 进行更新;
  • equals 方法:用于比较两个元素是否相等。首先比较 hashcode、checksum、count 是否 相等,如果这三个值相等,会循环比较 updateList 中每个元素的 hashCode 是否一致;

 按照这种方式判断两个对象是否相等,一方面能很严格的判断是否一致避免出现误判, 另外一方面能提高比较的效率; 

おすすめ

転載: juejin.im/post/7084886691940892679
おすすめ