news 2026/9/16 1:23:02

手撕LRU缓存:从Java标准实现到Redis内存淘汰机制全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手撕LRU缓存:从Java标准实现到Redis内存淘汰机制全解析

已经进入后端的面试季,几乎每一轮技术面都会有一道“手撕LRU缓存”的题目,你说它是八股吧,它确实高频出现;你说它有深度吧,真往底层问下去又可以一直聊到Redis的内存淘汰机制。最近我在梳理项目里的缓存治理方案时,又把LRU从数组最朴素的实现到Java里HashMap加双向链表的标准解,再到Redis近LRU的抽样淘汰逻辑完整过了一遍,颇有一种把一条知识链路彻底打通的感觉。这篇文章就顺着这条链路往下走,先把面试必考题的解法讲透,再剖析Redis内存淘汰背后到底藏了哪些设计权衡,最后附上线上排查和调优的经验。不管你是正在准备面试,还是工作中被Redis内存打满折腾过,这篇都值得花十几分钟认真看。

1. 先搞明白LRU到底是什么:为什么它成了缓存淘汰的事实标准

1.1 一句话讲清楚LRU的核心思想

LRU(Least Recently Used)翻译过来叫最近最少使用,核心思想特别直白:在一段时间内,如果某个数据被访问过,那么它在未来被再次访问的概率也更高;反过来,如果某个数据很久没被访问,那它大概率已经“凉了”,下次内存不够时优先淘汰它。

这个规律在计算机领域被称为局部性原理,包含时间局部性和空间局部性两层含义。时间局部性说的是一段频繁访问的指令或数据很可能马上还会被访问;空间局部性说的是某个地址附近的数据往往也会被一起访问。缓存系统之所以有效,依赖的就是这种访问规律,而LRU正是把时间局部性转化成了具体的淘汰规则。

举一个生活中的例子就很好理解了。你衣柜里有很多衣服,常穿的那几件永远挂在最顺手的位置,换季不穿的衣服早就塞进了收纳箱深处。衣帽间空间有限,新买一件衣服又没地方放的时候,你自然会把最久没穿过的那件处理掉。LRU干的就是这件事:把最久没穿的那件“衣服”请出去。

这里面有个细微但重要的区别值得先点一下:LRU淘汰的是“最久未被访问”的数据,而不是“最早写入”的数据。同一个key,如果写入很久但一直在被读取,它在LRU眼中仍然是“热”数据,不该被淘汰。这个逻辑乍一看很简单,但很多人在面试手写代码时恰恰会在这个基础上出问题,后面我会具体说。

1.2 面试官考LRU,到底在考什么

我做过一段时间的技术面试,坦白讲,面试官让你手撕LRU,意图绝不只是看你会不会背一个数据结构。一道LRU题背后至少隐藏了四层考察点。

第一层:你到底理解不懂淘汰规则。有些人把LRU和LFU搞混,写出来的逻辑是按访问次数淘汰,还有人把缓存删除和缓存过期混为一谈。能准确说出“按最后访问时间排序,淘汰最久未访问的”这一步,才算迈过了门槛。

第二层:你能不能设计出O(1)复杂度的读写实现。这是这道题最硬核的部分。如果你只想到数组加时间戳的暴力解法,那复杂度是O(n),面试官大概率会追问一句“还有没有更优做法”。只有答出HashMap加双向链表的标准组合,才算是过了核心考点。

第三层:你会不会处理边界情况和并发问题。容量为1的极端场景、重复key写入、get不存在的key、节点移动时的指针操作,任何一个环节出bug都会让代码在测试用例上翻车。更有经验的面试官还会问一句“并发环境下你这个实现还对不对”,这就涉及到加锁粒度、原子操作等更深层的问题。

第四层:你能不能举一反三。如果面试官顺着LRU继续问你“Redis的内存淘汰是怎么实现的”,你如果只能答出“maxmemory-policy设置成allkeys-lru”,那肯定是不够的。Redis的近似LRU和手写的标准LRU差了多远,为什么Redis不直接用标准LRU,这些才是真正把面试从八股推向深度的分水岭。

1.3 两种常见的理解误区

在继续往下讲实现之前,我想先把两个最容易踩的认知误区拎出来说清楚。

第一个误区是把“缓存过期”和“缓存淘汰”混为一谈。缓存过期是key本身带了TTL(Time To Live),时间到了自然失效;淘汰是内存不够时,不管key有没有过期,都可能被当作淘汰对象。在Redis里这两个机制是完全独立的,后面讲Redis内存淘汰时会详细展开。

