Redis这14道面试题,面试官最爱问,第3题90%的人答不准确
有个小伙伴去面试,回来跟我吐槽:
"面试官问Redis的String类型,用SDS存,到底比C字符串好在哪?"
我说:"这题简单啊,SDS获取长度是O(1),C字符串是O(n)。"
他说:"是啊,我答上来了。但他又问:Redis的Hash,在数据量大了以后,是用跳表还是B+树?"
我说:"……"
他回我:"我脑子嗡的一下。只记得资料上写的是'渐进式rehash',但具体数据结构……然后就没有然后了。"
后来我帮他复盘了一下,Redis面试有个规律:表面考八股文,实际上考你对底层原理的理解深度。
今天把他提供的Redis高频面试题系统梳理一遍,每道题都是大厂真题。
一、Redis为什么这么快?这个问题我背了3遍,但面试官想要的不是这个答案
我去阿里面试,第一道题就是"Redis为什么快"。
我当时按照资料背了三点:
基于内存
单线程避免上下文切换
I/O多路复用
面试官听完点点头,然后问了一句: "单线程避免上下文切换,那为什么后来又加了I/O多线程?"
我直接卡住。
实际上Redis 6.0引入的多线程只针对I/O层面,命令执行还是单线程。这就解释了为什么"快"——主线程专心执行命令,不用管网络I/O的读写。
// Redis 6.0前 client → command → execute → response // Redis 6.0后(I/O多线程) client → I/O线程读取命令 → 主线程执行 → I/O线程回写 ↑ ↓ 8个I/O线程并发 单线程执行面试官追问:"那什么场景下,多线程反而会更慢?"
答案是:命令本身很重的场景。比如SMEMBERS会遍历整个集合,或者执行Lua脚本。如果命令本身耗CPU,多线程反而有线程竞争开销。
二、String能存多大?512MB?不对,有个陷阱我踩过
有个面试题问:"String类型最大能存多少字节?"
我答:"512MB。"
面试官笑了笑,又问:"那你说说,SET操作时,SDS的alloc是怎么分配的?"
这道题我没答好,回去翻了半天源码,才搞清楚里面的门道。
Redis的SDS不是上来就给你512MB,而是预分配的:
如果 len < 1MB,每次扩展翻倍,比如 len=60KB,alloc就给120KB
如果 len >= 1MB,每次扩展加1MB,比如 len=2MB,alloc就给3MB
// SDS分配策略伪代码 if (newLen < 1024 * 1024) { newAlloc = newLen * 2; // 翻倍 } else { newAlloc = newLen + 1024 * 1024; // 加1MB }好处是:减少内存分配次数。但问题在于,如果你SET一个超大的值,比如一次性SET 400MB,整个alloc会直接分配400MB+,内存可能被打爆。
实际生产中,String超过10MB就要警惕,优先考虑压缩或拆分。
三、过期删除:别再只说"惰性删除"了,面试官想听的是这个
"Redis的过期键怎么删除?"——这个问题我被问了两次。
第一次我答:"惰性删除,访问时检查过期。"
面试官:"没了?"
我:"……没了。"
第二次我学乖了,又加了"定期删除,每100ms随机扫描"。
面试官点点头,然后问了一句: "那如果定期删除时,正好有个大key在过期,Redis会阻塞吗?"
答案是:不会。Redis 4.0后,大key过期会用异步方式删除,不阻塞主线程。用UNLINK替代DEL也是同样的道理。
所以正确的说法应该是:惰性删除(被动)+ 定期删除(主动)+ 异步删除(大key),三位一体。
四、面试官问我Redis和Memcached的区别,我答了6个点,面试官却说他只要一个
面试时我列举了Redis的6个优势:支持多种数据结构、支持持久化、支持集群……
面试官打断我:"你就说一点,为什么生产环境基本都用Redis而不是Memcached?"
我想了想,答不上来。
后来我才明白,核心就一句话:Redis是"多功能瑞士军刀",Memcached只是"单功能螺丝刀"。
Memcached唯一比Redis好的地方是:用 libevent 事件驱动,纯网络模型性能高一点。但这一点的优势,在Redis的SDS和Pipeline面前几乎可以忽略。
场景 | Memcached | Redis |
|---|---|---|
纯缓存,kv简单 | ✅ 可以 | ✅ 也可以 |
需要List/Set操作 | ❌ 不支持 | ✅ 支持 |
需要持久化 | ❌ 不支持 | ✅ 支持 |
需要集群分片 | ❌ 不支持 | ✅ Cluster |
需要事务 | ❌ 不支持 | ✅ MULTI/EXEC |
但如果你说:"面试官,我们项目里用Memcached做页面缓存,因为它性能更高。"——这也没毛病,够用就行。
五、内存淘汰策略:我之前一直用LRU,直到被面试官怼了
生产环境配置Redis,我之前的做法是:
maxmemory 3gb maxmemory-policy allkeys-lru简单粗暴,所有key按最近访问时间排序,淘汰最久没被访问的。
面试官问我:"你这个策略,在什么场景下会出问题?"
我想了想,没想出来。
他说:如果你的数据有明显热点,比如80%的请求集中在20%的key上,LRU没问题。但如果请求是随机的,LRU就会误杀很多刚放入但马上要用的大key。
后来我才知道,Redis 4.0引入了LFU(Least Frequently Used),按访问频率淘汰,而不是访问时间。
# LFU配置:统计访问次数,而不是最近访问时间 maxmemory-policy allkeys-lfu实际选型建议:
有明显热点:LRU
请求分散,热点不明显:LFU
需要保证某些key不被删除:Redis 6.2+ 的
volatile-lfu配合TTL
六、Redis的Hash查询很慢?不对,是你没用对
有个面试题让我当场出丑:
"Redis的Hash,如果field数量超过多少,查询会变慢?"
我答不上来,回去查了文档才发现:Hash类型在field少时用ziplist(压缩列表),field多了会转成hashtable。
// 转换条件(redis.conf) hash-max-ziplist-entries 512 // field超过512个,转为hashtable hash-max-ziplist-value 64 // value超过64字节,转为hashtablehashtable的查询是O(1),但存储开销比ziplist大。所以对于像用户信息这类"field数量固定但value小"的场景,ziplist更省内存;对于"field数量动态增长"的场景,hashtable更稳。
还有一个坑:使用HGET而不是HGETALL。之前我有个同事写代码用了HGETALL遍历一个10万field的Hash,结果把Redis给阻塞了。
七、RDB和AOF同时开,数据会不会丢失?我之前理解错了
"Redis持久化怎么配?"
这个问题我之前一直答得模棱两可,直到有一次线上事故。
当时用的是RDB+AOF同时开,理论上应该是"最多丢1秒数据"。结果有一次机器重启后,AOF文件损坏,应用起不来,我慌得不行。
后来排查发现:AOF的rewrite过程中,如果机器突然宕机,新的AOF文件可能是截断的。
Redis 7.0后引入了AOF+混合持久化来解决这个问题:
aof-use-rdb-preamble yes # RDB内容+AOF增量生产环境推荐配置:
appendonly yes appendfsync everysec aof-use-rdb-preamble yes这个配置的意思是:每秒刷盘一次,用RDB做基础快照,AOF记录增量操作。兼顾恢复速度和可靠性。
八、面试官问我"Redis集群为什么是16384个槽",我赌了5秒钟,答错了
这个问题我之前见过,但没仔细研究,面试时瞎猜了个答案。
面试官:"Redis Cluster为什么是16384个槽,而不是65536个?"
我说:"因为16384是2的14次方,方便路由计算?"
面试官笑了笑:"不是。"
后来我仔细研究过,16384这个数字是作者拍脑袋定的,但有几点合理性:
心跳包大小:节点间每s互发心跳,携带所有槽位信息。16384个槽,用2KB就能传输。如果用65536个槽,心跳包会大很多,占带宽。
够用:假设一个集群有1000个节点,每个节点平均分16384/1000 ≈ 16个槽,完全够用。
历史原因:作者Antirez在2014年的博客里解释过,最开始选了16384,后来发现够用就一直没改。
这道题其实没有标准答案,面试官想看的是你"知其然也知其所以然"的态度。
九、分布式锁:我之前写的是错的,差点让公司损失100万
分布式锁这个问题,我之前写过一版代码:
if (redis.setnx("lock", "1")) { // 加锁成功 redis.expire("lock", 30); // 业务逻辑 redis.del("lock"); }这段代码有巨大的安全隐患:setnx和expire不是原子操作,如果加锁后程序崩溃,锁就永远不会过期。
后来我改成了:
String lockValue = UUID.randomUUID().toString(); redis.set("lock", lockValue, "NX", "EX", 30); // 释放锁时校验owner String v = redis.get("lock"); if (v.equals(lockValue)) { redis.del("lock"); }但这样还是有问题:get和del之间不是原子的。
正确的做法是用Lua脚本保证原子性:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end生产环境建议直接用Redisson,它内置了:
看门狗自动续期
公平锁/读写锁
等待队列
十、大厂追问:数据一致性怎么保证?我之前答了3种方案,面试官只认可一种
"Redis和MySQL的一致性怎么保证?"
我当时答了三种方案:Cache Aside、Read Through、Write Behind。
面试官问:"你们项目用哪个?"
我说:"Cache Aside,读先缓存写先DB。"
他追问:"那为什么不用Write Behind?听起来更先进。"
我想了想,说:"Write Behind会有数据丢失风险。"
他点点头:"对,你们项目能用Redis容忍数据丢失吗?"
我明白了。Cache Aside是最普适的方案,但如果业务要求"绝对不能丢数据",那Redis只能做缓冲层,真正的数据必须直接写DB。
实际生产中的一致性策略:
// 读:先缓存,后DB User user = redis.get("user:1"); if (user == null) { user = mysql.query("select * from user where id=1"); redis.set("user:1", user, 300); } // 写:先DB,后删缓存(不是更新!) mysql.execute("update user set name='fox' where id=1"); redis.del("user:1");为什么是先DB后删缓存而不是先删缓存后DB?因为如果先删缓存,数据库还没更新完,这时有个请求读到旧缓存就出事了。
十一、热key问题:我之前以为是Redis的Bug,后来发现是我代码写得烂
有一次双十一,Redis突然报警说某节点CPU 100%,排查发现是一个商品详情的key被访问了几十万次。
当时我怀疑是Redis出了Bug,后来才明白——这是热key问题,一个key的访问量远超过其他key。
热key的解决方案:
本地缓存:用Guava Cache或Caffeine把热点key缓存在应用进程
LoadingCache<String, Object> localCache = Caffeine.newBuilder() .maximumSize(10000) .expireAfterWrite(10, TimeUnit.SECONDS) .build();热点key打散:key后面加随机数,让它分散到不同节点
String key = "product:sku:" + skuId + ":" + RandomUtils.nextInt(4);多级缓存:本地缓存 → Redis → MySQL
十二、大key问题:这个问题差点让我被开除
生产环境遇到过一次:一个Hash存了几十万个field,每次HGETALL都要几秒,Redis主线程被阻塞,所有请求都在排队。
后来我才知道:单String超过10MB,或Hash/List/Set元素超过1万个,就算大key。
大key的危害:
查询慢,阻塞主线程
删除时Redis卡顿(4.0前)
集群迁移时槽迁移卡住
解决方案:
# 用redis-cli分析大key redis-cli --bigkeys # 删除用UNLINK代替DEL(非阻塞) redis.unlink("big:key")对于已经存成大key的Hash,可以改成field:hashId:fieldValue的方式拆分成多个小key:
原来:user:1000 → {name, age, phone, email...} (10万个field) 拆后:user:1000:name → value user:1000:age → value user:1000:phone → value十三、Redis 7.0的新特性:我之前不知道,面试直接被降级
Redis 7.0出来后,我面字节的时候被问了:
"Redis 7.0新增了哪些特性?知道ACL v2吗?知道函数(Functions)吗?"
我一脸懵,只答出来一个"多租户隔离"。
Redis 7.0的几个重点升级:
ACL v2:更细粒度的权限控制,支持命令前缀、key模式、通道权限
Functions:替代Lua脚本的持久化函数,写入AOF后自动复制
**shardpubsub:集群内跨节点发布订阅
新增16个新命令:包括 CLUSTER SHARDS、WAIT 等
面试问这个,不是要你死记硬背,而是看你有没有持续关注Redis演进。
十四、Rehash为什么要渐进式?这个问题我答了5分钟
"Redis的Rehash为什么要渐进式?"
这个问题我之前看过答案,但理解不深,答了3分钟就开始胡说八道。
核心原因是:如果一次性完成Rehash,庞大的数据迁移会阻塞主线程。
Redis通过两个hashtable(ht[0]和ht[1])实现渐进式rehash:
ht[1]分配新空间
每次增删改查时,顺带迁移1个bucket
读写都查两个表,最终合并到ht[1]
// 渐进式rehash示意 dictEntry* dictAddRaw(dict* d, void* key) { if (d->ht[0].used >= d->ht[0].size) { // 如果正在进行rehash,迁移一个bucket if (d->rehashidx >= 0) { dictRehashStep(d); } } // 正常添加逻辑 }如果rehash进行到一半,Redis进程崩溃了怎么办?重启后ht[0]和ht[1]的数据会合并,不会丢,但可能会短暂出现同一个key在两个表里查到不同值的情况(实际不会,因为rehash期间写操作只写ht[1])。
面试标准答案(不是让你背,是让你理解后用自己的话答)
问题:Redis为什么快?
"Redis快的原因有两点:一是命令执行是单线程,避免了上下文切换和锁竞争;二是I/O多路复用,一个线程能处理大量并发连接。6.0后I/O层有多线程加速,但命令执行还是单线程,这是保证原子性的关键。"
问题:分布式锁怎么写?
"我之前用setnx+expire,但这两个不是原子操作,可能加锁后进程崩溃导致死锁。后来改成了SET key value NX EX,然后释放时用Lua脚本校验value是否是自己的。生产环境建议用Redisson框架,它有看门狗续期和等待队列。"
问题:Redis和MySQL一致性?
"我们用的是Cache Aside,读先缓存后DB,写先DB后删缓存。注意一定是删缓存而不是更新缓存,否则会有并发问题导致数据不一致。"
写在最后
面试Redis,核心考的是两点:
原理深度:SDS、Rehash、持久化机制——这些要理解,不能只背结论
生产经验:热key、大key、分布式锁、一致性——这些是实际踩过的坑
"背八股文"型选手,面试官一追问就露馅。每学一个知识点,都强迫自己在代码里实现一遍,才算真正掌握。