百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

你从未了解过的Redis内存淘汰机制

mhr18 2024-10-24 11:13 22 浏览 0 评论

Redis是基于内存存储,常用于数据的缓存,所以Redis提供了对键的过期时间的设置,实现了几种淘汰机制便于适应各种场景。


设置过期时间
我们可以在设置键时设置expire time,也可以在运行时给存在的键设置剩余的生存时间,不设置则默认为-1,设置为-1时表示永久存储。

2|0Redis清除过期Key的方式


定期删除+惰性删除

2|1定期删除


Redis设定每隔100ms随机抽取设置了过期时间的key,并对其进行检查,如果已经过期则删除。

为什么是随机抽取? 因为如果存储了大量数据,全部遍历一遍是非常影响性能的!

2|2惰性删除


每次获取key时会对key进行判断是否还存活,如果已经过期了则删除。


注意:Redis中过期的key并不会马上删除,因为定期删除可能正好没抽取到它,我们也没有访问它触发惰性删除

3|0Redis内存淘汰机制


思考一下,如果定期删除漏掉了很多过期的key,而我们也没有再去访问它,如果不加处理,很可能导致内存耗尽。


Redis配置文件中可以设置maxmemory,内存的最大使用量,到达限度时会执行内存淘汰机制。

3|1配置

  • redis.conf 配置文件中配置最大可用内存// 设置Redis 最大可用内存为 1024mb maxmemory 1024mb
  • 命令操作//获取设置的Redis能使用的最大内存大小 127.0.0.1:6379> config get maxmemory //设置Redis最大占用内存大小为1024M 127.0.0.1:6379> config set maxmemory 1024mb

3|2Redis中的内存淘汰机制

没有配置时,默认为noeviction 不驱逐(删除)数据

  • volatile为前缀的策略都是从 已过期的数据集 中进行淘汰。
  • allkeys为前缀的策略都是面向 所以key 进行淘汰。
  • LRU(least recently used)最近最少使用的。
  • LFU(Least Frequently Used)最不常用的。
  • 它们的触发条件都是Redis使用的内存达到阈值时。

内存淘汰机制设置获取修改

  • redis.conf 配置文件中配置最大可用内存// 设置Redis 淘汰机制为 volatile-lfu maxmemory-policy volatile-lfu
  • 命令操作//获取设置的Redis内存淘汰机制 127.0.0.1:6379> config get maxmemory-policy //设置Redis内存淘汰机制 127.0.0.1:6379> config set maxmemory-policy volatile-lfu

3|3LRU 算法


概念

LRU(Least Recently Used),最近最少使用,是一种缓存置换算法,其核心思想是:如果一个数据在最近一段时间内没有被用到,那么将来被使用的可能性也很小,所以就可以被淘汰掉。

实现原理

实现 LRU 算法除了需要 key/value 字典外,还需要附加一个链表,链表中的元素按照一定的顺序进行排列。当空间满的时候,会踢掉链表尾部的元素,当字典某个元素被访问时,它在链表中的位置会被移动到表头,所以链表的元素排列顺序就是元素最近被访问的时间顺序。

位于链表尾部的元素就是不被重用的元素,所以会被踢掉。位于表头的元素是刚被使用过的,因此暂时不会被踢。

下面使用 PHP 来实现一个简单的 LRU 算法。

<?php
class LRUCache
{
    private $cache = [];
    private $maxSize = 0;

    function __construct($size)
    {
        // 缓存最大存储数量
        $this->maxSize = $size;
    }

    public function set($key, $value)
    {
        // 如果存在,就先删除,然后在开头插入
        if (isset($this->cache[$key])) {
            unset($this->cache[$key]);
        }
        // 长度检查,超长则删除尾部元素
        if (count($this->cache) >= $this->maxSize) {
            array_pop($this->cache);
        }
        // 头部插入元素
        $this->cache = [$key=>$value] + $this->cache;
    }

    public function get($key)
    {
        $resultValue = null;
        if (isset($this->cache[$key])) {
            $resultValue = $this->cache[$key];
            // 移动到头部
            unset($this->cache[$key]);
            $this->cache = [$key=>$resultValue] + $this->cache;
        }
        
        return $resultValue;
    }

