缓存

缓存

一、金字塔的存贮体系

如下图所示,从上到下依次为寄存机,一级缓存,二级缓存,主存,外存(本地磁盘),远程存贮(分布式系统,Web服务器)。
这里写图片描述
缓存的作用,由于在每一级的读取速度都比下一级的读取速度快很多,如果CPU运算时,每次都去速度较慢的主存或者外存中去获得数据,必然会等待和浪费较长时间。因此就会诞生了缓存,提前将一部分的内容从主存取到缓存中,从外存取到内存中,从远地的资源中取到本地,从而缩短时间,提高效率。

具体请参见CASPP

二、缓存的处理机制

当我们有了缓存之后,Cpu的读取数据过程如下:
1. CPU就会先去缓存中取数据,如果该数据存在,就命中;
2. 如果缓存中没有,将去内存中查找,如果内存中存在,就将包含该数据返回给CPU,同时将所在的一页(一般会采用分页式的存贮结构)的数据写进缓存;
3. 如果内存中没有,则去外存中查找,如果外存中存在,将数据返回的同时,并将主存中该块的内容写进内存。

ps: 1. 为什么会将该页或者块的内容缓存?这里利用了局部性原理( Locality)。
2. 从这个过程中,我们可以看到多级缓存的里面,一级二级缓存保留的数据其实是内存中数据的一部分,内存又是做为外存的缓存,同样外存也可以作为更大的数据源的缓存。

三、缓存替换策略

当缓存区满了,并且没有命中数据时,这个时候就需要按照一定策略(或者说算法)把旧的对象提出,把新的对象加入缓存中。常见的缓存的算法有以下几种:
LRU (Least Recently Used)
LFU (Least Frequently Used)
FIFO
Random Cache(随机替换算法)
因为在实际中的业务场景不一样,就需要根据具体的业务来选择最合适的缓存替换策略。

四、Java实现LRU和LFU

LRU: 最近最少被使用的内容被替换,
首先因为希望能够较快速度查到缓存内容这里可以采用hash表的方式;
其次需要查询出最近最晚被使用的项,给最近使用的项做上标记;
最后删除相应的项。

LFU:代码如下
Leetcode题目参见:https://leetcode.com/problems/lfu-cache/#/description
主要思路:
使用HashMap来做缓存,保证整个操作在O(1)的时间复杂度内,其次,因为要根据每个节点的使用频率来选择最少被使用的节点删除,所以需要保存使用频率,在相同频率时使用FIFO,故而在每个频度下选择了LinkedHashSet结构。
注意点:
1. 在增加同一个key的时候,其使用频率增加,这点题目说的不是太清楚,
2. 注意边界

public class LFUCache {
    private int capacity;
    private Map<Integer, Integer> keyAndValueMap;
    private Map<Integer, Integer> keyAndFrequenceMap;
    private Map<Integer, LinkedHashSet<Integer>> frequenceAndKeyMap;
    private Integer smallestFrequenceValue = 1;

    public LFUCache(int capacity) {
        if (capacity <= 0) {
            capacity = 0;
        }
        this.capacity = capacity;
        keyAndValueMap = new HashMap<>(capacity);
        keyAndFrequenceMap = new HashMap<>(capacity);
        frequenceAndKeyMap = new LinkedHashMap<>();
    }

    public int get(int key) {
        if (capacity <=0 || keyAndValueMap.get(key) == null ) {
            return -1;
        }
        updateFrquence(key);
//        System.out.println("keyAndValueMap : " + keyAndValueMap);
//        System.out.println("keyAndFrequenceMap : " + keyAndFrequenceMap);
//        System.out.println( "frequenceAndKeyMap" + frequenceAndKeyMap);
//        System.out.println( "----------------------");
        return keyAndValueMap.get(key);
    }

    public void put(int key, int value) {
        if (capacity <= 0) {
            return;
        }
        if (keyAndValueMap.size() == capacity && !keyAndValueMap.keySet().contains(key)) {
            removeOldestValue();
        }
        if (keyAndValueMap.keySet().contains(key)) {
            updateFrquence(key);
            keyAndValueMap.put(key, value);
        } else {
            keyAndValueMap.put(key, value);
            keyAndFrequenceMap.put(key, 0);
            smallestFrequenceValue = 0;
            LinkedHashSet<Integer> keySet = frequenceAndKeyMap.get(0);
            if (keySet == null) {
                keySet = new LinkedHashSet<>();
            }
            keySet.add(key);
            frequenceAndKeyMap.put(0, keySet);
//            System.out.println("keyAndValueMap : " + keyAndValueMap);
//            System.out.println("keyAndFrequenceMap : " + keyAndFrequenceMap);
//            System.out.println("frequenceAndKeyMap" + frequenceAndKeyMap);
//            System.out.println("----------------------");
        }
    }

