[redis]数据淘汰策略以及lru,lfu的优化

1.redis中的内存占满后,当一个新的插入数据到来的时候.redis会怎么做?

 

 

如图

1.如果redis中存有静态数据,且不超时,不希望被删除,那么可以使用第二类

 

重点说下.lru策略和lfu策略

 

lru策略

常见的lru是通过一个 hashmap和链表组成.   map用于存储数据,链表用于存储数据的使用情况

贴一个我的实现

package 链表;

import java.util.HashMap;
import java.util.Map;

/**
 * 146题
 * 运用你所掌握的数据结构,设计和实现一个  LRU (最近最少使用) 缓存机制 。
 * 实现 LRUCache 类:
 * <p>
 * LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
 * int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
 * void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
 *  
 * <p>
 * 输入
 * ["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
 * [[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
 * 输出
 * [null, null, null, 1, null, -1, null, -1, 3, 4]
 *
 *
 * @version 1.0.0, 2020/12/3-2:22 PM.
 * @since 2020/12/3-2:22 PM
 */
class LRUCache {
    private Map<Integer, Node> data = new HashMap();
    private Node head = new Node();
    private Node tail = new Node();
    private int capacity;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        head.setNext(tail);
        tail.setPre(head);
    }

    public int get(int key) {
        Node v = data.get(key);
        if (v == null) {
            return -1;
        } else {
            //注意这里也需要移动node到头
            v.getNext().setPre(v.getPre());
            v.getPre().setNext(v.getNext());
            //2.把当前节点移动到头节点
            v.setNext(head.getNext());
            v.setPre(head);
            v.getNext().setPre(v);
            head.setNext(v);
            return v.getValue();
        }
    }

    public static void main(String[] args) {
        LRUCache lruCache = new LRUCache(1);
        lruCache.put(2, 1);
        System.out.println(lruCache.get(2));
        lruCache.put(3, 2);
        System.out.println(lruCache.get(2));


    }

    public void put(int key, int value) {
        Node v = data.get(key);
        //说明是新增的1.判断长度是否超长,如果超长,那么淘汰尾部数据
        if (v == null) {
            //数据满了 删除最后一个节点
            if (data.size() == capacity) {
                //-1
                Node last = tail.getPre();
                //-2
                Node lastPre = last.getPre();
                lastPre.setNext(tail);
                tail.setPre(lastPre);
                data.remove(last.getKey());
            }
            //不管满不满都要进行的操作1.插入数据 2.将数据移动到head
            Node currentNode = new Node(key, value);
            data.put(key, currentNode);
            //当前的上一个设置为头节点
            currentNode.setPre(head);
            //设置当前的下一个节点为oldFirst,oldFirst的下一个节点是当前节点
            Node first = head.getNext();
            currentNode.setNext(first);
            first.setPre(currentNode);
            head.setNext(currentNode);
        } else {
            //说明是已有数据,不会涉及到数据淘汰,只涉及到节点移动.
            //1.把当前节点前后节点处理了
            v.getNext().setPre(v.getPre());
            v.getPre().setNext(v.getNext());
            //2.把当前节点移动到头节点
            v.setNext(head.getNext());
            v.setPre(head);
            v.getNext().setPre(v);
            head.setNext(v);
            v.setValue(value);
        }
    }
}

class Node {
    private Node next;
    private Node pre;
    private int value;
    private int key;

    public Node(int key, int value) {
        this.value = value;
        this.key = key;
    }

    public Node() {
        super();
    }

    public int getKey() {
        return key;
    }

    public void setKey(int key) {
        this.key = key;
    }

    public Node getPre() {
        return pre;
    }

    public void setPre(Node pre) {
        this.pre = pre;
    }

    public Node getNext() {
        return next;
    }

    public void setNext(Node next) {
        this.next = next;
    }

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}


/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache obj = new LRUCache(capacity);
 * int param_1 = obj.get(key);
 * obj.put(key,value);
 */

可以看出,如果想实现lru,需要有一个链表,head代表最近访问的,tail代表很久之前访问的.

如果redis中每个key都存放在这个链表中,那么内存需要很大!!!

因此redis实现了一个简化版的lru.主要目的是减少 内存占用!.

1.随机选取n个key,组成一个资源池.根据每个key上存储的时间戳(最近访问).可以知道谁是最近访问的key(时间戳最大的),可以进行排序.淘汰的时候,选择时间戳最小的.

2.第二次,会再选取m个key放到资源池,但是存放时,有一些规定.就是时间戳要比池子中最小的还要小.是为了不用每次都去排序吧?.当内存不足时,就可以淘汰最小的key了.

此种方式,不用使用链表,局部化的lru

可以看下redis中的官网

 

The reason why Redis does not use a true LRU implementation is because it costs more memory. However the approximation is virtually equivalent for the application using Redis. The following is a graphical comparison of how the LRU approximation used by Redis compares with true LRU.

LRU comparison

lfu策略

 

lfu在lru的基础上,删除数据的时候,不是直接看谁最近没有被使用,而是先看谁被调用的少.因此需要增加一个计数器.当需要删除数据的时候, 1.判断计数 2.计数相同时,判断谁调用时间戳靠前

如图.RedisObject有一个24位的字段,前16位存放最近访问的时间戳,后8位存放访问次数. 

问题1:

1.8位,那么最大值就是2^9-1=255.如果每次访问都加1,那岂不是很快就满了,那请求256次和请求256000次没有区别了.

解决:在实现 LFU 策略时,Redis 并没有采用数据每被访问一次,就给对应的 counter 值加 1 的计数规则,而是采用了一个更优化的计数规则。

下面这段 Redis 的部分源码,显示了 LFU 策略增加计数器值的计算逻辑。其中,baseval 是计数器当前的值。计数器的初始值默认是 5(由代码中的 LFU_INIT_VAL 常量设置),而不是 0,这样可以避免数据刚被写入缓存,就因为访问次数少而被立即淘汰。


double r = (double)rand()/RAND_MAX;
...
double p = 1.0/(baseval*server.lfu_log_factor+1);
if (r < p) counter++;   

 

使用了这种计算规则后,我们可以通过设置不同的 lfu_log_factor 配置项,来控制计数器值增加的速度,避免 counter 值很快就到 255 了。

可以看到 参数为100后,效果很明显

 

 

2.如果一个数据第一分钟,频繁的访问.  后面1小时都不访问了,咋整

Redis 在实现 LFU 策略时,还设计了一个 counter 值的衰减机制。

lfu_decay_time 参数,衰减次数= (当前时间-最近访问时间)/lfu_decay_time

 

参考文章:https://time.geekbang.org/column/article/297270

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值