ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Redis高频面试题实战解析:从数据类型到分布式锁

Redis高频面试题实战解析:从数据类型到分布式锁 1. 面试官问Redis其实不是考八股先讲个真实的场景。我上个月面试一位三年经验的候选人前面聊项目聊得不错问到Redis时我随口追了一句项目里缓存穿透你是怎么处理的他说用了布隆过滤器。我接着问加了布隆过滤器之后如果有人故意打一个不存在但被过滤器误判为存在的Key这时候你的缓存和数据库会怎样他愣了几秒开始背布隆过滤器的原理但始终没回答“误判率带来的流量会继续穿透到DB”这个问题。这就是Redis高频面试最典型的现象答得出概念接不住追问。Redis这个组件几乎每个后端项目都在用面试官问它目的也不是让你背文档而是想确认三件事第一你日常是不是真的在用而不是Demo里跑一下第二你遇到问题时的排查路径是怎样的第三你在业务和中间件之间做取舍时有没有自己的判断。这篇文章我按高频考点拆开讲数据类型底层、持久化取舍、缓存三大问题、分布式锁、哨兵集群、以及一个我经常在群里被问到的RedisTemplate报错。面向的读者是准备跳槽面试的同学以及项目中已经在用Redis但总觉得差点意思的兄弟。内容偏实战不是纯八股每块我会把面试官连环追问的方向也点出来。2. 五大类型的底层设计别等到被问“SDS”才卡壳2.1 字符串SDS为什么是Redis的基石面试必问第一题Redis的String类型底层是什么你如果只答“动态字符串”那基本等于没答。面试官想听到的是SDSSimple Dynamic String对C字符串做了哪些改进。C语言里用char[]表示字符串以\0结尾问题很明显第一获取长度要遍历O(n)复杂度第二追加字符串需要手动管理内存容易越界第三二进制不安全字符串里一旦包含\0就会被截断。Redis里存的可能是一张图片的二进制内容或者一个序列化后的对象里面什么字节都有所以必须有一套长度已知、二进制安全的字符串结构。SDS的结构大致是len已用长度、alloc分配容量、flags类型标记、buf[]字节数组。有了len获取长度就是O(1)追加时如果alloc不够会触发扩容避免每次操作都做内存分配判断结束不依赖\0所以二进制安全。另外还有一个细节SDS在首部记录了长度这跟后面讲的quicklist节点思路也是一脉相承的。如果面试官继续追问内存碎片或者扩容策略你可以补一句Redis对SDS做了分级根据字符串长度选择不同的header类型sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64目的就是省内存。这个点能答出来说明你真看过源码。2.2 列表和哈希ziplist到quicklist的演进逻辑再一个高频题Redis的List底层是什么结构老版本的答案是ziplist或linkedlist新版本是quicklist。很多人只记了结论没搞懂为什么。ziplist是一块连续内存每个entry紧凑排列省内存、Cache友好但插入和删除需要移动后续元素元素多了性能下降。linkedlist是双向链表插入删除快但每个节点有prev/next指针内存开销大而且节点分散Cache不友好。quicklist把两者结合宏观上是双向链表每个节点内部是一个ziplist。这样既控制了内存碎片又保证中间插入删除不用大面积搬移数据。Redis 7.0之后又把ziplist换成了listpack原因是ziplist存在级联更新的问题——前一个entry长度变化可能导致后续entry的prevlen字段连锁扩容最坏情况是O(n²)。listpack每个entry自带长度从设计上消除了级联更新。哈希类型同样适用这个逻辑entry数量少、value长度短时用listpack超过阈值hash-max-listpack-entries默认128hash-max-listpack-value默认64后转为hashtable。面试官如果问“哪些命令会让编码转换”你直接说当哈希元素个数超过128或者某个value超过64字节就会从listpack转成hashtable这个转换是单向的。能说出“单向”两个字面试官会高看你一眼。2.3 有序集合跳表与哈希的组合ZSet的原理也是必考。很多人知道ZSet用跳表但不知道它其实是“哈希表跳表”的组合哈希表保存member到score的映射保证按member查score是O(1)跳表按score排序支持范围查询。追问点通常有两个为什么用跳表而不是平衡树红黑树跳表的层数是怎样决定的我的回答思路是跳表实现简单区间遍历方便而且可以通过调整概率参数控制层高和内存红黑树在内存占用上更紧凑但实现复杂度高区间查询需要中序遍历跳表在工程上更划算。层数方面Redis默认ZSKIPLIST_P 0.25也就是每个节点有25%的概率往上加一层最大层数32。再深一层面试官可能问ZSet的查询一个member的排名ZRANK为什么也是O(log n)因为跳表节点里存了一个span字段记录了当前节点到下一个节点的跨度通过累加span就能算出排名不需要遍历整条链。这个细节能答出来属于加分项。2.4 别忽略的命令行验证我在面试别人时最反感“我背过原理但没跑过命令”。其实数据类型这块你完全可以用本地Redis验证一遍比如redis-cli SET name hello OBJECT ENCODING name # int 或 embstr 或 raw LPUSH list a b c OBJECT ENCODING list # quicklist7.x 版本 HSET hash f1 v1 OBJECT ENCODING hash # listpack 或 hashtableOBJECT ENCODING这个命令能直接看到Key当前的编码方式面试时可以拿真实数据说话。这种“我跑过、我见过”的素材比背十遍博客都管用。3. 持久化宕机之后你到底丢了多少数据3.1 RDB快照fork可能比你想象的更影响性能面试题里关于持久化的问法通常是RDB和AOF有什么区别RDB有什么缺点很多人只答“RDB是快照、AOF是日志”然后就没了。但面试官真正想确认的是你有没有在线上遇到过RDB生成导致的卡顿。RDB生成有两种方式SAVE同步阻塞和BGSAVEfork子进程后台生成。线上肯定用BGSAVE但BGSAVE的fork过程会阻塞主线程fork耗时跟内存大小、操作系统配置有关。如果实例占内存20GBfork耗时有可能是几百毫秒甚至更久期间所有请求都会被卡住。所以真正的考点在这一个4GB内存的Redis实例你预计fork需要多久怎么调优经验数据是在物理机上fork一个10GB的Redis主进程大约需要几十到一百毫秒具体取决于页面数量。调优方向包括开启vm.overcommit_memory1避免fork时内存不足避免在业务高峰期触发BGSAVE把save参数改成低峰期执行或者直接关掉RDB只用AOF 定期备份。3.2 AOF日志三种刷盘策略你选哪个AOF的刷盘参数是appendfsync三个值always、everysec、no。面试官问“选哪个”正确答案不是“everysec”而是要先说清楚你的业务容忍丢多少数据。always每次写命令都刷盘最多丢一条命令但吞吐量下降明显everysec每秒钟刷一次盘最多丢一秒内的写命令这也是官方默认no交给操作系统决定什么时候刷盘丢的数据量不可控一般不推荐。如果你答完“我们用的是everysec”面试官大概率会追问为什么不用always你可以说我们业务里缓存即使丢一秒数据也能接受但要求写入吞吐不受影响所以用everysec。如果你的业务是支付流水级别的写操作那AOF就不应该成为首选存储这不是刷盘策略的问题而是架构问题。3.3 AOF重写和混合持久化AOF还有一个必考分支文件膨胀怎么办答案是AOF rewrite。Redis会fork子进程把当前内存状态重新生成一份最小的写命令集合替换旧日志。注意重写期间的新写请求会记录在AOF缓冲区和重写缓冲区里避免丢失增量。再往下追就是Redis 4.0引入的混合持久化思路RDB快照作为AOF文件的开头后面追加增量AOF日志。这样重启时加载RDB部分速度快丢失数据范围又比纯RDB小。现在新项目里一般都推荐打开aof-use-rdb-preamble yes。3.4 重启恢复的优先级面试最后一道工序可能是如果Redis目录下同时有RDB和AOF文件重启时加载哪个答案是优先加载AOF因为AOF的数据完整性更好。这个要记死。我见过有同事改配置只开了RDBAOF文件过期没清理结果重启后发现数据不对就是因为对加载顺序理解反了。4. 缓存三大件穿透、击穿、雪崩答到第几层才算过关4.1 三者本质区别缓存穿透、缓存击穿、缓存雪崩是Redis面试命中率最高的一组题没有之一。但很多人答成“缓存没有命中”这一层就停了。先给一个对比问题场景本质典型后果缓存穿透查询一个根本不存在的KeyDB中也没有数据导致每次请求都打到DBDB压力剧增容易被恶意流量打垮缓存击穿某个热点Key过期瞬间大量请求同时发现缓存失效全部去DB查单个Key过期为导火索DB瞬时压力缓存雪崩大量Key同一时间过期大面积缓存失效请求全部穿透到DBDB被打爆甚至引发服务雪崩多说一句很多面试者会把击穿和雪崩搞混关键区分就在于“一个Key”还是“一批Key”。你用这个口径去答一下子就能跟其他人拉开差距。4.2 缓存穿透不是只有布隆过滤器一种解法缓存穿透的解决方案按从简单到复杂可以排出来缓存空值DB查询不到结果时也写一个空对象到缓存并设置一个较短的过期时间比如5到10分钟防止同一个不存在的Key反复打到DB。这个方案简单有效空Key带来的额外内存要考虑可以通过统一前缀清理。布隆过滤器启动时把所有可能存在的Key提前加载到布隆过滤器里请求进来先查过滤器过滤器说“不存在”就直接返回不再查DB。但要注意布隆过滤器有误判率误判时请求还是会走到DB而且过滤器无法删除Key标准版删除需要重建数据变更频繁的业务用起来很别扭。接口层参数校验比如ID要求是正整数负数直接拦截这种基本防护很多人会漏掉。面试官如果问布隆过滤器的误判率跟什么有关你要能说出来跟位数组长度m、哈希函数个数k、插入元素个数n有关。经验值是m/n 10、k 7左右时误判率大约1%。这个可以用公式算但能答出量级就够了。4.3 缓存击穿互斥锁和逻辑过期击穿的经典解法是两个互斥锁、逻辑过期。互斥锁的逻辑是当某个热点Key过期后不是所有线程都去重建缓存而是只让一个线程去查DB并回填缓存其他线程等待一段时间后重试。实现上可以用Redis的SET key value NX EX来抢锁。注意这里加锁的粒度要细化到业务Key不能让所有Key共用一把锁。逻辑过期是另一个思路缓存里不设置物理过期时间而是把过期时间放到value里。比如value结构是{ data: ..., expireTime: 1678888888888 }查询时发现逻辑上已过期就返回旧数据同时异步开启一个线程去刷新缓存。这种方案的好处是“读请求永远有数据返回”不会阻塞代价是数据短暂不一致。对于首页、商品详情这种容忍几秒延迟的场景很合适。面试官问“这两个方案你选哪个”不要回答“看情况”就结束要给出判断依据如果业务能接受短暂的阻塞等待用互斥锁简单可靠如果完全不能接受读请求变慢用逻辑过期。4.4 雪崩过期时间随机化只是第一步缓存雪崩的常规解法就三个层次过期时间加随机值比如基础TTL加5到10分钟的随机偏移避免大量Key同时过期使用多级缓存本地缓存Caffeine/Guava挡第一层Redis挡第二层DB兜底服务降级和限流当DB压力异常大时直接返回默认值或空数据保住核心链路。面试时我建议你补一个真实案例比如我们当时做活动页把几十个商品的缓存过期时间全部设成了同一个时间点零点一到DB查询量直接翻了8倍后来改成基础TTL random(0, 300)秒DB压力马上就下来了。这种有数据的例子比背一百个方案都有说服力。5. 分布式锁从setnx到Redisson别停留在“会加锁”阶段5.1 为什么不能只靠 SETNX分布式锁这块面试官最爱问Redis分布式锁怎么实现很多人答SETNX key value。但紧接着就会踩坑。首先SETNX本身不能设置过期时间如果忘了DEL锁就永远不释放形成死锁。正确姿势是SET key value NX EX 30一条命令同时保证“不存在才设置”和“自动过期”。其次value必须带上唯一标识比如UUID释放锁时要先比较这个标识防止删掉别人的锁。最典型的问题场景是线程A持锁执行时间超过过期时间锁自动释放线程B拿到锁此时A执行完直接DEL key把B的锁删了。解决方式就是用Lua脚本比较value再删if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end这个脚本要背下来面试手写时很加分。5.2 Redisson的看门狗机制如果面试继续深挖就会问持锁时间超过过期时间怎么办Redisson是怎么解决的答案就是Watchdog看门狗机制。Redisson加锁时默认leaseTime是30秒如果没指定leaseTime后台会有一个定时任务每10秒leaseTime/3检查锁是否还在如果还在就自动续期到30秒。业务执行完再释放锁看门狗就会被关闭。这里也有一个隐含问题看门狗在单点Redis下正常但如果Redis主节点宕机了锁还没同步到从节点其他线程就能获取到同一把锁这就是分布式锁在异步复制下的经典困境。面试官问到这里其实是在考察你对CAP的理解。5.3 Redlock真能解决主从切换问题吗Redlock红锁的思路是向多个独立的Redis节点通常是5个同时加锁只有超过半数节点加锁成功才算获取锁成功。它的目的是解决“主节点宕机锁丢失”的问题。但注意Redlock在业内争议很大2016年分布式系统专家Martin Kleppmann写过一篇著名的文章反驳它说它仍然存在时钟跳跃、GC pause等问题。我的建议是面试时不要一上来就鼓吹Redlock。你先说清楚业务场景——如果锁的保护对象是幂等性要求很高的资源容不得两个线程同时操作那Redis本身的分布式锁方案就有风险需要引入强一致性的协调服务比如ZooKeeper、etcd。如果业务允许极端情况下出现短暂并发比如秒杀场景下超卖一两个还能接受那么Redisson的普通锁足够了。能把这个权衡讲清楚面试官不会在Redlock上死磕你。5.4 业务侧还要防什么除了加锁本身业务侧的坑我总结三个锁的粒度不要太大。比如库存扣减锁整个商品Key没问题但锁整个店铺就严重影响吞吐获取锁失败后的策略。常见有自旋等待、快速失败返回、降级到本地锁。不要在锁失败后无限自旋这比不加锁还可怕锁内不要做耗时操作比如远程RPC、慢SQL。锁内操作时间超出锁过期时间会导致各种诡异问题这也是我排查线上问题最常看到的根因。6. 高可用架构主从、哨兵、集群的取舍6.1 主从复制全量同步和增量同步的分界线面试必问主从复制的流程。全量同步发生在从节点第一次连上主节点或复制积压缓冲区中数据丢失时大致链路是从节点发送PSYNC命令主节点执行BGSAVE生成RDB快照同时把生成期间的新写命令写到复制缓冲区快照发完后把增量命令继续发给从节点从节点加载RDB并执行增量命令。增量同步则依赖主节点的repl_backlog_buffer这是一个环形缓冲区。如果从节点落后的数据量超过了缓冲区的容量就会退化为全量同步。所以面试官问“如何避免频繁全量同步”答案是调大repl-backlog-size同时关注从节点的处理速度。6.2 哨兵主观下线、客观下线、leader选举哨兵模式考察的核心是故障转移流程。哨兵通过PING监控主节点的存活状态如果一个哨兵在一定时间内没有收到主节点的响应就标记为主观下线SDOWN多个哨兵都认为主节点不可用达到quorum阈值则升级为客观下线ODOWN。随后哨兵集群会选举一个leader由leader选出一个从节点执行SLAVEOF no one提升为新主节点其他从节点切换复制目标最后通知客户端更新主节点地址。这个流程里有个细节面试官喜欢套路人哨兵选leader用的是Raft协议不是Redis Cluster的选举。这个要分清。还有Sentinel本身的高可用哨兵至少部署3个节点防止哨兵自己挂掉后无法决策。这是生产环境的底线。6.3 Cluster16384个槽位为什么不能改Redis Cluster把整个Keyspace分成16384个槽位每个节点负责一部分槽位。Key通过CRC16(key) % 16384计算出槽位再根据槽位定位到节点。面试官可能会问为什么是16384不是65536这个其实网上有官方解释。我面试时遇到候选人说“不知道”其实答不精准没关系但要能说出设计考量心跳包需要携带节点的槽位信息65536个槽位的心跳包会占据更多额外空间网络开销更大而集群规模一般不会超过1000个节点16384足够用了。这是从工程效率角度做的折中。另一个Cluster的高频题是为什么MGET不能跨节点批量查询因为Key分散在不同节点上跨节点需要额外的路由聚合。解决思路有几种把相关Key放到同一个Hash Tag中比如{user:123}:profile和{user:123}:orders会落到同一个槽位或者在客户端做聚合查询。6.4 脑裂、数据倾斜、热点Key这三组都是实战场。脑裂的场景是主节点网络抖动哨兵选举了新的主节点但旧主节点还在接收写入导致网络恢复后旧主节点数据被覆盖。缓解办法是配置min-replicas-to-write最少从节点数和min-replicas-max-lag最大复制延迟让主节点在从节点失联过多时拒绝写入。数据倾斜主要是大量Key集中在少数节点。比如做排行榜时用自增ID做member前缀都一样Hash Tag如果设置不当就会集中到一个槽位。热点Key的问题更常见某个商品突发秒杀所有流量集中到一个节点。常规方案是本地缓存降级、对热点Key做多副本比如加后缀分散到多个节点、或者直接限流。但要注意多副本会带来一致性延迟问题要能接受短暂读旧数据。我个人建议像这种架构题面试时尽量结合自己项目里的真实节点数、QPS量级和数据量来答。哪怕你的集群只有3个节点也比空谈“大规模集群遇到热点”可信得多。7. 高频报错实战RedisTemplate的 increment() 报 “value is not an integer or out of range”最后分享一个我几乎每周都能在技术群里看到的报错。有人在Spring Boot里用RedisTemplate做自增计数代码如下redisTemplate.opsForValue().increment(visit:count, 1);结果抛异常ERR value is not an integer or out of range。很多人一上来怀疑是不是Redis版本问题其实九成是序列化器问题。Spring Boot里有两个常用模板RedisTemplateK, V默认使用JdkSerializationRedisSerializer把对象序列化成二进制字节StringRedisTemplate使用StringRedisSerializerKey和Value都是字符串。如果你用RedisTemplate写入数值存到Redis里的并不是数字而是JDK序列化后的二进制对象。Redis的INCR命令要求Key对应的Value必须是整数当然不认识JDK序列化的垃圾数据自然报“not an integer”。再往深挖一步有人会问为什么我用redisTemplate.opsForValue().set(k, 1)之后再increment也报错因为set进去的value是Integer对象经过Jdk序列化之后存到Redis里已经不是字符串数字了。而过期时间不会改变value的编码。真正要背的结论是increment操作只能操作String类型的Redis Valuenumerical value在存储时必须用String类型保存或者直接用StringRedisTemplate。排查路径分享给你照着走基本几分钟能定位先到Redis里执行TYPE visit:count确认Key类型是string再执行GET visit:count看返回的是不是纯数字如果返回结果是一堆\xAC\xED\x00\x05t...开头的内容那就是JDK序列化的典型特征打开代码看有没有自定义RedisTemplate的序列化器没有的话换成StringRedisTemplate试试。生产环境我一般这样统一配置序列化器避免一坑踩N次Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); GenericJackson2JsonRedisSerializer jsonSerializer new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(RedisSerializer.string()); template.setHashKeySerializer(RedisSerializer.string()); template.setValueSerializer(jsonSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; }注意Hash结构、Set结构、ZSet结构的序列化器同样要检查否则它们的increment方法也会报同样的错。另外一个我踩过的坑是即使配了Jackson序列化器value本身如果有类型信息increment依然用不了因为Redis里的INCR只认纯数字字符串。如果你需要存储对象又要计数建议把计数Key和对象Key拆开计数Key用StringRedisTemplate。这块面试也会问比如“为什么increment返回Long而不是Integer”因为Redis的数字是64位有符号整数Java的Integer不够存所以Spring返回的是Long。这些细节平时不踩坑很难想起来但面试问到了就是区分度。写在最后的一点个人建议我是从“背题党”一路过来的早年准备面试也刷过几十篇Redis面经后来真正把Redis源码和线上问题结合起来才明白大部分面试答案的“为什么”。这篇文章里举的每个问题我都建议你本地装个Redis跑一遍——不用多复杂的操作OBJECT ENCODING、INFO PERSISTENCE、DEBUG SLEEP、MONITOR几个命令就能验证你背过的结论。Redis这个中间件很特别它简单到一条命令就能跑起来又复杂到分布式场景下到处是坑。准备面试时不用贪多把这几个核心方向吃透比看一百篇面经都管用。以后遇到不会的题先别急着记答案花十分钟想清楚它背后的设计取舍下次再被问倒的概率就低很多。
返回列表