第二个误区是误以为LRU一定能留下最“重要”的数据。实际上LRU只看“最后一次访问时间”,不看访问频率。一个昨天被访问过一次的冷门key,和一个过去一周每天被访问几千次但最后一次访问发生在两小时前的key相比,LRU可能先淘汰后者,这就是LRU的天然盲区,也直接催生了LFU(Least Frequently Used)策略的出现。这个缺陷不是理论上的杞人忧天,在真实业务里你很可能遇到过热点key被莫名其妙淘汰的情况。

2. 手撕LRU:从暴力解法到标准的O(1)实现

2.1 先交一版能跑的:数组加时间戳

很多人在面试现场听到“手写LRU”之后,第一反应是来一个数组,每个元素存(key, value, 最后访问时间),get的时候遍历数组找到key并更新时间戳,put的时候如果key存在就更新,不存在就插入,如果容量满了就扫描整个数组把时间戳最旧的那个删掉。

这个方案在逻辑上完全正确,但致命的问题是性能。get要遍历数组O(n),put要遍历找keyO(n),满了之后还要再遍历一次O(n),三个核心操作全是线性复杂度。当缓存容量到万级甚至十万级以上,这个实现基本就没法用了,每一次淘汰都相当于全表扫描一遍,时延高到离谱。

所以这个版本唯一的用处,是帮你建立“按访问时间淘汰”的直觉。它用最直观的方式表达了LRU的定义,仅此而已。面试的时候你要是只交这一版,那大概率会被一句“还能优化吗”给顶回来。

2.2 标准答案:HashMap加双向链表

O(1)时间复杂度的关键组合是HashMap加双向链表,这是教科书级的解法,也是面试官最期待看到的方案。

设计思路拆开看是这样的:

  • HashMap负责O(1)地根据key找到对应的链表节点,解决查找效率问题;
  • 链表负责维护访问顺序,链头放最近访问的节点,链尾放最久未访问的节点,解决排序和淘汰问题;
  • 双向链表负责让任意节点的插入和删除都达到O(1),因为每个节点都存着前驱和后继指针,不需要为了找到某个节点的前一个节点而从头遍历。

这三者是缺一不可的组合。有人问过为什么不用单向链表?单向链表删除任意节点时必须先找到它的前驱节点,而找前驱只能从头遍历,复杂度又回到O(n)。所以双向链表不是“炫技”,是纯粹从性能要求倒推出来的数据结构选择。

下面是完整的Java实现,这段代码我基本是按面试标准写法整理的,关键位置都加了注释,可以直接在本地跑:

