你从未了解过的Redis内存淘汰机制
mhr18 2024-10-24 11:13 29 浏览 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位的计数器的,就是根据这个时钟与全局时钟进行比较,如果过了一定时间(做差)就会对计数器进行衰减。
相关推荐
- 【推荐】一个开源免费、AI 驱动的智能数据管理系统,支持多数据库
-
如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!.前言在当今数据驱动的时代,高效、智能地管理数据已成为企业和个人不可或缺的能力。为了满足这一需求,我们推出了这款开...
- Pure Storage推出统一数据管理云平台及新闪存阵列
-
PureStorage公司今日推出企业数据云(EnterpriseDataCloud),称其为组织在混合环境中存储、管理和使用数据方式的全面架构升级。该公司表示,EDC使组织能够在本地、云端和混...
- 对Java学习的10条建议(对java课程的建议)
-
不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...
- SQLShift 重大更新:Oracle→PostgreSQL 存储过程转换功能上线!
-
官网:https://sqlshift.cn/6月,SQLShift迎来重大版本更新!作为国内首个支持Oracle->OceanBase存储过程智能转换的工具,SQLShift在过去一...
- JDK21有没有什么稳定、简单又强势的特性?
-
佳未阿里云开发者2025年03月05日08:30浙江阿里妹导读这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。阅前声明:本文介绍的内容基于AJDK21.0.5[1]以及以上...
- 「松勤软件测试」网站总出现404 bug?总结8个原因,不信解决不了
-
在进行网站测试的时候,有没有碰到过网站崩溃,打不开,出现404错误等各种现象,如果你碰到了,那么恭喜你,你的网站出问题了,是什么原因导致网站出问题呢,根据松勤软件测试的总结如下:01数据库中的表空间不...
- Java面试题及答案最全总结(2025版)
-
大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...
- 数据库日常运维工作内容(数据库日常运维 工作内容)
-
#数据库日常运维工作包括哪些内容?#数据库日常运维工作是一个涵盖多个层面的综合性任务,以下是详细的分类和内容说明:一、数据库运维核心工作监控与告警性能监控:实时监控CPU、内存、I/O、连接数、锁等待...
- 分布式之系统底层原理(上)(底层分布式技术)
-
作者:allanpan,腾讯IEG高级后台工程师导言分布式事务是分布式系统必不可少的组成部分,基本上只要实现一个分布式系统就逃不开对分布式事务的支持。本文从分布式事务这个概念切入,尝试对分布式事务...
- oracle 死锁了怎么办?kill 进程 直接上干货
-
1、查看死锁是否存在selectusername,lockwait,status,machine,programfromv$sessionwheresidin(selectsession...
- SpringBoot 各种分页查询方式详解(全网最全)
-
一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...
- 《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略
-
《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...
- LoadRunner(loadrunner录制不到脚本)
-
一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...
- Redis数据类型介绍(redis 数据类型)
-
介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...
- RMAN备份监控及优化总结(rman备份原理)
-
今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle位图索引 (63)
- oracle批量插入数据 (62)
- oracle事务隔离级别 (53)
- oracle 空为0 (50)
- oracle主从同步 (55)
- oracle 乐观锁 (51)
- redis 命令 (78)
- php redis (88)
- redis 存储 (66)
- redis 锁 (69)
- 启动 redis (66)
- redis 时间 (56)
- redis 删除 (67)
- redis内存 (57)
- redis并发 (52)
- redis 主从 (69)
- redis 订阅 (51)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)
- redis 限流 (51)