第六篇:Redis——就是个分布式共享Map

📅 2026/7/15 23:07:01
第六篇:Redis——就是个分布式共享Map
别再背Redis的八股文了你只需要知道“它就是个放在内存里的公共MapA能访问B也能访问”写在前面Redis是面试里仅次于MySQL的第二大重灾区。很多人把Redis说得很玄乎——“高性能缓存数据库”、“支持多种数据结构”、“分布式锁”……别被这些词唬住。Redis的本质就三句话就是个Map——存键值对的和HashMap没本质区别。这个Map在内存里——所以读写极快微秒级。这个Map是独立部署的——A服务能访问B服务也能访问解决了“共享数据”的问题。你把这个本质抓住了后面所有的持久化、集群、淘汰策略都是围绕着“怎么让这个共享Map不丢数据、不宕机、装得下更多数据”做的工程化保障。数据结构和高级功能消息队列、Lua脚本、分布式锁是往这个Map里塞了更丰富的value类型和一些拓展操作但核心就是个Map。面试里Redis所有的考点都围绕下面四件事展开。把它们搞明白你就拿到了Redis面试的完整地图。一、Redis为什么这么快面试必问很多人背“单线程、内存、IO多路复用”这三个词但经不起追问。你要把**“为什么这样设计”**讲清楚。1.1 为什么快四个原因① 内存存储数据在内存里读写是纳秒级。磁盘是毫秒级差了好几个数量级。这是最根本的原因。② 单线程模型核心命令执行是单线程Redis的核心命令处理是单线程的。这里有个容易被挑战的点需要说清楚Redis 6.0之后引入了多线程IO但多线程只负责网络数据的收发read/write把数据包收完交给主线程后命令的执行处理逻辑、操作数据结构依然是单线程串行执行。所以“Redis是单线程的”这个说法在面试里依然成立你可以说“核心命令处理是单线程的网络IO在6.0后部分多线程化”面试官会觉得你关注了版本演进。为什么核心命令要设计成单线程因为Redis的瓶颈从来不在CPU而在内存带宽和网络带宽。单线程避免了多线程的上下文切换开销、锁竞争、CPU缓存失效代码也更简单可靠。③ I/O多路复用epoll一个单线程的Redis能处理几万并发连接靠的就是epoll。epoll的本质是你把所有连接的文件描述符注册进去内核告诉你“哪些连接上有数据来了”Redis只处理这些活跃连接没数据的连接根本不轮询。这就好比一个服务员单线程同时管100张桌子他不是挨桌问“你要点菜吗”而是等客人举手epoll通知他再去处理。这就是非阻塞IO用一个线程处理海量连接。④ 高效的数据结构Redis的数据结构不是简单的C语言数组而是专门优化的SDS简单动态字符串存字符串用的O(1)获取长度、二进制安全、预分配内存减少扩容次数。跳表Skip ListZSet底层之一实现简单、支持范围查询、插入删除都是O(logN)。1.2 单线程的瓶颈在哪里Redis的瓶颈有两个内存大小机器的物理内存是有限的数据量超过了内存就会出问题。网络IO高并发下网卡带宽可能成为瓶颈比如一次GET取1MB的大key并发一高网卡就打满了。所以单线程的Redis完全够用它的瓶颈从来不是CPU。二、Redis的数据结构不只是String2.1 五种基本类型类型底层实现特点典型场景StringSDS二进制安全可存图片/序列化对象简单缓存、计数器、分布式锁HashListPack HashTable存对象的多个字段用户信息、购物车ListQuickList双向链表压缩列表有序可重复消息队列、最新消息列表SetIntSet HashTable无序不可重复抽奖、共同好友、标签ZSetSorted SetListPack SkipList HashTable有序不可重复每个元素有权重排行榜、延时队列2.2 深入跳表ZSet的底层之一面试官“ZSet底层为什么用跳表不用红黑树”跳表的核心优势有两个①实现简单红黑树插入删除涉及复杂旋转跳表就是多级链表索引②支持范围查询更自然ZSet的ZRANK、ZRANGE等范围操作在跳表上很容易实现红黑树要范围查询需要中序遍历。HashTable存value→score的映射保证ZSCORE是O(1)跳表按score排序保证ZRANK、ZRANGE是O(logN)。两者互补。2.3 高级类型面试加分项BitMap用String的位操作存每天的签到记录365天只需要365个bit。HyperLogLog基数统计统计UV允许一定误差内存固定12KB。GEO存经纬度支持计算距离、附近的人。2.4 底层结构在演进但面试不用追太细Redis 7.0之后Hash底层从ZipList换成了ListPack解决了ZipList的连锁更新问题ZSet的底层也引入了ListPack作为小数据量的优化。这些你只要知道“Redis一直在优化内存效率”就够了面试不会深追具体结构。三、缓存三大杀手面试绝对高频这是Redis面试里最值钱的部分。面试官就看你知不知道这些坑以及怎么填。3.1 缓存穿透查不存在的数据场景黑客用大量不存在的userId比如-1、-2、-999疯狂刷你的接口。缓存里没有数据库里也没有每次请求都绕过缓存直接打数据库数据库瞬间压力飙升。破解方案面试必答缓存空值从数据库查不到数据时也往Redis里存一个null设置短过期时间比如1分钟。这样同样的key再来时直接返回空不再打数据库。但空值也会占内存大量不存在的key涌入时空值会把内存撑爆。布隆过滤器Bloom Filter在Redis前面加一层布隆过滤器。把所有可能存在的key比如所有合法的userId提前存进布隆过滤器。请求来了先问布隆过滤器“这个userId存在吗”布隆说过滤器说“不存在”就肯定不存在直接返回说“可能存在”才去查Redis→MySQL。布隆过滤器的原理面试官会追问多个hash函数 一个bit数组。存一个key时用多个hash函数算出多个位置把这些位置都标为1。查询时同样算多个位置只要有一个位置是0说明这个key肯定不存在。如果全是1只能说“可能存在”因为有哈希碰撞。面试官追问“布隆过滤器的误判率怎么调”数组长度越大内存占越多误判率越低。Hash函数个数越多误判率越低但插入和查询越慢。一般取3-5个。3.2 缓存击穿热点key过期场景某个极其热门的数据比如微博热搜第一、双11某个爆款商品缓存在某一刻过期了。此时正好有1万个并发请求同时来查这个数据全部绕过缓存瞬间把数据库打崩。破解方案互斥锁Mutex第一个请求发现缓存为空加锁去查数据库其他请求等待。第一个查完把数据写进缓存并释放锁后续请求直接从缓存取。可以用Redis的SETNX实现分布式锁。逻辑过期不设置物理过期时间而是存两个字段value和expireTime。查的时候发现逻辑过期了异步去更新缓存当前请求直接返回旧数据保证可用性。适合对数据一致性要求不高的场景。选哪种强一致性场景用互斥锁允许短暂不一致的场景用逻辑过期性能更好。比如库存扣减用互斥锁商品详情页用逻辑过期。3.3 缓存雪崩大批key同时过期场景你把一大批缓存数据设置成了同一个过期时间比如凌晨0点全部失效。结果凌晨0点一到大量请求全部绕过缓存直达数据库数据库瞬间扛不住。破解方案过期时间加随机偏移原定1小时实际设为1小时 random(0~300秒)让它们别在同一时刻集体失效。这是最简单也最有效的方案。永不过期 后台异步更新Redis里不设过期时间后台起一个定时任务在业务低峰期提前把数据刷进缓存。这样Redis里永远有数据不会出现集体失效。多级缓存本地缓存Caffeine Redis分布式缓存。Redis挂了或数据失效了本地缓存还能顶一阵。本地缓存设置短过期时间比如5分钟Redis挂了之后本地缓存的压力可控。四、持久化——断电了怎么办Redis数据在内存里一断电全没了。持久化就是给内存数据存个备份。4.1 RDBRedis DataBase——快照原理在某个时间点把内存里的所有数据压缩成一个.rdb二进制文件存到磁盘。触发手动SAVE/BGSAVE或者配置文件里设置save 900 1900秒内至少1个key变化就触发。优缺点优点恢复速度快直接把文件加载进内存文件体积小压缩了。缺点不是实时备份如果Redis意外宕机会丢失最后一次快照之后的数据可能几分钟的数据就没了。4.2 AOFAppend Only File——日志原理每一条写命令SET、DEL等都追加到一个日志文件里。重启时把日志里的命令重新执行一遍数据就恢复了。刷盘策略appendfsync always每次写命令都刷盘最安全但最慢appendfsync everysec每秒刷一次默认最多丢1秒的数据推荐appendfsync no让操作系统决定什么时候刷最快但可能丢大量数据优缺点优点数据安全最多丢1秒的数据。缺点文件体积大存的是命令文本恢复速度慢要逐条执行命令。4.3 混合持久化Redis 4.0RDB AOF 的结合。AOF文件里先存一份RDB快照二进制再往后追加增量命令。这样恢复时先加载RDB快再重放AOF增量命令。兼顾了恢复速度和数据安全。生产环境怎么选对数据安全性要求极高金融、支付→ AOFeverysec 混合持久化。对数据丢失有一定容忍度缓存场景→ RDB就够了。普遍做法两者都开启混合持久化。RDB做冷备比如每天备份一次到云存储AOF做热备宕机恢复用。五、淘汰策略——内存满了怎么办Redis内存是有限的总有填满的一天。内存满了写操作会失败。淘汰策略就是**“满了之后扔谁”**。noeviction默认内存满了写操作报错不淘汰任何数据。最安全但会导致业务写入失败生产环境一般不这么设。allkeys-lru在所有key中淘汰最近最少使用的key。这是最常用的策略适合热点数据缓存场景。allkeys-lfu4.0在所有key中淘汰使用频率最低的key。适合访问频率差异明显的场景比如新闻热点少数文章访问量远超其他。allkeys-random随机淘汰。适合数据访问完全随机的场景很少用。volatile-lru从设置了过期时间的key中淘汰最近最少使用的。适合缓存和持久化数据混合的场景你希望有明确过期时间的缓存数据被淘汰持久化数据永远保留。volatile-lfu/volatile-random/volatile-ttl类似的变种从设置了过期时间的key里按不同规则淘汰。volatile-ttl优先淘汰剩余存活时间最短的。LRU和LFU的区别面试常问LRULeast Recently Used看最后一次访问时间淘汰最久没被访问的。LFULeast Frequently Used看访问频率淘汰访问次数最少的。场景一个key昨天被疯狂访问频率极高但今天没人访问了。LRU会淘汰它因为最近没访问LFU会保留它因为历史频率高。选哪个取决于业务临时热点用LRU长期冷热分明用LFU。六、高可用集群方案6.1 主从复制一主Master多从Slave主负责写从负责读读写分离数据从主同步到从。作用读写分离分担主库读压力、数据备份从库做备份。同步机制面试高频全量同步初次复制从库连上主库主库执行BGSAVE生成RDB快照发给从库从库加载RDB。同时主库把同步期间的增量写命令记录到replication buffer中RDB传输完成后发给从库从库再执行这些命令追平数据。增量同步断开重连从库断开后重连主库把断连期间的写命令发过去从库记录了上次同步到的偏移量slave_repl_offset主库从这个偏移量之后开始发。增量同步的粒度是命令不是整块数据。6.2 哨兵Sentinel——主挂了自动切换作用监控主从节点的健康状态主挂了自动从从库里选一个当新主并通知客户端新的主库地址。核心机制监控哨兵每秒发送PING检查节点是否存活。主观下线一个哨兵发现某个节点超时未响应标记为“主观下线”SDOWN。客观下线多个哨兵配置的quorum数量都认为该节点主观下线标记为“客观下线”ODOWN。故障转移从从节点里选一个当新主优先级、复制偏移量、runid修改其他从的复制目标通知客户端新主地址。注意哨兵本身也要高可用通常部署3个及以上奇数个防止脑裂。6.3 Cluster集群——数据分片解决了什么问题单机内存不够用。数据太大比如1TB单机装不下必须拆开放在多个机器上。原理把16384个槽位Slot分配给多个节点。每个key用CRC16算法算一个hash值对16384取模落到对应的槽位上。key - CRC16(key) % 16384 - 落到某个槽 - 该槽由某个节点负责为什么是16384官方说法16384个槽位用2KB的bitmap就能存下每个节点的位图大小是16384bit2KB传输心跳包时效率高。如果用65536个槽位图变成8KB心跳包就重了。Cluster模式下的限制多key操作比如MSET要保证所有key在同一个槽位上用hash tag强制。事务只支持同一个槽内的key。七、分布式锁——Redis怎么当锁用面试高频“你们项目里分布式锁怎么实现的”标准答案一套完整的流程① 加锁用SET key value NX PX 30000。NXkey不存在时才设置保证只有一个线程能成功PX 30000设置过期时间30秒防止死锁② 解锁用Lua脚本保证原子性。-- 先判断value是不是自己防止解了别人的锁ifredis.call(get,KEYS[1])ARGV[1]thenreturnredis.call(del,KEYS[1])elsereturn0end③ value存什么存一个唯一标识比如UUID threadId解锁时判断是不是自己加的锁防止误删别人的锁。④ Redisson的看门狗机制加分回答如果你的业务执行时间超过了锁的过期时间30秒锁自动释放了别的线程拿到了锁而你的业务还在跑就出问题了。Redisson的做法是拿到锁后启动一个看门狗Watchdog线程每隔过期时间/3秒默认10秒自动给锁续期只要业务还在跑锁就自动续期不会提前释放。⑤ RedlockRedis作者提出的更严格的分布式锁方案为了解决主从切换时锁丢失的问题主节点挂了还没来得及同步锁数据从节点变成主节点后锁就凭空消失了。Redlock要求同时向多个独立的Redis节点N个申请锁只有超过半数N/21节点加锁成功才算成功。但在实际项目中用得不多因为多数业务场景可以接受主从切换的极端情况而Redlock的复杂度和性能开销较大。八、面试官真正想听什么场景1“你们Redis在生产环境遇到过什么问题”加分回答真实案例“我们有次Redis内存飙升报警排查发现是某个缓存key的value里存了一个大JSON将近50MB被频繁访问。虽然Redis单线程处理快但大数据量的读写会阻塞其他命令导致接口RT飙升。后来我们把大JSON拆成多个小key用Hash存字段解决了大key问题。大key在Redis里是禁忌因为GET一个50MB的数据要占用大量网络带宽和序列化时间会阻塞其他请求。”场景2“怎么保证缓存和数据库的一致性”这个问题没有完美答案只有权衡。面试官想听你说出延迟双删策略“我们用的策略是更新数据库后删除缓存而不是更新缓存让下一次查询时重建缓存。为了应对并发读请求在删除缓存后、更新数据库前读到旧数据的情况我们在删除缓存后延迟几百毫秒再删一次这就是延迟双删。虽然这个策略不能做到强一致性但在最终一致性层面是可行的因为我们的业务允许毫秒级的不一致窗口。”场景3“缓存穿透你们怎么解决的”加分回答“我们用了布隆过滤器。启动时把数据库里所有有效userId加载进布隆过滤器查询前先过布隆过滤器。有1%的误判率即1%不存在的key会放行到数据库但已经能把绝大部分无效请求挡在外面了。布隆过滤器的参数数组大小和hash函数个数我们根据数据量和可接受的误判率精确计算过没有用默认配置。”场景4“Redis和本地缓存怎么配合”加分回答多级缓存架构“热点数据我们做了多级缓存Caffeine本地缓存 Redis。本地缓存命中率大概40%延迟在微秒级没命中的走Redis延迟在毫秒级Redis再没命中的才查数据库。本地缓存的过期时间比Redis短比如5分钟 vs 1小时这样即使本地缓存数据有延迟Redis里也能拿到相对新的数据。”小结核心概念大白话面试考点为什么快内存 单线程 epoll SDS单线程为什么能高并发数据类型String/Hash/List/Set/ZSetZSet底层跳表缓存穿透查不存在的数据布隆过滤器原理缓存击穿热点key过期互斥锁 vs 逻辑过期缓存雪崩大批key同时过期过期时间加随机RDB内存快照备份可能丢最后一次快照之后的数据AOF写命令日志最多丢1秒淘汰策略内存满了踢谁LRU vs LFU主从一主多从读写分离全量同步增量同步哨兵主挂了自动切换主观下线 vs 客观下线Cluster16384个槽分片为什么是16384分布式锁SETNX Lua看门狗续期记住一句话Redis就是个分布式共享Map。你把它当Map用所有的问题持久化、集群、淘汰都是工程化的附加问题而不是Redis本身的难题。下一篇我们讲消息队列MQ——RocketMQ、Kafka、RabbitMQ到底怎么选消息不丢不重怎么保证下期见。