我们知道为了保证较高的性价比,缓存的空间容量必然要小于后端数据库的数据总量。随着要缓存的数据量越来越大,有限的缓存空间不可避免地会被写满。
解决这个问题就涉及到缓存系统的一个重要机制,即缓存数据的淘汰机制。数据淘汰机制包括两步:
- 第一,根据一定的策略,筛选出对应用访问来说“不重要”的数据;
- 第二,将这些数据从缓存中删除,为新来的数据腾出空间。
然而在正式介绍Redis的缓存淘汰机制之前,我们不妨关注一个问题:以系统在DB的数据量为基准,你会为你的系统缓存设置百分之多少的容量?
根据数据访问的局部性原理,在大多数的业务场景下,80% 的请求实际只访问了 20% 的数据(八二原理)。
如下图所示:
蓝线代表了“八二原理”表示的数据局部性,而红线则表示在当前应用负载下的数据局部性。蓝线描述的是正常情况下有 20% 的数据贡献了 80% 的访问量。这 80% 的数据在访问量上就形成了一条长长的尾巴,我们也称为“长尾效应”。
真实业务中,用户的个性化需求越来越多,在一个业务应用中,不同用户访问的内容可能差别很大,所以20% 的数据可能贡献不了 80% 的访问,而剩余的 80% 数据反而贡献了更多的访问量,我们称之为重尾效应。因此缓存容量占总数据量的比例,从 5% 到 40% 的都有,需要结合应用数据实际访问特征综合考虑的。一般来说,会建议把缓存容量设置为总数据量的 15% 到 30%,兼顾访问性能和内存空间开销。
说完了如何设置缓存容量以及依据之后,我们开始Redis内存管理的正题。
一、Redis内存组成、内存分配和释放
Redis的内存组成主要包括以下几个部分:
(1) 自身内存:这是Redis服务器运行所需的基本内存,包括Redis进程不存储任何key-value时本身所占用的内存。
(2) 键值对内存(key和value):
- key:存储Redis中每个键的内存占用。
- 对象内存(与value相关):存储Redis中每个值(对象)的内存占用。Redis中的值可以是字符串、列表、集合、哈希表、有序集合等数据类型,每种数据类型都有其特定的内部表示和内存占用。
(3) 缓冲内存:
- 客户端缓冲:用于存储客户端请求和响应数据的缓冲区。
- 复制积压缓冲:用于支持主从复制功能,存储复制过程中的数据缓冲区。
- AOF缓冲:如果启用了AOF(Append-Only File)持久化,则会有一个缓冲区用于存储写操作,以便在后台将其写入磁盘。
(4) 子进程内存:Redis可能会创建子进程来执行某些操作,如RDB快照持久化、AOF重写等。这些子进程也会占用相当大的内存。
(5) 内存碎片:由于Redis的内存分配器在分配和释放内存时可能会产生碎片,这些碎片会占用额外的内存空间。虽然Redis的内存分配器(如jemalloc)会尽量减少碎片的产生,但在长时间运行或频繁进行内存分配和释放的情况下,仍然可能会产生一定的内存碎片。
再说说看Redis的内存分配和释放过程。
Redis的内存分配和释放过程是其内存管理的核心环节,它确保了Redis能够高效地利用内存资源。以下是对Redis内存分配和释放过程的详细阐述:
1.内存分配过程
(1) 初始化阶段:
- 在Redis服务器启动时,会初始化一个空的内存空间,这个空间称为“arena”。Arena是一片连续的内存区域,用于存储各种数据结构和对象。
- Redis使用的Arena大小可以通过配置文件进行调整,默认为32MB。
(2) 动态分配阶段:
- 当有一个新的对象需要分配内存时,Redis会调用其内存分配器(默认是jemalloc)进行内存分配。
- Redis根据对象的大小选择合适的Arena进行内存分配。对象的大小决定了它在Arena中的存储位置。较小的对象通常存储在小的Arena中,较大的对象存储在大的Arena中。
- 在选定的Arena中,Redis会根据对象的大小分配一块合适大小的内存块。内存块的大小可能与实际需要的大小略有不同,这是为了减少内存碎片和提高分配效率。
- 分配的内存块会被初始化为指定类型的对象。
(3) 内存分配策略:
- Redis的内存分配器将内存空间划分为多个部分,如Small class、Large class、Huge class,每个部分又划分为很多小的内存块单位。这样,Redis可以采用不同大小的内存块来存储不同大小的内存对象。
- 例如,一个5KB的对象可能会存储在一个8KB的内存块中,剩下的3KB内存就变成了内存碎片,但这些碎片仍然可以被jemalloc管理,并在后续的内存分配中重用。
2.内存释放过程
(1) 主动释放:
- 使用DEL命令可以手动删除指定的键及其值,从而释放其占用的内存。
- FLUSHDB命令用于删除当前数据库中的所有键及其值,释放整个数据库的内存。
- FLUSHALL命令则用于删除所有数据库中的所有键及其值,释放所有数据库的内存。
(2) 被动释放(内存淘汰机制):
- 当Redis使用的内存达到maxmemory配置的限制时,Redis会根据配置的内存淘汰策略来释放内存。
- 常见的淘汰策略包括volatile-lru、volatile-ttl、allkeys-lru等,这些策略可以根据具体的业务场景和需求来选择,以平衡内存使用和性能。
(3) 内存碎片整理:
- 内存碎片化会导致内存浪费和碎片化问题。Redis提供了defragment命令来进行内存碎片整理,将内存中的碎片空间合并起来,减少内存占用。
(4) 对象池管理:
- Redis内部维护了一个对象池,对象的分配和回收都会通过对象池的方式进行。
- 当一个对象被释放后,它会被放回对象池,以便下次再次使用。
- 当Redis不再需要某个对象时,它会将对象从对象池中移除,并将相应的内存块标记为空闲状态。空闲的内存块可以被其他对象复用。
(5) 内存回收:
- 当大量的内存块为空闲状态时,Redis会调用jemalloc的内存回收函数来重新组织已分配的内存,以减少内存碎片。
二、Redis的键值过期处理策略
Redis的键值过期策略是内存管理中的重要一环。这里首先需要说一下,可能会有人将Redis的过期策略和淘汰策略给搞混,但其实两者并不是一回事。
Redis的过期策略是指对设置了过期时间的键进行管理,当这些键的过期时间到达时,Redis会采取相应的操作来删除这些键。其主要作用是确保Redis中存储的数据是有效的、最新的,并且防止无用数据长期占用内存资源。其触发条件是键的过期时间到达,这可以通过Redis的EXPIRE、PEXPIRE等命令来设置键的过期时间。
Redis的淘汰策略是指在内存使用达到上限时,为了保持Redis的稳定运行和内存的有效利用,Redis会采取一系列措施来淘汰部分键。淘汰策略的主要作用是防止Redis内存溢出,确保Redis能够持续稳定地提供服务。淘汰策略的触发条件是Redis的内存使用达到或超过其配置的最大内存限制(maxmemory)。
从作用对象来说,过期策略作用于设置了过期时间的键,而淘汰策略则可能作用于整个Redis实例中的所有键(取决于具体的淘汰策略)。
Redis的淘汰策略会在下面介绍,这里回到过期策略。
Redis对过期键的删除策略主要包括:惰性删除和定期删除。
1.惰性删除
原理:在客户端尝试访问一个键时,Redis会检查该键是否过期。如果已过期,则删除该键并返回“键不存在”的响应;否则,返回该键的值。
(1) 优点:
- 只有在真正需要访问键时才会进行删除操作,减少了不必要的性能开销。
- 对CPU友好,因为不会在删除其他无关的过期键上花费CPU时间。
(2) 缺点:
- 如果数据库中有大量的过期键而它们又没有被访问到,那么这些键将永远不会被删除,可能导致内存泄漏。
- 对内存不友好,因为过期键可能长时间占用内存空间。
2.定期删除
(1) 原理:通过一种定时任务(如Redis的serverCron)来定期删除过期键。每隔一段时间,Redis会对一部分数据库中的键进行检查,并删除其中过期的键。
(2) 优点:
- 通过定期删除过期键,有效减少了内存浪费。
- 相比定时删除,对CPU的影响较小,因为删除操作是批量进行的,而且频率可控。
(3) 缺点:
- 可能会带来一定的性能开销,因为需要定期遍历数据库来检查过期键。
- 确定删除操作执行的时长和频率是一个难点,需要根据实际情况进行权衡和调整。
三、Redis的键值淘汰策略
首先为什么Redis需要淘汰策略呢?如果让大家回答这个问题,可能大家想到的可能只有:因为内存满了,所以需要根据一定的策略淘汰掉一些旧的或者少用的键值来存储新的或者常用的键值。但其实还可以从性能、数据一致性、内存成本和应对突发流量等角度来回答这个问题。
Redis需要淘汰策略的原因主要与其内存管理的特性和应用场景有关。以下是几个关键的原因:
- 内存限制:Redis是一个内存数据库,它依赖于内存来存储数据。然而,服务器的内存资源是有限的。当Redis使用的内存达到某个上限(通常由maxmemory配置参数指定)时,为了避免内存溢出和服务器崩溃,Redis需要一种机制来腾出空间,这就是淘汰策略的作用。
- 性能优化:通过淘汰策略,Redis可以移除那些不常用或不再需要的数据,从而保持内存中的数据是活跃和有用的。这有助于提高Redis的查询性能和响应速度,因为Redis可以更快地定位到用户需要的数据。
- 成本考虑:虽然增加服务器的内存可以容纳更多的数据,但这也会增加硬件成本。通过合理的淘汰策略,Redis可以在不牺牲太多性能的情况下,在有限的内存资源上运行,从而降低了成本。
- 数据一致性:在某些情况下,Redis中的数据可能是临时或缓存性质的。对于这些数据,使用淘汰策略可以确保Redis始终存储最新的或最重要的数据,而不是保留过时的或不再需要的数据。
- 应对突发流量:在突发流量或高并发场景下,Redis可能会接收到大量的数据请求。通过淘汰策略,Redis可以在内存资源紧张时自动释放一些数据,从而确保系统能够继续稳定运行,而不会因内存耗尽而崩溃。
那么Redis的整体淘汰键值的流程是怎么样的呢?
Redis淘汰策略的流程主要涉及内存检测、淘汰策略选择、数据取样、淘汰池维护、数据删除等步骤。以下是详细的流程说明:
1.内存检测
(1) 周期性内存检查:
- Redis会周期性地检查当前使用的内存是否超过了配置的maxmemory限制。
- 这一检查是通过调用freeMemoryIfNeeded函数来实现的。
(2) 判断是否需要淘汰数据:
- 如果当前内存使用量没有超过maxmemory,则不需要进行淘汰操作。
- 如果超过了maxmemory,则需要根据配置的淘汰策略来选择要淘汰的数据。
2.淘汰策略选择
(1) 配置淘汰策略:
- Redis提供了多种淘汰策略,如volatile-lru、allkeys-lru、volatile-lfu(如果Redis版本支持)、allkeys-lfu、volatile-ttl、volatile-random、allkeys-random以及no-eviction等。
- 这些策略可以在Redis的配置文件中通过maxmemory-policy参数进行配置。
(2) 选择淘汰策略:
- 根据配置的淘汰策略,Redis会决定是从设置了过期时间的数据集中选择(如volatile-lru),还是从所有数据中选择(如allkeys-lru)。
- 同时,根据策略的不同,Redis会选择不同的算法来确定要淘汰的数据,如LRU算法、LFU算法等。
3.数据取样
(1) 随机取样:
- Redis会从数据集(或设置了过期时间的数据集)中随机选择一定数量的key作为取样数据。
- 取样的数量可以通过maxmemory-samples参数进行配置,默认值为5。
(2) 计算访问频率或时间:
- 对于LFU策略,Redis会计算每个取样数据的访问频率。
- 对于LRU策略,Redis会记录每个取样数据的最近访问时间。
3.淘汰池维护
(1) 淘汰池初始化:
- Redis会维护一个淘汰池(eviction pool),用于存储待淘汰的key。
- 淘汰池的大小是固定的,默认为16。
(2) 数据比较与排序:
- 将取样数据与淘汰池中的数据进行比较,根据淘汰策略(如LRU、LFU等)来确定哪些数据更适合被淘汰。
- 将更适合被淘汰的数据放入淘汰池(例如如果按照LRU策略来淘汰的话,那么如果取样数据的最后访问时间比淘汰池中的最晚访问的数据的访问事件要早,那么这个取样数据就会被放入淘汰池),并按照适合的程度对淘汰吃中的数据进行排序。
4.数据删除
(1) 选择最佳淘汰数据:
- 从淘汰池中选择最适合被淘汰的数据(通常是排序最靠后的数据)。
(2) 删除数据:
- 从Redis数据库中删除选中的key及其对应的value。
- 将删除操作的消息发布到本地(AOF持久化)和从机(主从连接)。
(3) 更新内存使用量:
- 删除数据后,更新Redis的内存使用量。
- 如果删除后内存使用量仍然超过maxmemory,则继续执行淘汰流程,直到内存使用量满足要求为止。
上面的Redis淘汰流程中说到,Redis会根据具体的淘汰策略来决定哪些键值应该被淘汰,下面介绍一下Redis的淘汰策略。
其实Redis的键值淘汰策略遵循主流的内存淘汰算法:LFU和LRU算法。所以在说具体的淘汰策略之前,我会先简单介绍下这两种算法的特点和实现。
5.LRU算法
(1) 原理:
LRU算法根据数据的访问时间来决定哪些数据会被淘汰。其核心思想是:最久未被访问的数据被认为是最不常用的数据,应该被优先淘汰。
(2) 实现方式:
Redis使用双向链表和哈希表来实现LRU算法。链表用于维护数据的使用顺序,链表的头部表示最新使用的数据,而链表的尾部表示最旧的数据。哈希表则用于加快数据的查找速度,哈希表的键是数据的键,值是指向双向链表节点的指针。
当有数据被访问时,Redis会将该数据从链表中当前位置移动到链表头部。这样一来,经常被访问的数据就会一直保持在链表头部,而少访问的数据就会逐渐向链表尾部移动。当需要淘汰数据时,只需要选择链表尾部的数据即可。
(3) 特点:
LRU算法实现简单,能够较好地保留热数据(即经常被访问的数据),提高命中率。
但是,LRU算法可能会受到突发性访问模式的影响,即某些数据可能只是偶尔被访问一次,但根据LRU算法,这些数据可能会被保留在内存中较长时间。
(4) 适用场景:
LRU算法适用于访问频率分布相对均匀的场景,能够较好地平衡内存使用和性能需求。
6.LFU算法
(1) 原理:
LFU算法根据数据被访问的频率来决定哪些数据会被淘汰。其核心思想是:使用频率越低的数据被认为是最不常用的数据,应该被优先淘汰。
(2) 实现方式:
Redis使用哈希表和最小堆(或其他数据结构,如双向链表和计数器等)来实现LFU算法。哈希表用于存储数据和其对应的访问频率,而最小堆则用于快速找到访问频率最低的数据。
当有数据被访问时,Redis会更新该数据在哈希表中的访问频率。当需要淘汰数据时,Redis会从最小堆中选择访问频率最低的数据进行删除。下图展示了LFU算法执行的过程。
(3) 特点:
LFU算法能够更准确地识别出那些真正不常用的数据,并将其淘汰掉。
但是,LFU算法可能会受到访问频率的波动影响,即某些数据可能在某个时间段内被频繁访问,但之后就不再被访问了。这些数据可能会因为之前的访问频率较高而被保留在内存中较长时间。
(4) 适用场景:
LFU算法适用于某些数据的访问频率明显高于其他数据的场景,能够更准确地保留热门数据并淘汰冷门数据。
三、对比与选择
对比:
- LRU算法主要关注数据的访问时间,而LFU算法主要关注数据的访问频率。
- LRU算法实现简单且高效,但可能受到突发性访问模式的影响;而LFU算法能够更准确地识别出不常用的数据,但可能受到访问频率波动的影响。
了解了LRU和LFU算法是怎么一回事之后,我们再看看Redis具体的淘汰策略。Redis的主要淘汰机制及其适用场景如下:
(1) noeviction(不淘汰)
描述:当内存不足时,对写请求不再提供服务,直接返回错误(但DEL请求和部分特殊请求除外)。
适用场景:适合需要严格控制内存使用量的场景,但可能导致写入失败。当数据完整性或写入操作的重要性高于缓存性能时,可以选择此策略。
(2) allkeys-lru(从所有key中使用LRU算法淘汰)
描述:使用LRU(Least Recently Used,最近最少使用)算法从所有key中选择最久未使用的数据进行淘汰。
适用场景:适用于缓存场景,确保常用数据优先保留,不区分是否设置过期时间。当数据访问模式具有明显的时间局部性(即最近被访问的数据更有可能在未来被访问)时,此策略效果较好。
(3) volatile-lru(从设置了过期时间的key中使用LRU算法淘汰)
描述:仅对设置了过期时间的key使用LRU算法进行淘汰。
适用场景:适合需要定期清理过期数据的场景,同时希望保留最近使用的数据。当数据具有明确的过期时间,并且希望确保在内存紧张时优先淘汰这些过期数据中较久未使用的部分时,此策略较为合适。
(4) allkeys-random(从所有key中随机淘汰)
描述:从所有key中随机选择数据进行淘汰。
适用场景:适用于缓存策略不明确的场景,或者当数据访问模式没有明显的规律时。此策略较为随机,不保证常用数据优先保留。
(5) volatile-random(从设置了过期时间的key中随机淘汰)
描述:仅对设置了过期时间的key进行随机淘汰。
适用场景:适合不确定热点数据的场景,但随机淘汰方式可能不适合对性能有要求的应用。当数据具有过期时间,且对淘汰哪部分数据没有特定要求时,可以选择此策略。
(6) volatile-ttl(淘汰过期时间剩余最短的)
描述:在设置了过期时间的key中,淘汰过期时间剩余最短的。
适用场景:适合短时间内需要清除快过期的数据,但不适合热点数据访问的场景。当希望优先淘汰那些即将过期的数据时,此策略较为合适。
(7) allkeys-lfu(从所有key中使用LFU算法淘汰,如果Redis版本支持)
描述:使用LFU(Least Frequently Used,最少频率使用)算法从所有key中选择访问频率最低的数据进行淘汰。
适用场景:适用于访问模式稳定但不同key的访问频率差异明显的场景。当希望保留那些经常被访问的数据,并淘汰那些很少被访问的数据时,此策略效果较好。
(8) volatile-lfu(从设置了过期时间的key中使用LFU算法淘汰,如果Redis版本支持)
描述:仅对设置了过期时间的key使用LFU算法进行淘汰。
适用场景:适合需要在一定时间内淘汰使用频率较低的数据的场景,同时这些数据具有明确的过期时间。当希望确保在内存紧张时优先淘汰那些过期数据中访问频率较低的部分时,此策略较为合适。