    public function getAll()
    {
        return $this->cache;
    }
}

$cache = new LRUCache(3);
$cache->set('a', 1);
$cache->set('b', 2);
$cache->set('c', 3);
var_dump($cache->getAll());

$cache->set('d', 4);
var_dump($cache->getAll());

LRU 在 redis 中的实现

Redis 使用了一种近似 LRU算法,之所以不使用 LRU 算法,是因为其需要消耗大量的额外内存。

redis 为了实现近似 LRU 算法,给每个 key 增加了一个 24 bit的字段,用于保存最后一次被访问的时间。

Redis维护了一个24位时钟,可以简单理解为当前系统的时间戳,每隔一定时间会更新这个时钟。每个key对象内部同样维护了一个24位的时钟,当新增key对象的时候会把系统的时钟赋值到这个内部对象时钟。比如我现在要进行LRU,那么首先拿到当前的全局时钟,然后再找到内部时钟与全局时钟距离时间最久的(差最大)进行淘汰,这里值得注意的是全局时钟只有24位,按秒为单位来表示才能存储194天,所以可能会出现key的时钟大于全局时钟的情况,如果这种情况出现那么就两个相加而不是相减来求最久的key。

struct redisServer { pid_t pid; char *configfile; //全局时钟 unsigned lruclock:LRU_BITS; ... };

typedef struct redisObject { unsigned type:4; unsigned encoding:4; /* key对象内部时钟 */ unsigned lru:LRU_BITS; int refcount; void *ptr; } robj;

近似的 LRU 算法实际原理是 维护一个候选池(大小16),第一次选取 5 个(默认值)key 放到池中,随后每次选取的 key 值只有 访问时间(与系统时钟)间隔 大于 池中最小访问时间间隔的 才会被放到 池中,直到放满,如果有新加入的,则移除间隔时间最小的 key,当需要淘汰时,则直接从池中选取时间间隔最大(最久没用被调用)的进行淘汰。

LRU 和 近似 LRU 效果对比

下图是常规LRU淘汰策略与Redis随机样本取一键淘汰策略的对比,浅灰色表示已经删除的键,深灰色表示没有被删除的键,绿色表示新加入的键,越往上表示键加入的时间越久。从图中可以看出,在redis 3中,设置样本数为10的时候能够很准确的淘汰掉最久没有使用的键,与常规LRU基本持平。

3|4LFU 算法


概念

LFU(Least Frequently Used),它的核心思想是 如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小,所有就可以被淘汰掉。

实现原理

根据 key 的最近访问频率进行淘汰,很少被访问的优先被淘汰,被访问多的则留下来。

下面使用 PHP 实现 LFU 算法

class LFUCache
{
    private $cache = [];
    private $maxSize = 0;
    // 访问时间key=>count
    private $lfu = [];

    function __construct($size)
    {
        // 缓存最大存储数量
        $this->maxSize = $size;
    }

    public function set($key, $value)
    {
        // 如果存在,就更新访问次数+1
        if (isset($this->cache[$key])) {
            $this->lfu[$key] += 1; 
        }
        // 长度检查,超长则删除最久访问数据
        $this->cleanup();

        // 插入元素, 更新访问次数
        $this->cache[$key] = $value;
        if (!isset($this->lfu[$key])) {
            $this->lfu[$key] = 1; 
        }
    }

    public function cleanup()
    {
        if (count($this->cache) >= $this->maxSize) {
            asort($this->lfu);
            $k = array_keys($this->lfu)[0];
            unset($this->cache[$k]);
            unset($this->lfu[$k]);
        }
        return true;
    }

    public function get($key)
    {
        $resultValue = null;
        if (isset($this->cache[$key])) {
            $resultValue = $this->cache[$key];
            // 更新访问时间
            $this->lfu[$key] += 1;
        }
        
        return $resultValue;
    }

