深入探讨LRU:从原理到工程实践

1、LRU

1.1、什么是LRU?

LRU(Least Recently Used,最近最少使用)算法是一种常用于缓存淘汰策略的算法。其基本思想是根据数据的访问历史,当缓存空间达到上限时,淘汰最近最少被使用的数据,以腾出空间来存储新的数据。

1.2、手写LRU

核心思想:

  • 查询时:如果命中缓存,数据移到最前面
  • 添加时:
    • 如果缓存中有,修改缓存中的数据,并将数据移到最前面;
    • 如果缓存中没有,创建节点添加到缓存中,当缓存数量达到最大值时,将最近最少访问的数据剔除

代码实现如下:

  • 利用哈希表快速定位
  • 利用双向链表存储数据
    在这里插入图片描述
class LRUCache {
    int capacity ;  //缓存容量
    int size ;  //缓存当前存储元素个数
    Map<Integer , ListNode > cache ; // 缓存
    ListNode head ,tail ; 

    public LRUCache(int capacity) {
        cache = new HashMap<>(capacity);
        this.capacity =capacity ;
        this.size = 0 ; 
        head =new ListNode();
        tail = new ListNode();
        head.next =tail ;
        tail.pre = head ;
    }
    
    public int get(int key) {
        //先看缓存中有没有,如果没有返回-1
        if(!cache.containsKey(key)){
            return -1 ; 
        }
        //有则将当前节点移动到头部,即head后第一个
        ListNode node = cache.get(key);
        moveToFirst(node);
        return node.value;
    }
    
    public void put(int key, int value) {
        //先看缓存中有没有,如果没有
        if(!cache.containsKey(key)){
            //创建,同时添加到cache和双向链表中
            ListNode node=  new ListNode(key, value);
            cache.put(key , node);
            addToFirst(node);
            size++;
            //检查容量,如果超过则淘汰tail前一个
            if(size > capacity) {
                //注意:删除时同时删除cache和双向链表中节点
                cache.remove(tail.pre.key);
                removeLast();
                size -- ; 
            }
        }else{
            //拿出来更新节点,并移动到第一个
            ListNode node =  cache.get(key);
            node.value = value ; 
            moveToFirst(node);
        }
    }

    public void moveToFirst(ListNode node){
        //断开连接
        node.pre.next = node.next ; 
        node.next.pre = node.pre; 
        //在将节点添加到头部
        addToFirst(node);
    }
    public void addToFirst(ListNode node){
        node.next =head.next ;
        node.pre = head ;
        node.next.pre = node;
        head.next = node ; 
    }
    public void removeLast(){
        ListNode node = tail.pre;
        node.pre.next = node.next ;
        node.next.pre = node.pre; 
    }
}
class ListNode{
    int key ;
    int value ; 
    ListNode pre , next ; 
    public ListNode(){
    }
    public ListNode(int key , int value){
        this.key = key;
        this.value = value; 
    }
}

2、工程实践

2.1、LRU在MySQL中的应用

MySQL的数据存储在磁盘上,操作时需要加载到内存中。在内存中存储数据页的结构是Buffer Pool。

假设我们根据主键去修改某条数据的字段,根据索引查到对应记录行,在这期间所有访问的数据页都会加载到内存中,我们去修改的实际上是内存中的值,我们被修改的缓存页称为脏页,这些脏页是需要刷新到磁盘上的,称为刷脏。

Buffer Pool主要由以下部分组成:

  • 缓存页
  • 控制块
  • 空闲链表
  • 刷新链表
  • LRU链表

Buffer Pool相当于一个缓存,缓存总有满的时候,其中LRU链表负责在BufferPool满时,将不常用的页给淘汰掉

  • Innodb将LRU链表分成了5/8的热数据区域和3/8的冷数据区域,并且前1/4的页被访问不移动
  • 新加入的页会放到冷数据头部,如果一秒后被访问,就会放到热数据头部,这样设计是为了在全表扫描时,频繁修改LRU
  • 每次从LRU链表的尾部选择一个数据页。如果此数据页是脏页,将其刷新并将其放入空闲链表中。

在这里插入图片描述
MySQL的BufferPool基于冷热数据隔离改进了其LRU算法,并且进入冷数据区一秒后被访问,才会添加到热数据区头部,避免全表扫描时大换血。

2.2、LRU在Redis中的应用

1、思路总结

Redis中则采用随机采样这种近似LRU算法,其思路为:

  • 维护一个大小为16(默认)的待淘汰数据集合,其中数据按照最近被访问的时间进行排序
  • 每次随机选择5(默认)个key 加入待淘汰数据集合中
  • 当待淘汰集合满之后,访问的时间间隔最大的key就会被取出来淘汰掉
2、什么是待淘汰数据集合

为了淘汰数据,Redis 定义了一个数组 EvictionPoolLRU,用来保存待淘汰的候选键值对。这个数组的元素类型是 evictionPoolEntry 结构体,该结构体保存了待淘汰键值对的空闲时间 idle、对应的 key 等信息。
在这里插入图片描述

以下代码展示了 EvictionPoolLRU 数组和 evictionPoolEntry 结构体:

//该数组的大小默认是 16 个元素,也就是可以保存 16 个待淘汰的候选键值对
static struct evictionPoolEntry *EvictionPoolLRU; 

struct evictionPoolEntry {
    unsigned long long idle;    //待淘汰的键值对最后一次被访问时间
    sds key;                    //待淘汰的键值对的key
    sds cached;                 //缓存的SDS对象
    int dbid;                   //待淘汰键值对的key所在的数据库ID
};

3、如何知道数据的最近一次访问时间

Redis是基于内存的k-v结构, 采用RedisObject结构来存储value,在RedisObject有一个lru属性,采用24个bit位记录该对象最后一次被访问的时间,如下:

typedef struct redisObject {
    unsigned type:4;  //对象类型,String、List、Set、ZSet、Hash
    unsigned encoding:4; // 底层编码
    unsigned lru:LRU_BITS;  //采用24个bit位记录该对象最后一次被访问的时间
    int refcount; // 对象引用计数器,为0可被回收
    void *ptr; // 指向实际数据地址
} robj;

4、从哪里选择key,为什么是5个

根据在Redis中设置的淘汰算法,如:

  • volatile-lru 从设置过期时间的key中随机选择
  • allkeys-lru 从所有key中随机选择

函数采样的 key 的数量,是由 redis.conf 中的配置项 maxmemory-samples 决定的,该配置项的默认值是 5。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值