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每次扩展翻倍比如 len60KBalloc就给120KB如果 len 1MB每次扩展加1MB比如 len2MBalloc就给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面前几乎可以忽略。场景MemcachedRedis纯缓存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引入了LFULeast 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持久化怎么配这个问题我之前一直答得模棱两可直到有一次线上事故。当时用的是RDBAOF同时开理论上应该是最多丢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 id1); redis.set(user:1, user, 300); } // 写先DB后删缓存不是更新 mysql.execute(update user set namefox where id1); redis.del(user:1);为什么是先DB后删缓存而不是先删缓存后DB因为如果先删缓存数据库还没更新完这时有个请求读到旧缓存就出事了。十一、热key问题我之前以为是Redis的Bug后来发现是我代码写得烂有一次双十一Redis突然报警说某节点CPU 100%排查发现是一个商品详情的key被访问了几十万次。当时我怀疑是Redis出了Bug后来才明白——这是热key问题一个key的访问量远超过其他key。热key的解决方案本地缓存用Guava Cache或Caffeine把热点key缓存在应用进程LoadingCacheString, 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通过两个hashtableht[0]和ht[1]实现渐进式rehashht[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层有多线程加速但命令执行还是单线程这是保证原子性的关键。问题分布式锁怎么写我之前用setnxexpire但这两个不是原子操作可能加锁后进程崩溃导致死锁。后来改成了SET key value NX EX然后释放时用Lua脚本校验value是否是自己的。生产环境建议用Redisson框架它有看门狗续期和等待队列。问题Redis和MySQL一致性我们用的是Cache Aside读先缓存后DB写先DB后删缓存。注意一定是删缓存而不是更新缓存否则会有并发问题导致数据不一致。写在最后面试Redis核心考的是两点原理深度SDS、Rehash、持久化机制——这些要理解不能只背结论生产经验热key、大key、分布式锁、一致性——这些是实际踩过的坑背八股文型选手面试官一追问就露馅。每学一个知识点都强迫自己在代码里实现一遍才算真正掌握。