    public function getAll()
    {
        return $this->cache;
    }
}

$cache = new LFUCache(3);
$cache->set('a', 1);
$cache->set('b', 2);
$cache->set('c', 3);

var_dump($cache->getAll());
$cache->get('a');
$cache->set('d', 4);
var_dump($cache->getAll());

LFU 在 redis 中的实现

LFU是在Redis4.0后出现的,LRU的最近最少使用实际上并不精确,考虑下面的情况,如果在|处删除,那么A距离的时间最久,但实际上A的使用频率要比B频繁,所以合理的淘汰策略应该是淘汰B。LFU就是为应对这种情况而生的。

A~~A~~A~~A~~A~~A~~A~~A~~A~~A~~~|
B~~~~~B~~~~~B~~~~~B~~~~~~~~~~~B|

LFU 把原来的 key 对象的内部时钟的 24 位分成两部分,前16位还代表时钟,后8位代表一个计数器。16位的情况下如果还按照秒为单位就会导致不够用,所以一般这里以时钟为单位。而后8位表示当前 key 对象的访问频率,8位只能代表255,但是redis 并没有采用线性上升的方式,而是通过一个复杂的公式,通过配置两个参数来调整数据的递增速度。
下图从左到右表示 key 的命中次数,从上到下表示影响因子,在影响因子为100的条件下,经过10M次命中才能把后8位值加满到255.

# +--------+------------+------------+------------+------------+------------+
# | factor | 100 hits   | 1000 hits  | 100K hits  | 1M hits    | 10M hits   |
# +--------+------------+------------+------------+------------+------------+
# | 0      | 104        | 255        | 255        | 255        | 255        |
# +--------+------------+------------+------------+------------+------------+
# | 1      | 18         | 49         | 255        | 255        | 255        |
# +--------+------------+------------+------------+------------+------------+
# | 10     | 10         | 18         | 142        | 255        | 255        |
# +--------+------------+------------+------------+------------+------------+
# | 100    | 8          | 11         | 49         | 143        | 255        |
# +--------+------------+------------+------------+------------+------------+
  uint8_t LFULogIncr(uint8_t counter) {
      if (counter == 255) return 255;
      double r = (double)rand()/RAND_MAX;
      double baseval = counter - LFU_INIT_VAL;
      if (baseval < 0) baseval = 0;
      double p = 1.0/(baseval*server.lfu_log_factor+1);
      if (r < p) counter++;
      return counter;
  }
lfu-log-factor 10
lfu-decay-time 1

上面说的情况是 key 一直被命中的情况,如果一个 key 经过几分钟没有被命中,那么后8位的值是需要递减几分钟,具体递减几分钟根据衰减因子lfu-decay-time来控制

unsigned long LFUDecrAndReturn(robj *o) {
    unsigned long ldt = o->lru >> 8;
    unsigned long counter = o->lru & 255;
    unsigned long num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;
    if (num_periods)
        counter = (num_periods > counter) ? 0 : counter - num_periods;
    return counter;
}
lfu-log-factor 10
lfu-decay-time 1

上面递增和衰减都有对应参数配置,那么对于新分配的 key 呢?如果新分配的 key 计数器开始为0,那么很有可能在内存不足的时候直接就给淘汰掉了,所以默认情况下新分配的 key 的后8位计数器的值为5(应该可配置),防止因为访问频率过低而直接被删除。

低8位我们描述完了,那么高16位的时钟是用来干嘛的呢?目前我的理解是用来衰减低8位的计数器的,就是根据这个时钟与全局时钟进行比较,如果过了一定时间(做差)就会对计数器进行衰减。

相关推荐

Redis合集-使用benchmark性能测试

采用开源Redis的redis-benchmark工具进行压测,它是Redis官方的性能测试工具,可以有效地测试Redis服务的性能。本次测试使用Redis官方最新的代码进行编译,详情请参见Redis...

Java简历总被已读不回?面试挂到怀疑人生?这几点你可能真没做好

最近看了几十份简历,发现大部分人不是技术差,而是不会“卖自己”——一、简历死穴:你写的不是经验,是岗位说明书!反面教材:ד使用SpringBoot开发项目”ד负责用户模块功能实现”救命写法:...

redission YYDS(redission官网)

每天分享一个架构知识Redission是一个基于Redis的分布式Java锁框架,它提供了各种锁实现,包括可重入锁、公平锁、读写锁等。使用Redission可以方便地实现分布式锁。red...

从数据库行锁到分布式事务:电商库存防超卖的九重劫难与破局之道

2023年6月18日我们维护的电商平台在零点刚过3秒就遭遇了严重事故。监控大屏显示某爆款手机SKU_IPHONE13_PRO_MAX在库存仅剩500台时,订单系统却产生了1200笔有效订单。事故复盘发...

SpringBoot系列——实战11:接口幂等性的形而上思...

欢迎关注、点赞、收藏。幂等性不仅是一种技术需求,更是数字文明对确定性追求的体现。在充满不确定性的网络世界中,它为我们建立起可依赖的存在秩序,这或许正是技术哲学最深刻的价值所在。幂等性的本质困境在支付系...

如何优化系统架构设计缓解流量压力提升并发性能?Java实战分享

如何优化系统架构设计缓解流量压力提升并发性能?Java实战分享在高流量场景下。首先,我需要回忆一下常见的优化策略,比如负载均衡、缓存、数据库优化、微服务拆分这些。不过,可能还需要考虑用户的具体情况,比...

Java面试题: 项目开发中的有哪些成长?该如何回答

在Java面试中,当被问到“项目中的成长点”时,面试官不仅想了解你的技术能力,更希望看到你的问题解决能力、学习迭代意识以及对项目的深度思考。以下是回答的策略和示例,帮助你清晰、有说服力地展示成长点:一...

互联网大厂后端必看!Spring Boot 如何实现高并发抢券逻辑?

你有没有遇到过这样的情况?在电商大促时,系统上线了抢券活动,结果活动刚一开始,服务器就不堪重负,出现超卖、系统崩溃等问题。又或者用户疯狂点击抢券按钮,最后却被告知无券可抢,体验极差。作为互联网大厂的后...

每日一题 |10W QPS高并发限流方案设计(含真实代码)

面试场景还原面试官:“如果系统要承载10WQPS的高并发流量,你会如何设计限流方案?”你:“(稳住,我要从限流算法到分布式架构全盘分析)…”一、为什么需要限流?核心矛盾:系统资源(CPU/内存/数据...

Java面试题:服务雪崩如何解决?90%人栽了

服务雪崩是指微服务架构中,由于某个服务出现故障,导致故障在服务之间不断传递和扩散,最终造成整个系统崩溃的现象。以下是一些解决服务雪崩问题的常见方法:限流限制请求速率:通过限流算法(如令牌桶算法、漏桶算...

面试题官:高并发经验有吗,并发量多少,如何回复?

一、有实际高并发经验(建议结构)直接量化"在XX项目中,系统日活用户约XX万,核心接口峰值QPS达到XX,TPS处理能力为XX/秒。通过压力测试验证过XX并发线程下的稳定性。"技术方案...

瞬时流量高并发“保命指南”:这样做系统稳如泰山,老板跪求加薪

“系统崩了,用户骂了,年终奖飞了!”——这是多少程序员在瞬时大流量下的真实噩梦?双11秒杀、春运抢票、直播带货……每秒百万请求的冲击,你的代码扛得住吗?2025年了,为什么你的系统一遇高并发就“躺平”...

其实很多Java工程师不是能力不够,是没找到展示自己的正确姿势。

其实很多Java工程师不是能力不够,是没找到展示自己的正确姿势。比如上周有个小伙伴找我,五年经验但简历全是'参与系统设计''优化接口性能'这种空话。我就问他:你做的秒杀...

PHP技能评测(php等级考试)

公司出了一些自我评测的PHP题目,现将题目和答案记录于此,以方便记忆。1.魔术函数有哪些,分别在什么时候调用?__construct(),类的构造函数__destruct(),类的析构函数__cal...

你的简历在HR眼里是青铜还是王者?

你的简历在HR眼里是青铜还是王者?兄弟,简历投了100份没反应?面试总在第三轮被刷?别急着怀疑人生,你可能只是踩了这些"隐形求职雷"。帮3630+程序员改简历+面试指导和处理空窗期时间...

取消回复欢迎 发表评论: