• Home
  • Articles
    • 日志
    • 妍小言
    • 舒小书
    • 浩然说
    • 生活日记
  • All Tags

缓存

28 Feb 2017

Reading time ~3 minutes

记录一下我所了解的几种缓存机制

缓存

缓存是一种存储在内存中,可以快速定位查找数据的数据结构,并且一个缓存算法是包含了冷数据淘汰的机制的。

LRU

LRU-Least Recently Used 的缩写,即近期最少使用算法,一般会使用 双向链表 按照访问顺序对缓存数据进行排序,当满足一定条件后可以将链表中最少使用的数据从缓存中剔除出去。
在Java中,从1.4开始提供了java.util.LinkedHashMap类来帮助我们简单的实现LRU缓存算法。

  • 继承java.util.LinkedHashMap
  • 通过构造函数定义java.util.LinkedHashMap#accessOrder为true,即链表排列顺序为访问顺序(默认为false,即插入顺序)
  • 覆盖java.util.LinkedHashMap的removeEldestEntry方法
    • JDK会在向该Map中put数据时会调用该方法
    • 如果返回true,则会将存在时间最长的数据剔除

下面是一个利用java.util.LinkedHashMapLRU的简单实现

/**
 * Created by Justice-love on 2017/2/28.
 */
public class MyLRU extends LinkedHashMap{

    public MyLRU(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor, true);
    }

    /**
     * Returns <tt>true</tt> if this map should remove its eldest entry.
     * This method is invoked by <tt>put</tt> and <tt>putAll</tt> after
     * inserting a new entry into the map.  It provides the implementor
     * with the opportunity to remove the eldest entry each time a new one
     * is added.  This is useful if the map represents a cache: it allows
     * the map to reduce memory consumption by deleting stale entries.
     * <p>
     * <p>Sample use: this override will allow the map to grow up to 100
     * entries and then delete the eldest entry each time a new entry is
     * added, maintaining a steady state of 100 entries.
     * <pre>
     *     private static final int MAX_ENTRIES = 100;
     *
     *     protected boolean removeEldestEntry(Map.Entry eldest) {
     *        return size() &gt; MAX_ENTRIES;
     *     }
     * </pre>
     * <p>
     * <p>This method typically does not modify the map in any way,
     * instead allowing the map to modify itself as directed by its
     * return value.  It <i>is</i> permitted for this method to modify
     * the map directly, but if it does so, it <i>must</i> return
     * <tt>false</tt> (indicating that the map should not attempt any
     * further modification).  The effects of returning <tt>true</tt>
     * after modifying the map from within this method are unspecified.
     * <p>
     * <p>This implementation merely returns <tt>false</tt> (so that this
     * map acts like a normal map - the eldest element is never removed).
     *
     * @param eldest The least recently inserted entry in the map, or if
     *               this is an access-ordered map, the least recently accessed
     *               entry.  This is the entry that will be removed it this
     *               method returns <tt>true</tt>.  If the map was empty prior
     *               to the <tt>put</tt> or <tt>putAll</tt> invocation resulting
     *               in this invocation, this will be the entry that was just
     *               inserted; in other words, if the map contains a single
     *               entry, the eldest entry is also the newest.
     * @return <tt>true</tt> if the eldest entry should be removed
     * from the map; <tt>false</tt> if it should be retained.
     */
    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        if (this.size() > 100) {
            return true;
        } else {
            return false;
        }
    }
}

不足

  • 从Java1.8java.util.LinkedHashMap#afterNodeAccess源码可以看出,当读取一个缓存数据之后,访问顺序链表将会发生重排。
  • 由于缓存的特性,所以他必须是全局变量,当并发量比较大的情况下,缓存作为临界资源在发生重排时势必加锁,使对链表的变更操作由异步转换为同步操作。

软引用

Java中的软引用也经常被用在缓存的实现上。

软引用属于Java中四种引用方式中的一种,不同于我们平常使用的强引用,软引用并非只在对象未被根搜索算法查找到时才可能被回收,而是在内存空间不足时,同样会回收这些对象,这是软引用的特性(除了强引用,软引用,还有弱引用,虚引用,但这两种很少用到)。

  • 依照这种特性,如果使用软引用来存储缓存数据,则缓存数据会被保存至内存将要溢出时。

以下是我使用软引用实现的缓存结构。

/**
 * Created by Justice-love on 2017/2/28.
 */
public class SoftCache<K, V> extends HashMap<K, SoftReference<V>> {

    public V getValue(K key) {
        SoftReference<V> result =  super.get(key);
        return result.get();
    }

    public V putValue(K key, V v) {
        SoftReference<V> va = new SoftReference<V>(v);
        SoftReference<V> re = super.put(key, va);
        return re.get();
    }
}

不足

  • 数据的清理是不可控的,可能会热点数据的全部失效,瞬时增加下游服务的压力。

Oracle缓存算法

Oracle申请的专利缓存算法,数据存储使用HashMap或其他简单的Map即可,缓存的数据可以定义好他的生命周期,如数据使用次数为10次等。
数据是否进入缓存是依赖一个函数f(),函数的返回值是true或者false,当结果为true时,则将数据put进缓存。


该种缓存算法的命中率极高,原因在于热点数据的get次数远远大于冷数据,即虽然f()相同,但热点数据更多的读取次数保证了数据在缓存中保持的时间更长。

以下是我的简单实现(未实现数据的移除)。

/**
 * Created by Justice-love on 2017/2/28.
 */
public class OracleCache<K, V> {

    private Map<K, V> cache = new HashMap<>();

    public V get(K key) {
        if (cache.containsKey(key)) {
            return cache.get(key);
        }
        V v = new Real<V>().create();
        putToCache(key, v);
        return v;
    }

    private void putToCache(K key, V v) {
        if (Math.random() > 0.5) {
            cache.put(key, v);
        }
    }
}

class Real<V> {

    public  V create() {
        return null;
    }
}

注意

  • 数据存储可以使用HashMap,但是因为HashMap是线程不安全的,注意可能并发环境下扩容而导致的死循环问题,具体原因可查看https://my.oschina.net/hosee/blog/673521


JavaCache