import java.util.HashMap; import java.util.Map; public class LRUCache { // 双向链表节点 static class Node { int key; int value; Node prev; Node next; public Node() {} public Node(int key, int value) { this.key = key; this.value = value; } } private final int capacity; private final Map<Integer, Node> map = new HashMap<>(); // 哨兵节点,避免空指针判断 private final Node head = new Node(); private final Node tail = new Node(); public LRUCache(int capacity) { this.capacity = capacity; head.next = tail; tail.prev = head; } public int get(int key) { Node node = map.get(key); if (node == null) { return -1; } // 一旦被访问,就把节点移动到链表头部 moveToHead(node); return node.value; } public void put(int key, int value) { Node node = map.get(key); if (node != null) { // key已存在:更新值,并移动到头部 node.value = value; moveToHead(node); } else { // key不存在:新建节点,插入头部 Node newNode = new Node(key, value); map.put(key, newNode); addToHead(newNode); // 超出容量:淘汰链表尾部的节点 if (map.size() > capacity) { Node tailNode = removeTail(); map.remove(tailNode.key); } } } private void addToHead(Node node) { node.prev = head; node.next = head.next; head.next.prev = node; head.next = node; } private void removeNode(Node node) { node.prev.next = node.next; node.next.prev = node.prev; } private void moveToHead(Node node) { removeNode(node); addToHead(node); } private Node removeTail() { Node node = tail.prev; removeNode(node); return node; } }

这段代码里最关键的是哨兵节点的用法。head和tail作为两个虚拟节点,不存储任何实际业务数据,只负责标记链表的头和尾。有了它们,插入头节点和删除尾节点时不需要判断链表是否为空,也不需要对头尾节点做特殊处理,代码干净很多,也不容易在边界情况下写出空指针bug。

2.3 手写实现时最容易翻车的几个细节

代码看着不长,但我在面试现场见过太多人在细节上栽跟头,这里单独挑几个高频坑讲一讲。

第一个坑是只更新value忘了移动节点顺序。put一个已存在的key时,有人会下意识地只改value,然后直接return,完全不调整链表位置。这在功能上不算错,但缓存的热度信息没有被更新,这个key明明是刚被写入的,却被排在了最久未访问的位置,下次淘汰它就成了第一个牺牲品。正确的做法是:更新value之后,必须把它从链表中摘下来,重新放到头部。

第二个坑是容量为1时的极端情况。插入新节点后链表里只有这一个节点,此时淘汰尾部时removeTail拿到的是tail.prev,正好是这个唯一的节点,逻辑上能跑通,但前提是哨兵节点的初始化一定要正确。head.next必须指向tail,tail.prev必须指向head,这个初始状态一旦写错,后面所有的指针操作都会乱套。

第三个坑是指针操作顺序问题。addToHead方法里,很多人会先写head.next = node再写head.next.prev = node,结果因为head.next已经被覆盖了,后面那行实际操作的是新节点的prev,导致链表断裂。正确的做法是先处理新节点的前驱后继,再处理原头节点的prev,最后才改head.next,顺序不能乱。

第四个坑是get一个不存在的key时返回什么。LeetCode风格的要求通常返回-1,但真实项目里如果value允许为负数,你得用异常或者包装对象才能区分“没找到”和“找到了value=-1”。这个在做工程实现时要留意,面试时用-1没问题,但能主动提出这个歧义,反而是一个加分细节。

2.4 并发环境下怎么改

如果面试官追问并发,你需要意识到一个事实:上面的实现完全没有做任何同步控制,多个线程同时读写会出问题。

最简单的改造是给所有public方法加synchronized,让get和put互斥执行。好处是正确性有了保障,坏处是吞吐量会变得很难看,因为get操作本身是高频读操作,却被强行串行化了。

进一步的优化是使用读写锁(ReentrantReadWriteLock),get方法加读锁,put方法加写锁,让多个读线程可以并发执行。考虑到LRU缓存绝大多数操作都是读取,这种优化效果立竿见影。

更激进的方案是像ConcurrentHashMap那样做分段锁甚至无锁化设计,但那已经超过一般面试的考察范围了。实际项目中如果你不是刚需,优先用现成的Caffeine或Guava Cache,不要自己造并发缓存轮子,这句话我后面还会再说一次。

3. 从手写LRU到Redis:工业级实现为什么选择了“近似LRU”

3.1 Redis为什么没有直接用标准LRU

既然手写LRU的标准解法是HashMap加双向链表,那Redis直接把这套方案搬过去不就行了吗?为什么Redis的内存淘汰策略被称为“近似LRU”,而不是真正的LRU?

原因有三个。

首先是内存开销的问题。标准LRU要求每个key额外保存两个指针(前驱prev和后驱next)来维持双向链表结构。在Java里这两个引用算两个对象头字段,在C语言里就是两个指针,按8字节算,100万个key光链表指针就要吃掉16MB。Redis本身以k-v存储见长,单实例动辄存几千万个key,这个指针成本会被急剧放大,得不偿失。

其次是操作的开销。标准LRU在每次get和put时都要把节点从原位置摘除再插入链表头部,中间涉及多个指针的赋值。Redis是单线程模型,任何操作都直接占用主线程执行时间,如果每次访问都要做链表调整,Redis的整体吞吐会被明显拉低。

第三个原因是全局有序结构的维护成本。标准LRU需要整个数据集保持一个有序的淘汰序列,这意味着数据结构内部必须时刻维护全局的顺序信息。Redis的设计哲学是“启动快、访问快、内存省”,为了内存淘汰引入一个全局有序结构,和自身定位严重不符。

于是Redis选择了一条更聪明的路:不必做到绝对准确,只要在绝大多数情况下淘汰掉“足够冷”的数据就行了。这就是近似LRU策略的出发点。

3.2 Redis近似LRU具体是怎么实现的

近似LRU的核心逻辑说起来特别简单,就一句话:在内存达到上限时,随机抽取一部分key,只在这部分key里淘汰掉最久没被访问的那个。

Redis官方把这个抽样数量称为maxmemory-samples,默认值是5。你可能觉得5个样本也太少了,但实践中5个样本已经能让近似LRU的效果达到真实LRU的90%以上。原因是你每次淘汰都在淘汰多个候选key里最旧的那个,长期下来,冷数据被淘汰的概率就非常高了,不必每次都很精准。

Redis 3.0之后还引入了一个非常重要的优化:淘汰池(eviction pool)。每次需要淘汰时,Redis会把采样出来的key连带它们的空闲时间(idle time)放进一个按空闲时间排序的池子里,然后从池子中选择最旧的key淘汰。下一次采样时,新样本会和池子里已有的候选key一起参与排序。这样即使单次采样数只有5,经过多轮淘汰后,池子里会累积非常多历史上被采样过的冷key,淘汰结果可以非常接近标准LRU。

这种“小样本+累积池”的思路很有意思,它相当于用极低的单次计算成本,换取了长期接近全局LRU的淘汰效果,是一种典型的工程折中。

3.3 Redis的LRU时钟:为什么用24bit就够了

每个Redis对象都维护了一个lru字段,标准LRU模式下存的就是这个对象最近一次被访问时的时间。但Redis没有直接用系统时间戳,而是维护了一个独立的LRU时钟server.lruclock,单位是秒,只有24bit。

24bit能表示的最大值是2^24-1,大概是1677万秒,折合194天。也就是说,Redis的LRU时钟大约每194天会回绕一次。回绕之后,较新的key可能因为时钟倒退被误判成“很久没访问”,进而被误淘汰。

这在实际场景里真的会发生吗?真实案例是有的。有人发现一台跑了半年多的Redis实例,明明有些热点key最近还在频繁访问,却被内存淘汰了。排查到最后发现就是LRU时钟回绕导致的误判。Redis 4.0之后对这个情况做了优化,不过如果你用的是老版本,遇到类似“热点数据莫名被淘汰”的问题,时钟回绕绝对是一个值得怀疑的方向。

这个细节提醒我们:工业级的“近似”背后,往往隐藏着时间精度、内存开销和实现复杂度的多重妥协。

3.4 Redis内存淘汰策略全景:不只是LRU一种

先看核心配置参数。Redis内存淘汰相关的配置有三个你很可能会碰到:

配置项作用默认值
maxmemory设置内存上限,单位是字节64位系统默认为0,表示不限制
maxmemory-policy内存达到上限后采取的淘汰策略noeviction
maxmemory-samples近似LRU或LFU的抽样数量5

这里要特别强调:如果maxmemory不设置,maxmemory-policy再怎么写都是白搭,因为内存根本没有上限,淘汰逻辑压根不会被触发。很多人排查了半天Redis内存持续上涨的问题,最后发现是maxmemory没配置。

maxmemory-policy共有8种取值,我整理成了一张表,方便你横向对比:

策略作用范围说明
noeviction不淘汰内存满时写命令直接报错,读命令正常
allkeys-lru所有key对所有key执行近似LRU淘汰
volatile-lru设置过过期时间的key只淘汰设置了TTL的key,按LRU
allkeys-random所有key随机淘汰任意key
volatile-random设置过过期时间的key在设置了TTL的key中随机淘汰
volatile-ttl设置过过期时间的key淘汰剩余生存时间最短的key
allkeys-lfu所有key对所有key执行LFU淘汰,Redis 4.0后支持
volatile-lfu设置过过期时间的key只淘汰设置了TTL的key,按LFU

其中noeviction的杀伤力最容易被低估。如果你用的是默认配置,maxmemory打满之后,所有写请求都会直接返回OOM错误,表现就是业务突然报错:“OOM command not allowed when used memory > ‘maxmemory’”。这种情况在线上比LRU淘汰本身更危险,因为宕机至少你能感知到,而这种只让写失败、读还正常的半瘫痪状态,最容易让业务团队排查很久。

策略选型本身没有银弹,但有一个经验可以分享:如果绝大多数key都设置了过期时间,用volatile-lru能让淘汰只发生在“本身就会过期”的key范围内,避免把那些永不过期的“重要配置类key”误淘汰掉;如果所有key一视同仁,用allkeys-lru更省心。LFU只在少数访问频率差异极大、热点非常集中的场景里有优势,后文会细说。

3.5 从LRU到LFU:当“老用户”挡住“新流量”的时候

LRU有一个很经典的问题,我前面简单提过:它记录的是“最后一次访问时间”,而不是“这段时间内到底被访问过多少次”。假设一个key昨天被访问了一次,今天你把它淘汰的候选队列里排了第2位;另一个key过去一周每天被访问1万次,只是最后一次访问发生在3小时前。按LRU规则,后面这个高频key反而更接近淘汰边缘,因为它的last access时间更旧。这种场景在秒杀、热点新闻、爆款商品里真实存在,一个KV缓存如果没有LFU能力,热key可能被冷key挤掉,导致缓存命中率骤降。

Redis在4.0版本引入了LFU策略,核心思想从“看最后访问时间”变成了“结合访问频率来判断热度”。但Redis的内存结构里,每个对象只有一个lru字段,标准LRU时用来存最后访问时间,LFU模式下对同一个字段做了重新拆分:高16位存“最后的衰减时间戳”(单位是分钟),低8位存一个近似访问计数器。注意计数器只有8bit,也就是最大只能表示255,是刻意做小的,目的是省内存。

这里有个很棒的设计细节:计数器的增长速度不是线性的,而是接近对数曲线。访问次数越多,计数器增长越慢。这样既能让高频key的计数保持在一个相对高位,又不会让一个极端高频的key直接把计数器打爆,留出了区分度。Redis还配合一个衰减机制,让计数随着时间自动降低,相当于给“热度”降权,否则所有老key的计数都越积越高,新key永远插不进来。

LFU两个关键参数一个是lfu-log-factor,控制计数增长快慢;另一个是lfu-decay-time,控制计数衰减周期,单位是分钟。线上调优时一般先调log-factor让热点频率能区分开,再调decay-time让热度能随业务周期自然回落,切忌两个参数一起乱改,否则很难判断是哪个参数导致命中率波动。

3.6 单线程模型下的淘汰成本如何控制

Redis是单线程处理命令,任何耗时操作都会阻塞后续所有请求,所以内存淘汰这套逻辑在设计上非常讲究“低成本”。

Redis在每次执行写入命令之前,会检查当前内存是否超过了maxmemory,如果超过了,就进入淘汰流程。淘汰流程中,Redis会调用函数从字典中随机采样key、计算空闲时间、维护淘汰池,然后逐出一个或多个key。整个过程的耗时虽然和采样数量、对象大小有关,但因为限制了采样次数(默认5),单次淘汰的成本是可以接受的。

但注意一个隐含风险:如果写入频率极高,内存快速膨胀,每次写入前都要做一次淘汰判断,淘汰线程的工作量会被放大。更麻烦的是,如果淘汰的key是大key(比如一个几十MB的字符串或一个庞大集合),逐出时要释放内存,这个过程同样在主线程执行,必然带来短暂的事件循环阻塞。所以Redis提供了UNLINK命令做异步删除,把释放内存的操作放到后台线程执行,主线程只负责把key从命名空间摘除,返回速度几乎不受影响。DEL和UNLINK在数据量小时看不出差别,但涉及超大对象时,差距能到几个数量级,这也是线上运维值得注意的细节。

4. 实战落地:从验证手写LRU到线上Redis内存治理

4.1 手写LRU怎么验证写对了

代码写完不等于一通百通,我强烈建议给LRU实现补几个单元测试,把关键行为锁死,否则线上出了问题根本分不清是缓存逻辑的锅还是其他模块的锅。

我常用的测试用例有这几条:

  • 容量为1时,连续put两个key,第一个key要被淘汰;
  • put同一个key两次,不会增加节点数量,且value是最后一次写入的值;
  • get一个不存在的key返回-1,且不会触发淘汰;
  • get完一个key之后,该key在后续淘汰时的顺序会延后;
  • 多次交替读写后,最终被淘汰的key一定是当前链表中“最久未被访问”的那个。

如果你偷懒不想写全套测试,至少要覆盖前两条,因为这两条最容易暴露链表指针操作的问题。

还有一个更隐蔽的坑:如果你在真实项目里自己造LRU,一定要特别小心value是引用类型的情况。缓存里存的是同一个对象引用,调用方改了对象内部字段,缓存也会跟着变,很多人排查半天数据不一致,最后发现缓存和上游共享了同一个可变对象。

4.2 线上Redis内存淘汰是否发生,怎么快速确认

一个真实场景:某天业务反馈数据库压力变大,Redis缓存命中率明显下降。你第一个动作应该是什么?

先确认Redis到底有没有在淘汰。执行一下:

redis-cli info stats | grep evicted_keys

evicted_keys这个计数器的值如果一直在增长,说明Redis确实在持续淘汰key。配合看used_memory和maxmemory的比值,如果used_memory已经顶着上限,那几乎可以断定是内存不足触发了淘汰。

还有一种更微妙的场景:evicted_keys没怎么增长,但缓存命中率还是低。这时候要怀疑是key集体过期导致的,去看expired_keys统计,可能会发现大量key在同一个时段集中过期,这就是典型的缓存雪崩场景。解决办法也简单,给TTL加一个随机偏移,让过期时间分散开,避免同一秒内几千个key一起失效。

缓存穿透和缓存击穿也和这里的话题紧密相关。缓存穿透是查一个根本不存在的数据,每次都绕过缓存打到数据库;缓存击穿是某个热点key刚好过期,瞬间大量请求打到DB。这两种情况单靠LRU解决不了,通常需要布隆过滤器、互斥锁重建缓存、逻辑过期等手段配合使用。大家常说“缓存三分靠数据结构,七分靠治理”,并不是夸张。

4.3 我把遇到过的高频问题整理成了速查表

现象可能原因排查思路
Redis写命令报OOM错误maxmemory-policy配置为noeviction且内存打满检查maxmemory-policy,改为allkeys-lru或volatile-lru
evicted_keys持续增长,命中率下降内存上限设置过低或存在大key调大maxmemory,定位大key并拆分,考虑数据压缩
设置了volatile-lru还是有key被淘汰大量key没设过期时间,淘汰池里全是可淘汰key改用allkeys-lru,或给关键业务key单独设置TTL保护
热点key频繁被淘汰LRU对访问频率不敏感,冷key残留切换到LFU策略,调整lfu-log-factor让高频key识别更准
Redis内存缓慢增长不下降大量已过期key未被及时清理检查过期key的清理机制,适当调整hz频率,必要时手动scan清理
淘汰造成数据库瞬时压力大大量key同时淘汰或过期给TTL增加随机化,调整淘汰策略,给热点key做多级缓存兜底

这张表覆盖了我在项目里遇到过的绝大多数Redis内存淘汰问题。如果你遇到的现象不在里面,也基本逃不出这几个原因的组合,按上面思路排查总会有收获。

4.4 面试追问加分点:从LRU到LFU到业务选型

如果你能在面试里主动讲清楚“为什么Redis不用标准LRU”以及“近似LRU怎么用5个样本做到接近真实LRU的效果”,这道题的层次就已经超过大多数候选人了。

关于LRU和LFU的业务选型,我个人的判断标准是这样的:如果业务对象的热度变化非常剧烈,今天爆火的明天就降温,用LRU更合理;如果业务的访问热度相对稳定,存在一批长期高频访问的核心数据,用LFU更能保护这些热点。最典型的场景是内容推荐系统,头部内容访问量长期碾压长尾内容,LFU明显比LRU更合适。

还有一个容易被忽略的加分点:Redis的淘汰分为“主动淘汰”和“被动过期”两个完全不同的机制。被动过期靠的是惰性删除加定期删除,处理的是已经过了TTL的key;主动淘汰则是内存达到上限后对maxmemory-policy的执行。这两个机制并发存在、互不替代,能把这个区别讲清楚的候选人,说明对Redis底层确实有系统性的理解,而不只是背了命令。

各策略的选择要点也可以补充一句:当业务key都带TTL时用volatile-lru能保护长命key,当内存上限是该实例的唯一硬性约束时用allkeys-lru更彻底,热点非常集中时升级到LFU,完全无规律访问就random兜底。没有绝对最优的策略,只有看业务特征匹配的策略。

4.5 一套可直接参考的Redis内存治理配置建议

最后给一套我在生产环境用下来比较稳的基线配置,不一定适合所有场景,但可以作为起点参考:

# 设置最大内存,建议为物理内存的70%左右 maxmemory 8gb # 淘汰策略:如果业务key大多带TTL就用volatile-lru # 如果key不带TTL很多,就改成allkeys-lru maxmemory-policy allkeys-lru # 采样数调大一点,淘汰会更精准,但CPU占用略高 maxmemory-samples 10 # 开启内存碎片整理(4.0以上版本) activedefrag yes

需要强调的是maxmemory不是越大越好,尤其要留足操作系统和其他进程的内存余量。如果Redis和数据库部署在同一台机器上,Redis把内存吃得太狠,数据库的页缓存就不够了,反而拖慢主业务。这就是为什么我建议先按物理内存的70%基线设置,再结合监控微调。

在实际操作中我发现,很多线上事故并不是Redis没配maxmemory,而是配置了之后没人持续关注used_memory的走势,直到内存打满才后知后觉。建议加一个针对used_memory的监控告警,超过maxmemory的80%就预警,给运维留出足够的人工干预时间。

写在最后的一点体会

从手写LRU到Redis内存淘汰,这条链路表面上是几个数据结构和配置参数,实际上贯穿了计算机系统设计里最常见的取舍哲学:标准解追求绝对正确,工程解追求足够好用。标准LRU准确但昂贵,Redis选择用采样加淘汰池换来几乎一样的淘汰效果和低得多的成本,只用了5个默认样本就解决了大多数场景下的缓存淘汰问题。我在项目里亲手配置过allkeys-lru,也处理过evicted_keys暴涨引发的缓存命中率暴跌,每次排查到最后都会感叹这些看似简单的机制背后环环相扣的设计思路。如果你能耐心把这道题从代码啃到配置再到线上排查,你收获的绝不仅仅是应付一道面试题的能力,而是一套分析缓存问题的底层思维模型。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 1:21:37

财务人Excel提效指南:从复制粘贴失灵到自动化处理

很多财务忙了一年&#xff0c;说白了就是给Excel打工。这话听着扎心&#xff0c;但确实是很多财务人的真实状态。年底结账、月度报表、预算分析、费用复核&#xff0c;哪一项不是泡在表格里完成的&#xff1f;我见过太多同行&#xff0c;每天打开Excel的时间比打开聊天软件都长…

作者头像 李华
网站建设 2026/9/16 1:20:17

Embedding层原理与实战:从Word2Vec到多模态向量表示

1. 嵌入层到底在干什么&#xff1a;从一个直觉问题讲起做 NLP 或者推荐系统的人&#xff0c;迟早会撞上 embedding 这个词。我刚入行那年第一次接触它&#xff0c;是在 Word2Vec 刚火起来的时候。当时导师甩给我一句“把词变成向量”&#xff0c;我对着代码看了半天也没想明白&…

作者头像 李华
网站建设 2026/9/16 1:19:53

Simulink通信系统建模与仿真:信道参数配置与源码实践指南

简介&#xff1a;MATLAB Simulink通信系统建模与仿真源码包是一份面向通信工程专业学生、科研人员及初学者的实操资料&#xff0c;聚焦信道建模与仿真中的核心环节。压缩包内共22个文件&#xff0c;包括12个.m脚本、2个.slx仿真模型、2个.slxc缓存文件以及.mat数据文件、xml配置…

作者头像 李华
网站建设 2026/9/16 1:19:29

C#调用ONNX Runtime实现YOLOv8红绿灯检测详解

简介&#xff1a;一份面向 C# 开发者的 YOLOv8 红绿灯检测源码&#xff0c;基于 ONNX Runtime 与 OpenCVSharp 实现模型推理&#xff0c;适合有一定 C# 基础、希望将深度学习模型部署到 .NET 桌面应用中的开发者。压缩包共 65 个文件&#xff0c;约 199.61MB&#xff0c;主要包…

作者头像 李华
网站建设 2026/9/16 1:19:00

CKEditor5实战:视频引入、预览链路与自定义工具栏全解析

最近在做一套内容管理后台&#xff0c;编辑器这块从零选型&#xff0c;第一反应就是上 CKEditor5。原因也很直白&#xff1a;项目里需要一个能自由插入视频、能在发布前直观预览效果、还能按不同角色收起或者扩展工具栏的富文本方案。CKEditor5 在这一轮对比中确实最合适——内…

作者头像 李华
网站建设 2026/9/16 1:18:42

LaTeX双栏模板跨栏图表排版详解:dblfloatfix实战指南

写毕业论文的那段日子&#xff0c;我几乎被LaTeX双栏模板里的跨栏图表折磨疯了。明明单栏插图都好好的&#xff0c;一到figure*这种跨栏环境&#xff0c;图表就开始“不听话”——有的跑到文章最后一页&#xff0c;有的直接消失&#xff0c;有的风格怪异地在页面顶部孤零零地待…

作者头像 李华