淘汰算法的介绍与代码实现

339 阅读4分钟

这是我参与8月更文挑战的第27天,活动详情查看: 8月更文挑战

Redis是我们比较常用的缓存系统,因为缓存是存放在内存中,内存的容量有限,不能像磁盘一样可以装几个T的容量,当我们不停的往缓存中存放数据,那么到一定的时候就会将缓存称爆,这个时候就需要淘汰算法来计算出哪些缓存数据需要删除,然后将数据从内存中清理,腾出空间给其他数据使用。比较常用的淘汰算法有先来先淘汰(FIFO)、最久未使用淘汰(LRU)、最久最少使用(LFU)这三大类算法。

先来先淘汰(FIFO)

  • 概述

这种算法就是字面的意思:先来的先被淘汰(First In First Out),往队列插入数据的时候,发现队列已满,就把最早插入的数据淘汰掉,然后插入新的数据

  • 算法实现介绍

在这里我们可以采用LinkedList来实现,每次往头部添加数据,然后判断如果容量大于已有的容量,就将队尾的数据取出淘汰掉,大致实现原理如下面的图: 算法.png 这种算法实现起来比较简单,内部维护一个有序的链表就行,但是有它的一定缺点:就是这个只管数据在最后一个都会被淘汰掉,如果被淘汰的数据一直频繁使用,每次被淘汰掉后,都要去其他地方把数据查询回来然后扔到缓存中,这样会造成额外的开销。

  • 简易代码
public class FIFO {
    LinkedList<Integer> fifo = new LinkedList<Integer>();
    int MAX_SIZE = 5;
    public void add(int item) {
        fifo.addFirst(item);
        if (fifo.size() > MAX_SIZE) {
            fifo.removeLast();
        }
    }

    public void read(int item) {
        Iterator<Integer> iterator = fifo.iterator();
        while (iterator.hasNext()) {
            int j = iterator.next();
            if (item == j) {
                System.out.println("find it!");
                return;
            }
        }
        System.out.println("not found!");
    }
}

最久未使用淘汰(LRU)

  • 概述

字面意思就是淘汰最长时间没有使用过的数据(Least Recently Used),相比先来先淘汰的算法没有那么粗暴,LRU算法认为过去频繁使用的数据,在未来的时候可能会被使用掉,如果被淘汰的话,会增加额外的开销,所以就优先淘汰那些很久为被使用的元素,Redis的淘汰机制就有LRU算法。

  • 算法实现介绍

实现的数据结构有很多种,这里仍然采用LinkedList来实现,数据的加入也是从头部增加,淘汰的时候从尾部取出来淘汰,但是相对于FIFO算法而已,在使用的时候做一点改进,每次使用到元素后,将命中的缓存数据从原来的位置摘出来,然后将元素重新插入到头部中,如何一个元素被频繁使用,那么它就会不停的被取来插入到头部中,这样的话就不会被淘汰。 算法 (1).png

  • 简易代码
public class LRU {
    LinkedList<Integer> lru = new LinkedList<Integer>();
    int size = 3;

    public void add(int i) {
        lru.addFirst(i);
        if (lru.size() > size) {
            lru.removeLast();
        }
    }

    public void read(int i) {
        Iterator<Integer> iterator = lru.iterator();
        int index = 0;
        while (iterator.hasNext()) {
            int j = iterator.next();
            if (i == j) {
                System.out.println("find it!");
                lru.remove(index);
                lru.addFirst(j);
                return;
            }
            index++;
        }
        System.out.println("not found!");
    }
}

最久最少使用(LFU)

  • 概述

最近最少使用的元素被淘汰(Least Freuently Used),淘汰最近一段时间内,使用次数最少的使用,相比LRU算法,在淘汰的时候增加了一层判断,不只是根据最久未使用的来淘汰,增加了最少使用这个维度指标来一同判断该元素是否能被淘汰。

  • 算法实现介绍

这种算法实现的时候,需要在每个元素上面内置一个计数器和访问时间,这样的话当出现同一段时间内,访问次数相同的情况,就可以根据访问时间来判断哪个元素被淘汰。在实现次数相同,根据访问时间比较的时候,我们可以自定义一个实体对象,继承Comparable接口,然后重写compareTo方法,这样话,在对集合排序的时候,可以根据我们重写的compareTo方法来比较顺序,可以获取到最小的一个元素,然后淘汰掉。

  • 简易代码
public class LFU {
    private final int size = 5;

    private Map<Integer, Integer> cache = new HashMap<>();
    private Map<Integer, Dto> count = new HashMap<>();

    public void put(Integer key, Integer value) {
        Integer v = cache.get(key);
        if (v == null) {
            if (cache.size() == size) {
                removeElement();
            }
            count.put(key, new Dto(key, 1, System.currentTimeMillis()));
        } else {
            addCount(key);
        }
        cache.put(key, value);
    }


    public Integer get(Integer key) {
        Integer value = cache.get(key);
        if (value != null) {
            addCount(key);
            return value;
        }
        return null;
    }

    private void removeElement() {
        Dto dto = Collections.min(count.values());
        cache.remove(dto.getKey());
        count.remove(dto.getKey());
    }

    private void addCount(Integer key) {
        Dto Dto = count.get(key);
        Dto.setCount(Dto.getCount() + 1);
        Dto.setLastTime(System.currentTimeMillis());
    }

    class Dto implements Comparable<Dto> {

        private Integer key;
        private int count;
        private long lastTime;

        @Override
        public int compareTo(Dto o) {
            int compare = Integer.compare(this.count, o.count);
            return compare == 0 ? Long.compare(this.lastTime, o.lastTime) : compare;
        }
    }
}

上面三种淘汰机制在实际中用的比较多,主要淘汰算法用于缓存服务中,用来淘汰我们存入的缓存数据。