zoukankan      html  css  js  c++  java
  • 开源框架是如何使用设计模式的-MyBatis缓存机制之装饰者模式

    写在前面

    聊一聊MyBatis是如何使用装饰者模式的,顺便回顾下缓存的相关知识,可以看看右侧目录一览内容概述。

    装饰者模式

    这里就不聊它的概念了,总结下就是套娃。利用组合的方式将装饰器组合进来,增强共同的抽象方法(与代理很类似但是又更灵活)

    MyBatis缓存

    回忆下传统手艺

      <!-- 先进先出,60秒刷新一次,可存储512个引用,返回对象只读,不同线程中的调用者之间修改会导致冲突 -->
     <cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
    

    粗略回顾下MyBatis缓存

    一级缓存

    MyBatis的一级缓存存在于SqlSession的生命周期中,在同一个SqlSession中查询时,MyBatis会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个Map对象中。如果同一个SqlSession中执行的方法和参数完全一致,那么通过算法会生成相同键值,当Map缓存对象中已经存在该键值时,则会返回缓存中的对象。

    默认开启

    二级缓存

    MyBatis的二级缓存非常强大,它不同于一级缓存只存在于SqlSession的生命周期中,而是可以理解为存在于SqlSessionFactory的生命周期中。

    默认不开启,需要如下配置后开启全局配置,再在对应的Mapper.xml中添加“传统手艺”-标签

    <settings>
      <setting name = "cacheEnabled" value="true"/> 
    </settings>
    
     <cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
    

    另一种开启方式-注解

    @CacheNamespace(
      eviction = FifoCache.class,
      flushInterval = 60000,
      size = 512,
      readWrite = true
    )
    public interface RoleMapper {
      // 接口方法
    }
    
    • eviction(收回策略)
      • LRU(最近最少使用的):移除长时间不使用的对象,这是默认值
      • FIFO(先进先出):按对象进入缓存的顺序来移除它们
      • SOFT(软引用):移除基于垃圾回收器状态和软引用规则的对象
      • WEAK(弱引用):更积极地移除基于垃圾收集器状态和弱引用规则的对象
    • flushInterval(刷新间隔)
    • size(引用数目)
    • readOnly(只读)只读的缓存会给所有调用者返回缓存的相同实例,因此这些对象不能被修改,这提供了很重要的性能优势。可读写的缓存会通过序列化返回缓存对象的拷贝,这种方式会慢一些,但是安全,因此默认是false

    集成第三方缓存

    MyBatis还支持通过“type”来集成第三方缓存,如下就是集成了Redis缓存,这样就从本地缓存跳跃到了分布式缓存了。

    <mapper namespace="xxx.xxx.xxx.mapper.RoleMapper">
      <!-- 集成Redis缓存-->
      <cache type="org.mybatis.caches.redis.RedisCache" />
    </mapper>
    

    二级缓存的问题-脏数据

    二级缓存虽然能提高应用效率,减轻数据库服务器的压力,但是如果使用不当,很容易产生脏数据

    MyBatis的二级缓存是和命名空间绑定的,所以通常情况下每一个Mapper映射文件都拥有自己的二级缓存,不同Mapper的二级缓存互不影响。在常见的数据库操作中,多表联合查询非常常见,由于关系型数据库的设计,使得很多时候需要关联多个表才能获得想要的数据。在关联多表查询时肯定会将查询放到某个命名空间下的映射文件中,这样一个多表的查询就会缓存在该命名空间的二级缓存中。涉及这些表的增删改操作通常不在一个映射文件中,它们的命名空间不同,因此当有数据变化时,多表查询的缓存未必会被清空,这种情况下就会产生脏数据。

    基于MyBatis缓存机制结合源码解析装饰器模式

    Cache接口:
    Cache接口

    Cache核心方法:

    • putObject
    • getObject
    • removeObject

    DEMO-实战使用MyBatis的装饰者模式

        public static void main(String[] args) {
            final String cacheKey = "cache";
            final Cache cache = new LoggingCache(new BlockingCache(new PerpetualCache(cacheKey)));
            Object cacheValue = cache.getObject(cacheKey);
            if (Objects.isNull(cacheValue)) {
                log.debug("缓存未命中 >>>>>>>>> key:[{}]", cacheKey);
                cache.putObject(cacheKey, "MyCacheValue");
            }
    
            cacheValue = cache.getObject(cacheKey);
            log.debug("缓存命中 >>>>>>>>> key:[{}],value:[{}]", cacheKey, cacheValue);
        }
    

    如代码所示,是不是看到了“装饰者模式”的影子了,在构造函数中疯狂套娃。使用的是MyBatis的API,给基本缓存组件装饰了“日志打印”、“阻塞“的能力。
    结果演示:
    缓存Demo结果演示
    可以看到,LogginCache在读缓存的时候还会打印出缓存命中率。 好了,接下来进入正题,看看其他缓存是怎么实现的吧。以下源码基于MyBatis3.4.5

    PerpetualCache

      private final Map<Object, Object> cache = new HashMap<>();
    
      @Override
      public void putObject(Object key, Object value) {
        cache.put(key, value);
      }
    
      @Override
      public Object getObject(Object key) {
        return cache.get(key);
      }
    
      @Override
      public Object removeObject(Object key) {
        return cache.remove(key);
      }
    

    这是MyBatis的基础缓存,套娃的基本得有它,它的核心就是个HashMap来作为缓存容器,其实现的Cache接口的几个核心方法也都是委托给了HashMap去做。

    FifoCache

    一个支持先进先出的缓存策略的MyBatisCache

      private final Cache delegate;
      //维护一个key的双端队列
      private final Deque<Object> keyList;
      private int size;
    
      public FifoCache(Cache delegate) {
        //通过构造函数,将Cache组合进来,取名”委托“
        this.delegate = delegate;
        this.keyList = new LinkedList<>();
        this.size = 1024;
      }
    
      @Override
      public void putObject(Object key, Object value) {
        //先走自己的增强
        cycleKeyList(key);
        //真实的写缓存交给”委托“去做
        delegate.putObject(key, value);
      }
    
      @Override
      public Object getObject(Object key) {
        return delegate.getObject(key);
      }
    
      @Override
      public Object removeObject(Object key) {
        return delegate.removeObject(key);
      }
    
      private void cycleKeyList(Object key) {
        //将新写的缓存key添加到双端队列末尾
        keyList.addLast(key);
        // 如果key的大小大于了1024(构造函数中默认赋值1024)则会移除最早添加的缓存
        // 1. 移除自身维护的key队列的队头 2.委托给“委托”去真实删除队头缓存对象
        if (keyList.size() > size) {
          Object oldestKey = keyList.removeFirst();
          delegate.removeObject(oldestKey);
        }
      }
    

    以上就是MyBatis先进先出缓存的实现了,FifoCache维护了key的双端队列,每次写缓存的时候会判断大小如果大于阈值则会先移除队头的key,再委托给组合进来的Cache来删除对应缓存操作,完成“先进先出”的增强(装饰)

    LruCache

    一个支持LRU(Least Recently Used ,最近最少使用)缓存策略的MyBatisCache

    回忆下缓存策略

    • LRU:Least Recently Used,最近最少使用
    • LFU:Least Frequently Used,最近不常被使用

    LRU 算法有一个缺点,比如说很久没有使用的一个键值,如果最近被访问了一次,那么即使它是使用次数最少的缓存,它也不会被淘汰;而 LFU 算法解决了偶尔被访问一次之后,数据就不会被淘汰的问题,它是根据总访问次数来淘汰数据的,其核心思想是“如果数据过去被访问多次,那么将来它被访问次数也会比较多”。因此 LFU 可以理解为比 LRU 更加合理的淘汰算法。

    回忆下LinkedHashMap的核心机制-LRU

    LinkedHashMap相比HashMap多了两个节点,before,after这样就能够维护节点之间的顺序了。

    我们看看LinkedHashMap的get方法,它内部有LinkedHashMap开启LRU机制的秘密。

        public V get(Object key) {
            Node<K,V> e;
            if ((e = getNode(hash(key), key)) == null)
                return null;
            if (accessOrder)  // 为true则会执行afterNodeAccess(将节点移动到队尾)
                afterNodeAccess(e);
            return e.value;
        }
    
        void afterNodeAccess(Node<K,V> e) { // move node to last  (官方注释 言简意赅 -> 将节点移动到队尾)
            LinkedHashMap.Entry<K,V> last;
            if (accessOrder && (last = tail) != e) {
                LinkedHashMap.Entry<K,V> p =
                    (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
                p.after = null;
                if (b == null)
                    head = a;
                else
                    b.after = a;
                if (a != null)
                    a.before = b;
                else
                    last = b;
                if (last == null)
                    head = p;
                else {
                    p.before = last;
                    last.after = p;
                }
                tail = p;
                ++modCount;
            }
        }
    

    那么这个accessOrder变量是怎么维护的呢?看代码

        public LinkedHashMap(int initialCapacity,
                             float loadFactor,
                             boolean accessOrder) {
            super(initialCapacity, loadFactor);
            this.accessOrder = accessOrder;
        }
    

    你会发现,LinkedHashMap有这么一个构造函数,第三个参数便是accessOrder,所以决定是否开启LRU是你在运行时传参决定的!开启后则会在每次读取键值对之后将读取的节点移动至队尾,那么队头就是最近最少使用的了,队尾就是刚刚使用的了,当需要删除最近最少使用的节点的时候,直接删除队头的即可。

    回忆下LinkedHashMap的核心方法-removeEldestEntry

    LinkedHashMap是一个有顺序的HashMap,它可以使得你的k,v能够按照某种顺序写入和读取,它的核心方法removeEldestEntry功不可没。

    在HashMap新增k,v之后会回调一个方法“afterNodeInsertion”,这个方法在HashMap中是一个空实现(俗称钩子方法),它的子类LinkedHashMap重写了它,代码如下。

        void afterNodeInsertion(boolean evict) { // possibly remove eldest     这是官方注释,言简意赅(可能会删除老key)
            LinkedHashMap.Entry<K,V> first;
            //前面的短路方法不管,我们关注removeEldestEntry方法 -> 如果该方法也返回true,则会走方法体中的removeNode方法(删除first节点的元素)。
            // 当开启LinkedHashMap的LRU模式,则队头的元素是“最近最少使用的元素”,因为每次读取k,v后都会将元素调整至队尾,所以队头的元素是“最近最少使用的元素“
            if (evict && (first = head) != null && removeEldestEntry(first)) {
                K key = first.key;
                removeNode(hash(key), key, null, false, true);
            }
        }
    
    

    进入正题

      private final Cache delegate;
      // 维护一个key和value都是缓存key的map
      private Map<Object, Object> keyMap;
      //最近最少使用的Key
      private Object eldestKey;
    
      public LruCache(Cache delegate) {
        //通过构造函数,将Cache组合进来,取名”委托“
        this.delegate = delegate;
        //初始化keyMap(重要)
        setSize(1024);
      }
    
      public void setSize(final int size) {
        // 构造函数第三个参数传递true(accessOrder),如上所述将开启LRU模式
        keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
          private static final long serialVersionUID = 4267176411845948333L;
            
          // 重写了LinkedHashMap的方法
          @Override
          protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
            boolean tooBig = size() > size;
            if (tooBig) {
              // 大小超过阈值,将队头(最近最少使用)的key更新至自身维护的"eldestKey" (重要)
              eldestKey = eldest.getKey();
            }
            return tooBig;
          }
        };
      }
    
      @Override
      public void putObject(Object key, Object value) {
        // 委托写入缓存
        delegate.putObject(key, value);
       // 删除最近最少使用的缓存
        cycleKeyList(key);
      }
    
      @Override
      public Object getObject(Object key) {
        keyMap.get(key); // touch
        return delegate.getObject(key);
      }
    
      @Override
      public Object removeObject(Object key) {
        return delegate.removeObject(key);
      }
    
      private void cycleKeyList(Object key) {
        // 因为重写了LinkedHashMap的removeEldestEntry方法,如上所述,超过阈值后eldestKey指向的就是最近最少使用的key
        keyMap.put(key, key);
        if (eldestKey != null) {
          // 委托移除最近最少使用的缓存
          delegate.removeObject(eldestKey);
          // 置空
          eldestKey = null;
        }
      }
      
    

    以上就是MyBatis中的LRU缓存的机制了,自身维护了一个LinkedHashMap,开启了LRU机制,重写了removeEldestEntry方法,当大小触发阈值的时候维护最近最少使用的元素key,委托给组合进来的Cache对象移除,整个流程下来就使得被装饰着有了LRU的增强。

    SoftCache

    一个软引用的MyBatisCache

    弱引用

    弱引用比强引用稍弱一些。当JVM内存不足时,GC才会回收那些只被软引用指向的对象,从而避免OutOfMemoryError。当GC将只被软引用指向的对象全部回收之后,内存依然不足时,JVM才会抛出OutOfMemoryError。(这一特性非常适合做缓存,毕竟最终数据源在DB,还能保护JVM进程)

      // 维护最近经常使用的缓存数据,该集合会使用强引用指向其中的每个缓存Value,防止被GC回收
      private final Deque<Object> hardLinksToAvoidGarbageCollection;
      //与SortEntry对象关联,用于记录已经被回收的缓存条目
      private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;
      private final Cache delegate;
      //强引用的个数,默认256。即有256个热点数据无法直接被GC回收
      private int numberOfHardLinks;
    
      public SoftCache(Cache delegate) {
        this.delegate = delegate;
        this.numberOfHardLinks = 256;
        this.hardLinksToAvoidGarbageCollection = new LinkedList<Object>();
        this.queueOfGarbageCollectedEntries = new ReferenceQueue<Object>();
      }
    
      @Override
      public void putObject(Object key, Object value) {
        // 同步删除已经被GC回收的Value
        removeGarbageCollectedItems();
        delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries));
      }
    
      private static class SoftEntry extends SoftReference<Object> {
        private final Object key;
        
        SoftEntry(Object key, Object value, ReferenceQueue<Object> garbageCollectionQueue) {
          // 关联引用队列。
         // 当SoftReference指向的对象被回收的时候,JVM就会将这个SoftReference作为通知,添加到与其关联的引用队列
          super(value, garbageCollectionQueue);
          this.key = key;
        }
      }
    
    
      @Override
      public Object getObject(Object key) {
        Object result = null;
        @SuppressWarnings("unchecked") // assumed delegate cache is totally managed by this cache
        SoftReference<Object> softReference = (SoftReference<Object>) delegate.getObject(key);     // 委托获取缓存
        if (softReference != null) {
          result = softReference.get();
          if (result == null) {
            // 重要的一步!判断Value是否为空,为空则表示弱引用指向的对象已经被GC回收了,就需要同步删除该缓存。
            delegate.removeObject(key);
          } else {
            // See #586 (and #335) modifications need more than a read lock 
            // 读取缓存后,维护“强引用”的数据。
            synchronized (hardLinksToAvoidGarbageCollection) {
              hardLinksToAvoidGarbageCollection.addFirst(result);   // 将缓存添加进强引用队列(热点数据)
              if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {
                hardLinksToAvoidGarbageCollection.removeLast();   // 维护队列个数  
              }
            }
          }
        }
        return result;
      }
    
      @Override
      public Object removeObject(Object key) {
        removeGarbageCollectedItems();  // 删除被GC回收的Value
        return delegate.removeObject(key);    // 委托删除缓存
      }
    
      private void removeGarbageCollectedItems() {
        SoftEntry sv;
        // 引用关联的队列如果有值,则说明有被GC回收的Value
        while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) {
          delegate.removeObject(sv.key);
        }
      }
    
    

    WeakCache

    一个弱引用的MyBatisCache
    与弱引用类似(基本相同),不过多介绍了。

    弱引用

    弱引用比软引用的引用强度还要弱。弱引用可以引用一个对象,但无法阻止这个对象被GC回收,也就是说,在JVM进行垃圾回收的时候,若发现某个对象只有一个弱引用指向它,那么这个对象会被GC立刻回收。(即遇GC比死,存活的时间为两次GC之间)

      // Entry继承的是WeakReference。
      // 其他内容参考弱引用Cache
      private static class WeakEntry extends WeakReference<Object> {
        private final Object key;
        
        private WeakEntry(Object key, Object value, ReferenceQueue<Object> garbageCollectionQueue) {
          super(value, garbageCollectionQueue);
          this.key = key;
        }
      }
    

    LoggingCache

    一个支持打印Debug级别的缓存命中率的MyBatisCache

      // 日志打印的log对象
      private final Log log;  
      private final Cache delegate;
      // 请求数
      protected int requests = 0;
      // 缓存命中数
      protected int hits = 0;
    
        public LoggingCache(Cache delegate) {
        //通过构造函数,将Cache组合进来,取名”委托“
        this.delegate = delegate;
        //log通过缓存id作为表示
        this.log = LogFactory.getLog(getId());
      }
    
      @Override
      public void putObject(Object key, Object object) {
        delegate.putObject(key, object);
      }
    
      @Override
      public Object getObject(Object key) {
        requests++;   // 请求数增加
        final Object value = delegate.getObject(key);
        if (value != null) {
          hits++;  // 缓存命中,命中数增加
        }
        if (log.isDebugEnabled()) {
          log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());   // 打印缓存命中率
        }
        return value;
      }
    
      @Override
      public Object removeObject(Object key) {
        return delegate.removeObject(key);
      }
    
      private double getHitRatio() {
        // 计算缓存命中率
        return (double) hits / (double) requests;
      }
    

    LoggingCache使得缓存读取的时候能够有缓存命中率的日志打印,挺实用的增强。

    BlockingCache

    一个支持阻塞的MyBatisCache

      private long timeout;
      private final Cache delegate;
      //每个key都有自己的ReentrantLock
      private final ConcurrentHashMap<Object, ReentrantLock> locks;
    
      public BlockingCache(Cache delegate) {
        this.delegate = delegate;
        this.locks = new ConcurrentHashMap<Object, ReentrantLock>();
      }
    
      @Override
      public void putObject(Object key, Object value) {
        try {
          delegate.putObject(key, value);    // 委托写入缓存
        } finally {
          releaseLock(key);    // 释放锁
        }
      }
    
      @Override
      public Object getObject(Object key) {
        acquireLock(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;
      }
    
      private void acquireLock(Object key) {
        Lock lock = getLockForKey(key);     // 获取对应的Lock,没有则新增一把Lock
        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 ReentrantLock getLockForKey(Object key) {
        ReentrantLock lock = new ReentrantLock();
        ReentrantLock previous = locks.putIfAbsent(key, lock);
        return previous == null ? lock : previous;
      }
     
      private void releaseLock(Object key) {
        ReentrantLock lock = locks.get(key);  // 获取Key对应的Lock
        if (lock.isHeldByCurrentThread()) {   // 如果是当前线程持有lock,则释放锁
          lock.unlock();
        }
      }
    
    

    SynchronizedCache

    一个支持同步的MyBatisCache,从名称就能知道实现原理是synchronized关键字

      public SynchronizedCache(Cache delegate) {
        this.delegate = delegate;
      }
    
        @Override
      public synchronized int getSize() {
        return delegate.getSize();
      }
    
      @Override
      public synchronized void putObject(Object key, Object object) {
        delegate.putObject(key, object);
      }
    
      @Override
      public synchronized Object getObject(Object key) {
        return delegate.getObject(key);
      }
    
      @Override
      public synchronized Object removeObject(Object key) {
        return delegate.removeObject(key);
      }
    

    同步缓存就是给核心方法加上了同步锁,保证了线程安全。

    跟随源码看看解析-装饰过程

    cacheElement方法解析cache标签

    可以看出最底层是PerpetualCache,默认装饰的是LruCache。

    如下就是将剩下的装饰器循环装饰的过程了,细节就不追进去了。

    以上就是MyBatis对于缓存的装饰者设计模式的实践相关的源码简单追踪了。

    跟随源码看看缓存的使用的地方

    先随便点击Cache接口的一方法,看看在哪里有使用。很明显,那个BaseExecutor的类就是正儿八经使用的地方。

    query方法中很明显表示了先从缓存中获取,如果没有则走DB(还会写缓存)

    代码也很简单,就是从DB获取然后写入缓存

    总结

    笔者先简单描述了装饰者模式,随后回忆了MyBatis的缓存传统手艺-cache标签的使用,以及一级二级缓存,描述了集成第三方缓存(解决JVM缓存的单点问题)。

    随后结合源码介绍了MyBatis的Cache接口及其相关的实现类,首先通过Demo言简意赅地表达了装饰者模式的使用以及MyBatisCache装饰者模式使用的效果(LoggingCache)

    紧接着笔者介绍了

    • PerpetualCache这个最关键最核心的缓存实现类,它的核心是一个HashMap;
    • FifoCache先进先出淘汰策略的缓存实现类,它的核心是一个维护key的双端队列,添加缓存前先维护这个双端队列,如果size到达阈值则移除队头的元素;
    • LruCache最近最少使用淘汰策略的缓存实现类,它的核心是基于LinkedHashMap实现LRU机制,我们也回忆了LRU以及LinkedHashMap相关的知识点,其关键点就是一个继承了LinkedHashMap的keyMap(KV都是缓存Key),重写了LinkedHashMap的重要方法removeEldestEntry,用于记录最近最少使用的key,在适当时机删除该缓存;
    • SoftCache、WeakCache我们回忆了软引用、弱引用的相关知识,其核心就是对应的Value组件Entry继承了SoftReference、WeakReference;
    • BlockingCache这个阻塞缓存的核心就是大名鼎鼎的ReentrantLock;
    • SynchronizedCache这个缓存顾名思义就是核心方法追加了synchronized的关键字,事实也确实如此。

    为什么要使用缓存?走DB的链路上层用缓存抗一抗再正常不过了。 为什么用装饰者模式?这个场景它的核心就是缓存策略有很多,它们互相可以叠加,可以在配置的时候灵活配置,那么就可以通过解析配置后在运行时灵活的“装饰”起来,达到最后的预期效果,挺妙的。
    关于多种Cache的核心实现,以及相关的周边技术可以反复琢磨,比如锁的使用、缓存的读写、LinkedHashMap、JVM的GC等等,毕竟这是开源框架的实战代码,这些都是值得我们像骆驼一样反复咀嚼,反复反刍的,至少了解了这一块,后续你真的有类似实战的时候之前可以先参考参考了!

    好了,以上就是MyBatis缓存解析-装饰者设计模式了。欢迎多多交流,希望对你有帮助。原创不易..(没想到这么难,本来想总结下,发现一两次还写不完,光扣字都扣傻了 哈哈..)

  • 相关阅读:
    NanUI文档
    NanUI文档
    NanUI文档
    开源组件NanUI一周年
    NanUI文档
    NanUI 0.4.4发布
    【开源】做了一个WinForm窗体的投影组件,能够为窗口添加影子效果
    NanUI for Winform 使用示例【第二集】——做一个所见即所得的Markdown编辑器
    NanUI for Winform 使用示例【第一集】——山寨个代码编辑器
    非常简洁简单的tcp socket库 XKSocket
  • 原文地址:https://www.cnblogs.com/deepSleeping/p/15043100.html
Copyright © 2011-2022 走看看