zoukankan      html  css  js  c++  java
  • myBatis源码解析-缓存篇(2)

    上一章分析了mybatis的源码的日志模块,像我们经常说的mybatis一级缓存,二级缓存,缓存究竟在底层是怎样实现的。此次开始分析缓存模块

    1. 源码位置,mybatis源码包位于org.apache.ibatis.cache下,如图

    2. 先从org.apache.ibatis.cache下的cache接口开始

    // 缓存接口
    public interface Cache {
      // 获取缓存ID
      String getId();
      // 放入缓存
      void putObject(Object key, Object value);
      // 获取缓存
      Object getObject(Object key);
      // 移除某一缓存
      Object removeObject(Object key);
      // 清除缓存
      void clear();
      // 获取缓存大小
      int getSize();
      // 获取锁
      ReadWriteLock getReadWriteLock();
    }

    mybatis提供了自定义的缓存接口,功能通俗易懂,没什么好解释的。有接口,必然有实现,看一下缓存接口的基本实现类PerpetualCache,所在路径为org.apache.ibatis.cache.impl下。

    public class PerpetualCache implements Cache {
    
      // 缓存的ID
      private String id;
      // 使用HashMap充当缓存(老套路,缓存底层实现基本都是map)
      private Map<Object, Object> cache = new HashMap<Object, Object>();
      // 唯一构造方法(即缓存必须有ID)
      public PerpetualCache(String id) {
        this.id = id;
      }
      // 获取缓存的唯一ID
      public String getId() {
        return id;
      }
      // 获取缓存的大小,实际就是hashmap的大小
      public int getSize() {
        return cache.size();
      }
      // 放入缓存,实际就是放入hashmap
      public void putObject(Object key, Object value) {
        cache.put(key, value);
      }
      // 从缓存获取,实际就是从hashmap中获取
      public Object getObject(Object key) {
        return cache.get(key);
      }
      // 从缓存移除
      public Object removeObject(Object key) {
        return cache.remove(key);
      }
      // hashmap清除数据方法
      public void clear() {
        cache.clear();
      }
      // 暂时没有其实现
      public ReadWriteLock getReadWriteLock() {
        return null;
      }
      // 缓存是否相同
      public boolean equals(Object o) {
        if (getId() == null) throw new CacheException("Cache instances require an ID.");
        if (this == o) return true; // 缓存本身,肯定相同
        if (!(o instanceof Cache)) return false; // 没有实现cache类,直接返回false
    
        Cache otherCache = (Cache) o; // 强制转换为cache
        return getId().equals(otherCache.getId()); // 直接比较ID是否相等
      }
      // 获取hashCode
      public int hashCode() {
        if (getId() == null) throw new CacheException("Cache instances require an ID.");
        return getId().hashCode();
      }
    
    }
        

    如上分析,mybatis的基本缓存实现类其实就是内部维护了一个HashMap,通过对HashMap操作来实现基本的功能。但需要注意的是,判断两个缓存是否相等,是比较的缓存ID是否相等。看Cache otherCache = (Cache) o;也就是说缓存接口可能有多种实现,也确实如此。PerpetualCache只提供了缓存的基本实现功能,但一看HashMap就是不安全的类,多线程下肯定会出问题。又比如说我想这个缓存有固定大小,缓存过期策越为先进先出或者LRU功能等。myabtis肯定想到这点,查看org.apache.ibatis.cache.decorators包。看名字就知道用到了装饰者模式。查看包下的类,如SynchronizedCache为缓存保障了线程安全,LruCache定义了缓存的过期策略为淘汰最近最少访问的数据,LoggIngCache提供了日志打印功能。用户想让自己的缓存具备什么功能,就使用这些装饰者类进行装饰。

    3. 分析缓存装饰类SynchronizedCache

    // 在操作前加锁,保证线程安全
      @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);
      }
    
      @Override
      public synchronized void clear() {
        delegate.clear();
      }

    很简单。就是在方法前使用synchronized加锁,保证线程安全。

    4. 分析缓存装饰类LruCache

    介绍LruCache前,先介绍下Lru的实现,Lru是很常用的淘汰策略,意为最近最少使用的对象。查看LruCache,发现内部使用了LinkedHashMap,熟悉LinkedHashMap的伙伴应该知道了。我们一般手写LRU功能就是通过复写LinkedHashMap的方法来实现,LruCache也一样。先大致了解下LinkedHashMap。

    public class LinkedHashMap<K,V>
        extends HashMap<K,V>
        implements Map<K,V>

    LinkedHashMap继承HashMap类,实际上就是对HashMap的一个封装。

    // 内部维护了一个自定义的Entry,集成HashMap中的node类
    static class Entry<K,V> extends HashMap.Node<K,V> {
            // linkedHashmap用来连接节点的字段,根据这两个字段可查找按顺序插入的节点
            Entry<K,V> before, after;
            Entry(int hash, K key, V value, Node<K,V> next) {
                super(hash, key, value, next);
            }
        }

    查看LinkedHashMap构造方法,具体访问顺序见下文分析

        public LinkedHashMap(int initialCapacity,
                             float loadFactor,
                             boolean accessOrder) {
            // 调用HashMap的构造方法
            super(initialCapacity, loadFactor);
            // 访问顺序维护,默认false不开启
            this.accessOrder = accessOrder;
        }    

    引入两张图来理解下HashMap和LinkedHashMap

     以上时HashMap的结构,采用拉链法解决冲突。LinkedHashMap在HashMap基础上增加了一个双向链表来表示节点插入顺序。

     如上,节点上多出的红色和蓝色箭头代表了Entry中的before和after。在put元素时,会自动在尾节点后加上该元素,维持双向链表。了解LinkedHashMap结构后,在看看究竟什么是维护节点的访问顺序。先说结论,当开启accessOrder后,在对元素进行get操作时,会将该元素放在双向链表的队尾节点。源码如下:

     public V get(Object key) {
            Node<K,V> e;
           // 调用HashMap的getNode方法,获取元素
            if ((e = getNode(hash(key), key)) == null)
                return null;
           // 默认为false,如果开启维护链表访问顺序,执行如下方法
            if (accessOrder)
                afterNodeAccess(e);
            return e.value;
        }
    
    
    // 方法实现(将e放入尾节点处)
    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; // 将待调整的e节点赋值给p
               
                p.after = null;
                if (b == null) // 说明e为头节点,将老e的下一节点值为头节点
                    head = a;
                else
                    b.after = a;// 否则,e的上一节点直接指向e的下一节点
                if (a != null)
                    a.before = b; // e的下一节点的上节点为e的上一节点
                else
                    last = b;  
                if (last == null)
                    head = p;  
                else {
                    p.before = last;   // last和p互相连接
                    last.after = p;
                }
                tail = p;   // 将双向链表的尾节点指向p
                ++modCount; // 修改次数加以
            }
        }

    代码很简单,如上面的图,我访问了节点值为3的节点,那木经过get操作后,结构变成如下

    经过如上分析我们知道,如果限制双向链表的长度,每次删除头节点的值,就变为一个lru的淘汰策略了。举个例子,我想限制双向链表的长度为3,依次put 1 2 3,链表为 1 -> 2 -> 3,访问元素2,链表变为 1 -> 3-> 2,然后put 4 ,发现链表长度超过3了,淘汰1,链表变为3 -> 2 ->4;

    那木linkedHashMap是怎样知道自定义的限制策略,看代码,因为LinkedHashMap中没有提供自己的put方法,是直接调用的HashMap的put方法,查看hashMap代码如下:

    // hashMap
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            Node<K,V>[] tab; Node<K,V> p; int n, i;
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
            else {
                Node<K,V> e; K k;
                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p;
                else if (p instanceof TreeNode)
                    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                else {
                    for (int binCount = 0; ; ++binCount) {
                        if ((e = p.next) == null) {
                            p.next = newNode(hash, key, value, null);
                            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                                treeifyBin(tab, hash);
                            break;
                        }
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            break;
                        p = e;
                    }
                }
                if (e != null) { // existing mapping for key
                    V oldValue = e.value;
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    afterNodeAccess(e);
                    return oldValue;
                }
            }
            ++modCount;
            if (++size > threshold)
                resize();
            // 看这个方法
            afterNodeInsertion(evict);
            return null;
        }
    
    // linkedHashMap重写了此方法
    
     void afterNodeInsertion(boolean evict) { // possibly remove eldest
            LinkedHashMap.Entry<K,V> first;
            // removeEldestEntry默认返回fasle
            if (evict && (first = head) != null && removeEldestEntry(first)) {
                K key = first.key;
                // 移除双向链表中的头指针元素
                removeNode(hash(key), key, null, false, true);
            }
        }

    原来只需要重新实现removeEldestEntry就可以自定义实现lru功能了。了解基本的lru原理后,开始分析LruCache。

    public class LruCache implements Cache {
      // 被装饰的缓存类,即真实的缓存类,提供真正的缓存能力
      private final Cache delegate;
      // 内部维护的一个linkedHashMap,用来实现LRU功能
      private Map<Object, Object> keyMap;
      // 待淘汰的缓存元素
      private Object eldestKey;
      // 唯一构造方法
      public LruCache(Cache delegate) {
        this.delegate = delegate; // 被装饰的缓存类
        setSize(1024); // 设置缓存大小
      }
      ....
     }

    经分析,LruCache还是个装饰类。内部除了维护真正的Cache外,还维护了一个LinkedHashMap,用来实现Lru功能,查看其构造方法。

    // 唯一构造方法
      public LruCache(Cache delegate) {
        this.delegate = delegate; // 被装饰的缓存类
        setSize(1024); // 设置缓存大小
      }
      
       // setSize()是构造方法中方法
      public void setSize(final int size) {
        // 初始化keyMap
        keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
          private static final long serialVersionUID = 4267176411845948333L;
          // 什么时候自动删除缓存元素,此处是根据当缓存数量超过指定的数量,在LinkedHashMap内部删除元素
          protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
            boolean tooBig = size() > size;
            if (tooBig) {
              // 将待删除元素赋值给eldestKey,后续会根据此值是否为空在真实缓存中删除
              eldestKey = eldest.getKey();
            }
            return tooBig;
          }
        };
      }

    和上文分析一样,重写了removeEldestEntry方法。此方法返回一个boolean值,当缓存的大小超过自定义大小,返回true,此时linkedHashMap中会自动删除eldest元素。在真实缓存cache中也将此元素删除。保持真实cache和linkedHashMap元素一致。其实就是用linkedHashMap的lru特性来保证cache也具有此lru特性。

    分析put方法和get方法验证此结论

    @Override
      public Object getObject(Object key) {
        keyMap.get(key); // 触发linkedHashMap中get方法,将key对应的元素放入队尾
        return delegate.getObject(key); // 调用真实的缓存get方法
      }
      
      // 放入缓存时,除了在真实缓存中放一份外,还会在LinkedHashMap中放一份
       @Override
      public void putObject(Object key, Object value) {
        delegate.putObject(key, value);
        // 调用LinkedHashMap的方法
        cycleKeyList(key);
      }
      
      private void cycleKeyList(Object key) {
        // linkedHashMap中put,会触发removeEldestEntry方法,如果缓存大小超过指定大小,则将双向链表对头值赋值给eldestKey
        keyMap.put(key, key); 
        // 检查eldestKey是否为空。不为空,则代表此元素是淘汰的元素了,需要在真实缓存中删除。
        if (eldestKey != null) {
          // 真实缓存中删除
          delegate.removeObject(eldestKey);
          eldestKey = null;
        }
      }


    Lru分析结束,除了LruCache外,TransactionCache也是mybatis常用的缓存装饰类。下文进行分析。



  • 相关阅读:
    MongoDB监控一 mongostat
    PHP 中 AJAX 中文乱码解决
    php常用的header头
    PHP判断访客是否移动端浏览器访问
    PHP部分字符串函数汇总
    memcached启动参数
    处理PHP中字符串的常用操作及函数
    Linux Top 命令解析
    XML的特殊字符
    Zookeeper学习(转自知乎)
  • 原文地址:https://www.cnblogs.com/xiaobingblog/p/13393744.html
Copyright © 2011-2022 走看看