    private void updateFrquence(int key) {
        Integer currenceFrequence = keyAndFrequenceMap.get(key);
        if (currenceFrequence == null) {
            return;
        }
        if (frequenceAndKeyMap.get(currenceFrequence) == null) {
            LinkedHashSet keySet = new LinkedHashSet();
            keySet.add(key);
            frequenceAndKeyMap.put(++currenceFrequence, keySet);
        } else {
            LinkedHashSet keySet = frequenceAndKeyMap.get(currenceFrequence);
            keySet.remove(key);
            if (currenceFrequence == smallestFrequenceValue.intValue() && keySet.size() == 0) {
                smallestFrequenceValue++;
            }
            keySet = frequenceAndKeyMap.get(++currenceFrequence);
            if (keySet == null) {
                keySet = new LinkedHashSet();
            }
            keySet.add(key);
            frequenceAndKeyMap.put(currenceFrequence, keySet);
        }
        keyAndFrequenceMap.put(key, currenceFrequence);
    }

    private void removeOldestValue() {
        LinkedHashSet keySet = frequenceAndKeyMap.get(smallestFrequenceValue);
        Iterator<Integer> it  = keySet.iterator();
        int oldestKey = it.next();
        it.remove();
        if (keySet.size() == 0) {
            for (Map.Entry<Integer, LinkedHashSet<Integer>> entry : frequenceAndKeyMap.entrySet()) {
                if(entry.getValue() != null && entry.getValue().size() > 0) {
                    smallestFrequenceValue = entry.getKey();
                    break;
                }
            }
        }
        keyAndValueMap.remove(oldestKey);
        keyAndFrequenceMap.remove(oldestKey);
    }
}

测试test

public class LFUCacheTest {
    @Test
            /*
            * [[2],[1,1],[2,2],[1],[3,3],[2],[3],[4,4],[1],[3],[4]]
            */
    public void test() throws Exception {
        LFUCache cache = new LFUCache(10);

        String value = "10,13],[3,17],[6,11],[10,5],[9,10],[13],[2,19],[2],[3],[5,25],[8],[9,22],[5,5],[1,30],[11],[9,12],[7],[5],[8]," +
                "[9],[4,30],[9,3],[9],[10],[10],[6,14],[3,1],[3],[10,11],[8]," +
                "[2,14],[1],[5],[4],[11,4],[12,24],[5,18],[13],[7,23],[8],[12]," +
                "[3,27],[2,12],[5],[2,9],[13,4],[8,18],[1,7],[6],[9,29],[8,21]," +
                "[5],[6,30],[1,12],[10],[4,15],[7,22],[11,26],[8,17],[9,29],[5]," +
                "[3,4],[11,30],[12],[4,29],[3],[9],[6],[3,4],[1],[10],[3,29],[10,28]," +
                "[1,20],[11,13],[3],[3,12],[3,8],[10,9],[3,26],[8],[7],[5],[13,17],[2,27]," +
                "[11,15],[12],[9,19],[2,15],[3,16],[1],[12,17],[9,1],[6,19],[4],[5],[5],[8,1]," +
                "[11,7],[5,2],[9,28],[1],[2,2],[7,4],[4,22],[7,24],[9,26],[13,28],[11,26" ;

        String[] temp = value.split("],\\u005B");
        for (int i =0 ;i< temp.length;i++) {
            if(temp[i].split(",").length > 1) {
                int key = Integer.parseInt(temp[i].split(",")[0]);
                int val = Integer.parseInt(temp[i].split(",")[1]);
                cache.put(key, val);
            } else {
                System.out.println(cache.get(Integer.parseInt(temp[i])));
//                System.out.println("");
            }
        }
    }
}

五、Redis、Memcached、Guava、Ehcache中的算法

缓存那些事,一是内存爆了要用LRU(最近最少使用)、LFU(最少访问次数)、FIFO的算法清理一些;二是设置了超时时间的键过期便要删除,用主动或惰性的方法。

  1. LRU 简单粗暴的Redis
    今天看 Redis3.0的发行通告里说,LRU算法大幅提升了,就翻开源码来八卦一下,结果哭笑不得,这旧版的”近似LRU”算法,实在太简单,太偷懒,太Redis了。
    在 Github的Redis项目里搜索lru,找到代码在redis.c的freeMemoryIfNeeded()函数里。
    先看 2.6版的代码: 竟然就是随机找三条记录出来,比较哪条空闲时间最长就删哪条,然后再随机三条出来,一直删到内存足够放下新记录为止…….可怜我看 配置文档后的想象,一直以为它会帮我在整个Redis里找空闲时间最长的,哪想到我有一百万条记录的时候,它随便找三条就开始删了。
    好,收拾心情再看 3.0版的改进:现在每次随机五条记录出来,插入到一个长度为十六的按空闲时间排序的队列里,然后把排头的那条删掉,然后再找五条出来,继续尝试插入队列………嗯,好了一点点吧,起码每次随机多了两条,起码不只在一次随机的五条里面找最久那条,会连同之前的一起做比较……

中规中矩的Memcached
相比之下,Memcached实现的是再标准不过的LRU算法,专门使用了一个教科书式的双向链表来存储slab内的LRU关系,代码在 item.c里,详见 memcached源码分析—–LRU队列与item结构体,元素插入时把自己放到列头,删除时把自己的前后两个元素对接起来,更新时先做删除再做插入。
分配内存超限时,很自然就会从LRU的队尾开始清理。

同样中规中矩的Guava Cache
Guava Cache同样做了一个双向的Queue,见 LocalCache中的AccessQueue类,也会在超限时从Queue的队尾清理,见 evictEntries()函数。

和Redis旧版一样的Ehcache/Hazelcast
文档,居然和Redis2.6一样,直接随机8条记录,找出最旧那条,刷到磁盘里,再看代码, Eviction类和 OnHeapStore的evict()函数。
再看Hazelcast,几乎一样,随机取25条。 这种算法,切换到LFU也非常简单。

小结
不过后来再想想,也许Redis本来就不是主打做Cache的,这种内存爆了需要通过LRU删掉一些元素不是它的主要功能,默认设置都是noeviction——内存不够直接报错的,所以就懒得建个双向链表,而且每次访问时都要更新它了,看Google Group里长长的讨论,新版算法也是社区智慧的结晶。何况,Ehcache和Hazelcast也是和它的旧版一样的算法,Redis的新版还比这两者强了。
后来,根据@刘少壮同学的提示,JBoss的InfiniSpan里还实现了比LRU更高级的 LIRS算法,可以避免一些冷数据因为某个原因被大量访问后,把热数据挤占掉。

  1. 过期键删除
    如果能为每一个设置了过期的元素启动一个Timer,一到时间就触发把它删掉,那无疑是能最快删除过期键最省空间的,在Java里用一条 DeplayQueue存着,开条线程不断的读取就能做到。但因为该线程消耗CPU较多,在内存不紧张时有点浪费,似乎大家都不用这个方法。
    所以有了惰性检查,就是每次元素被访问时,才去检查它是否已经超时了,这个各家都一样。但如果那个元素后来都没再被访问呢,会永远占着位子吗?所以各家都再提供了一个定期主动删除的方式。

Redis
代码在 redis.c的activeExpireCycle()里,看过文档的人都知道,它会在主线程里,每100毫秒执行一次,每次随机抽20条Key检查,如果有1/4的键过期了,证明此时过期的键可能比较多,就不等100毫秒,立刻开始下一轮的检查。不过为免把CPU时间都占了,又限定每轮的总执行时间不超过1毫秒。

Memcached
Memcached里有个文不对题的 LRU爬虫线程,利用了之前那条LRU的队列,可以设置多久跑一次(默认也是100毫秒),沿着列尾一直检查过去,每次检查LRU队列中的N条数据。虽然每条Key设置的过期时间可能不一样,但怎么说命中率也比Redis的随机选择N条数据好一点,但它没有Redis那种过期的多了立马展开下一轮检查的功能,所以每秒最多只能检查10N条数据,需要自己自己权衡N的设置。

Guava Cache
在Guava Cache里,同一个Cache里所有元素的过期时间是一样的,所以它比Memached更方便,顺着之前那条LRU的Queue检查超时,不限定个数,直到不超时为止。而且它这个检查的调用时机并不是100毫秒什么的,而是每次各种写入数据时的 preWriteCleanup()方法中都会调用。
吐槽一句,Guava的Localcache类里面已经4872行了,一点都不轻量了。

Ehcache
Ehcache更乱,首先它的内存存储中只有惰性检查,没有主动检查过期的,只会在内存超限时不断用近似LRU算法(见上)把内存中的元素刷到磁盘中,在文件存储中才有超时检查的线程, FAQ里专门解释了原因。
然后磁盘存储那有一条8小时左右跑一次的线程,每次遍历所有元素…..见 DiskStorageFactory里的DiskExpiryTask。 一圈看下来,Ehcache的实现最弱。

该部分(第五节)转载自:https://my.oschina.net/ffy/blog/501003


  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值