ARTICLE DETAIL

资讯详情

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

Redis十大经典面试题详解:缓存穿透、分布式锁与持久化核心原理

Redis十大经典面试题详解:缓存穿透、分布式锁与持久化核心原理 不用纠结“7天”是不是夸大这类标题能火本质是大家真正需要的是“面试前快速过一遍Redis核心考点”。我在带团队面试和帮朋友突击面试时发现大部分人不是不懂Redis而是懂得很散知道String能存缓存却说不清SDS和C字符串的区别知道Redis快却解释不了单线程为什么还快用过分布式锁但被问到“锁过期了业务没执行完怎么办”就卡住。这篇我会用一线开发者的视角把Redis十大经典面试题全部拆开揉碎。每一题都按“高频问法 → 核心考点 → 深度原理 → 回答思路 → 避坑提醒”来写答案里该有的源码细节、实际踩坑、场景取舍都会给到位。内容不要求你背题而是帮你建立一套能应对追问的知识骨架。适用人群准备Java/Python/Go后端面试的候选人、需要带新人梳理Redis知识的mentor、以及所有想把Redis用得更扎实的工程师。1. 缓存穿透、击穿、雪崩面试官最爱问的“三兄弟”1.1 三个概念先分清别一上来就混淆这三个问题名字像本质却完全不同。我在模拟面试时发现很多人能背出定义但一旦面试官问“你们项目里到底遇到了哪一个”就开始含糊。先给一张对比表后面逐个讲。场景查询的数据缓存状态数据库压力典型表现穿透根本不存在的数据如恶意id-1缓存和数据库都没有每次请求都直达DB缓存命中率骤降DB QPS暴涨击穿某一个热点key如秒杀商品缓存恰好过期大量请求同时打到DB单key高并发下DB瞬时崩溃雪崩大量key同时失效大批key在同一时间过期请求批量涌入DBDB压力呈阶梯式暴涨连锁故障穿透是“查无此人”击穿是“一个人扛不住”雪崩是“一群人同时倒下”。记住这个类比答题时先给定义再给场景面试官就知道你心里有数。1.2 缓存穿透的四种解法不只是布隆过滤器穿透的核心问题是“查询一个不存在的数据缓存起不到拦截作用”。最直接的方案是缓存空值如果查询DB结果为空也往缓存写一个null值并设置一个较短的过期时间比如5分钟。这里有两个细节要注意空值的过期时间不能太长否则数据真的写入后缓存不能及时更新空值要区分业务含义别把真正的异常和“查无此数据”混在一起。布隆过滤器是更优雅的方案。在请求进来之前先通过布隆过滤器判断key是否存在如果过滤器认为不存在直接拦截根本不打到缓存和DB。布隆过滤器有个特点它会说“可能有”但不会说“一定没有”。也就是说过滤器返回存在时可能误判返回不存在时一定不存在。所以它的正确用法是“拦截确定不存在的请求放行可能存在的请求”。实现上可以用Redis的bitmap自己写一个简单的布隆过滤器也可以用Redisson内置的RBloomFilter生产环境建议直接用Redisson避免重复造轮子。还有一个容易被忽略的解法参数合法性校验。比如用户查询订单id先校验id是否为正整数、是否在合理范围内。很多穿透攻击是用负数id或超长字符串这种请求在入口处直接返回“参数错误”连过滤器都不用过。这是成本最低的第一道防线建议先做。追问点如果布隆过滤器误判了导致一个真实存在的key被拦截怎么办答案是使用“白名单布隆”双通道或者不拦“可能存在”的请求只拦“一定不存在”的请求。实际项目中我通常用“缓存空值布隆”双保险空值是兜底布隆是加速拦截。1.3 缓存击穿的互斥锁与逻辑过期击穿的经典解法是互斥锁mutex key。流程当缓存过期后不是所有请求都去查DB而是先尝试获取锁。拿到锁的请求去查DB、回写缓存、释放锁没拿到锁的请求先等待一小段时间再重新读取缓存。这里一定要设置锁的过期时间不然持锁线程宕机会导致死锁。建议用set nx ex命令原子性加锁释放锁时用Lua脚本保证“检查owner删除”的原子性避免误删别人的锁。另一种思路是逻辑过期即不真正设置物理过期时间而是在缓存值里存一个逻辑过期时间戳。每次读到数据时如果发现逻辑时间已经过期不会立刻返回旧数据而是先返回旧数据同时异步起一个线程去更新缓存。这种方案的好处是“永远有旧数据可用”不会出现缓存空窗期坏处是数据一致性变弱读到的可能是几秒前的旧数据。适合对一致性要求不高的热点数据比如商品详情标题、配置信息。我在做秒杀系统时用的是“逻辑过期异步续更”的方案。因为秒杀开始瞬间一个SKU的key可能被上万人读取如果让所有线程去等待锁更新缓存体验会很差。异步更新能保证绝大多数请求瞬间拿到旧库存数据只有少数线程去做DB查询。当然这里要控制好异步线程的并发度别把更新队列打爆。1.4 缓存雪崩的批量过期拆解雪崩有两种诱因一种是大量key设置了相同的过期时间比如凌晨零点统一过期另一种是Redis实例本身宕机导致所有缓存失效。针对第一种解决办法是给过期时间加随机扰动比如固定过期时间加上一个1~5分钟的随机值让过期时间散开。针对第二种就要做Redis高可用部署比如主从哨兵或Cluster集群同时做本地缓存兜底如Caffeine把请求在应用层拦住一部分。其实雪崩和击穿很容易被搞混。我习惯这样给面试官讲击穿是“一个热点key过期瞬间崩溃”雪崩是“一批key同时过期或Redis整体挂了”。如果有人问“怎么解决雪崩”先从过期时间随机化讲到多级缓存再讲Redis高可用最后讲到降级熔断。回答要有层次感别一句话说完。2. Redis为什么快单线程模型与多路复用2.1 单线程为什么还能扛高并发这是Redis面试中最基础但也最容易被问深的问题。要回答清楚得从三个层面拆内存存储、单线程模型、I/O多路复用。内存存储是前提。Redis把数据存在内存里读写操作基本是内存级别的延迟通常微秒级。相比磁盘IO的毫秒级差距是几个数量级。这一点面试官不会深挖但你要主动提。单线程模型是核心。Redis命令执行是单线程的所以不存在多线程竞争锁、上下文切换、CPU调度开销。但要注意“单线程”指的不是整个Redis进程只有一个线程而是“处理命令的线程只有一个”。Redis 6.0之后引入了多线程IO用来处理网络读写但命令执行仍然是单线程。这里容易被面试官追问“既然命令执行是单线程的那为什么还快”答案是因为Redis的数据结构都是精心设计的操作复杂度普遍是O(1)或O(log N)单线程足以应对每秒几十万级别的QPS。I/O多路复用是救命的。Redis用epollLinux或kqueueBSD监听大量socket连接当某个socket可读或可写时内核才通知Redis处理。这样Redis能同时管理成千上万个连接而不用每个连接占一个线程。类比来说餐馆只有一个服务员单线程命令执行但他在门口装了按钮哪桌客人按铃他就过去服务不会干等某一桌。这个类比面试官很吃透。2.2 为什么Redis不采用多线程6.0后的变化如果面试官继续追问“为什么不用多线程”你就回答因为Redis的性能瓶颈不在CPU而在网络IO和内存。早期版本用单线程已经能跑到10万QPS加锁带来的复杂性和不确定性反而会降低稳定性。Redis的作者antirez在多个场合说过单线程模型让Redis的实现和调试变得极其简单而且bitmap、HyperLogLog等数据结构可以安全地做原子操作。6.0引入多线程IO后主线程仍然负责命令执行但网络读写可以由额外的线程并行处理。也就是说多线程只负责“搬运数据”不负责“执行命令”。这样做的好处是能更快地处理大量小包的读写请求减少主线程的网络等待时间。但如果你只是做常规缓存单线程IO其实也够用。这里有个常见误区有人认为Redis 6.0多线程就可以提升所有场景性能。实际上多线程IO只对“网络开销占比大”的场景有效比如大量小型key的GET/SET。对于keys命令、大key扫描这类耗时操作多线程IO帮不了你因为瓶颈在命令执行本身。面试里能区分这一点说明你思考过。2.3 哪些命令会让Redis变慢——面试官挖坑点面试官经常问“既然单线程快为什么你的Redis会变慢”这就是挖坑点答案是你用了慢速命令。常见慢命令包括KEYS *遍历所有keyO(N)N大时直接卡死。应该用SCAN命令分批遍历。SMEMBERS获取集合所有成员O(N)。应该用SSCAN分批。HGETALL获取Hash所有字段O(N)。应该用HSCAN。SORT排序操作O(NM*log(M))非常吃CPU。大key操作比如对一个包含百万元素的List执行LRANGE 0 -1一次性传输大量数据。面试回答时可以这样讲“我会用SCAN系列命令替代KEYS用大key扫描工具定期检测还要避免在Redis里做复杂聚合计算。如果业务确实需要可以把大key拆分比如用Hash分片存储。” 然后主动提一下Redis 6.0的MEMORY USAGE和DEBUG OBJECT这两个排查命令能显得你有实操经验。3. Redis的数据类型不只是五种基础类型3.1 五大数据类型的底层结构与使用场景面试题常问“Redis的String、List、Hash、Set、ZSet分别适合什么场景底层结构是什么”这个问题要学会“场景→类型→底层结构”三层递进回答。String最基础底层是SDS简单动态字符串。适合缓存、计数器、分布式锁、Session共享。SDS和C字符串的区别是SDS记录了使用长度、可自动扩容、二进制安全这是很多人容易漏的细节。List底层是快速链表quicklist由ziplist压缩列表升级。适合消息队列但一般不推荐因为有更专业的Stream、最新消息列表、时间线。Hash底层是压缩列表小数据量时和哈希表大数据量时。适合存储对象信息比如用户信息、商品信息可以单独更新某个字段而不需要整体反序列化。Set底层是整数集合小数据量时和哈希表。适合去重、共同好友、随机抽奖SRANDMEMBER。ZSet底层是跳表skiplist哈希表。适合排行榜、延时队列、限流等场景。跳表是高频追问点。跳表为什么不用红黑树跳表实现简单、支持范围查询如ZRANGEBYSCORE、O(log N)查询和插入而且并发情况下更容易调试。红黑树在内存和稳定性上有优势但面试时不需要太深入讲清楚跳表“多层索引加速查找”的原理即可。3.2 Redis压缩列表与快速列表的工作原理压缩列表ziplist是Redis为节省内存设计的一种连续内存块结构它把多个元素紧凑地存放在一块连续内存中。优点是省内存、缓存友好缺点是插入和删除可能触发内存拷贝。当列表或哈希的数据量超过阈值时会自动升级为更复杂的结构。Redis 7.0用listpack替代了ziplist它是一种改进的紧凑结构解决了ziplist的连锁更新问题。连锁更新是什么意思想象一长串排在内存里的元素每个元素头部记录前一个元素的长度。如果其中一个元素变大后续所有元素的头部记录都要往前挪引发一连串的内存移动极端情况下性能会严重退化。listpack改用了不依赖前一个元素长度的编码方式彻底消除了连锁更新风险。这个细节能答出来面试官会觉得你真的跟进了Redis版本演进。3.3 底层结构转换的触发条件Redis的底层结构转换是自动的转换条件在redis.conf中有默认配置。比如Hash和List的小数据量用压缩列表当元素个数超过128或单个元素长度超过64字节时转为哈希表。ZSet同理超过阈值转为跳表。整数集合intset只适用于元素都是小整数的Set当插入字符串或大整数时自动升级为哈希表。面试时可以这样补充“我们线上Redis的Hash对象如果字段很多会预估大小提前设计分片策略避免因自动转换带来内存抖动。”这能体现出你在架构层面的思考。4. Redis持久化RDB与AOF的博弈4.1 RDB快照与AOF日志的区别持久化是每个Redis面试必考的点。RDB是定期生成内存快照保存的是二进制数据。优点文件小、恢复快、适合备份。缺点两次快照之间的数据可能丢失比如默认配置是5分钟至少1000次写入才触发一次快照如果Redis突然宕机最后一次快照后的数据就没了。AOF是记录每个写命令以追加日志的形式保存。优点数据安全性高可以做到每秒同步一次appendfsync everysec最多丢1秒数据。缺点文件体积大、重放慢。所以生产环境常用“RDB做冷备AOF做热备”在redis.conf中同时开启。4.2 AOF的三种同步策略与重写机制AOF有三种同步策略always每次写入都同步到磁盘最安全但性能损耗大、everysec每秒同步推荐、no交给操作系统决定数据丢失风险不可控。面试官问“你们公司Redis用哪种策略”时回答everysec是标准答案但要补充一句“我们结合实际业务对核心数据用always对普通缓存用everysec。”AOF重写rewrite是压缩AOF文件的关键机制。Redis会fork一个子进程读取当前内存状态生成新的AOF文件期间新到达的写命令被缓存重写完成后追加到新文件末尾。AOF重写能有效缩小文件体积比如对同一个key的100次INCR可以合并成一次SET。配置上可以设置auto-aof-rewrite-percentage和auto-aof-rewrite-min-size来触发自动重写。4.3 混合持久化与恢复顺序的坑Redis 4.0之后支持混合持久化AOF文件头部是RDB格式的快照尾部是追加的AOF命令。这样既保留了RDB的恢复速度又弥补了RDB的数据丢失问题。默认开启重启时会先加载RDB部分再重放AOF增量部分。恢复顺序有个坑Redis启动时如果同时存在RDB和AOF优先加载AOF其实不是。默认情况下如果AOF关闭加载RDB如果AOF开启加载AOF。也就是说AOF文件的可靠性优先级高于RDB。但在混合持久化下AOF本身就是“RDBAOF”的组合所以不需要纠结。实际经验我们的备份策略是每天凌晨做一次RDB备份同时开启AOF everysec并设置AOF重写阈值。这样即使RDB文件损坏也能从AOF中恢复最近1秒的数据。另外我用过一个坑——AOF文件被误删结果Redis启动失败。后来我们给AOF目录做了定期备份避免人为事故。5. Redis的过期删除与内存淘汰不是一回事5.1 过期删除策略惰性删除与定期删除Redis对过期的key有两种删除策略惰性删除访问时发现过期就删除和定期删除每隔一段时间抽取一批key检查并删除过期key。两者配合使用因为单靠惰性删除可能导致大量过期key占内存单靠定期删除又无法保证及时清除所有key。定期删除是怎么实现的Redis在serverCron函数里周期性执行默认每100ms抽查一批key如果过期了就删除。这里有个细节抽查不是全量扫描而是随机抽样所以两个问题会存在一是过期key可能没被抽查到二是如果已经过了过期时间但一直未被访问这些key只有在惰性删除触发时才会被清掉。解决办法是配合内存淘汰策略兜底。5.2 内存淘汰策略LRU与LFU的选择当内存满时Redis会根据maxmemory-policy执行淘汰策略。常见的有noeviction不淘汰直接报错。适合不允许丢失数据的场景。allkeys-lru对所有key使用LRU最近最少使用淘汰。volatile-lru只对设置了过期时间的key使用LRU淘汰。allkeys-lfu使用LFU最不经常使用淘汰。volatile-random、allkeys-random随机淘汰。volatile-ttl删除即将过期的key。面试官会问LRU和LFU的区别。LRU考虑“多久没被使用”LFU考虑“单位时间内被使用的频率”。LRU存在“一次性大量读取导致冷数据被误淘汰”的问题LFU能更好地区分冷热数据。Redis 4.0后默认策略是noeviction生产环境我建议根据业务选allkeys-lfu或volatile-lfu并且要预估内存增长趋势别等内存打满才调优。5.3 为什么Redis明明设置了过期时间内存却还在增长这是一个高频线上问题。原因可能有过期key没有被定期删除和惰性删除及时清理大key长时间未被访问惰性删除没机会触发内存碎片率太高。排查方法用INFO memory查看mem_fragmentation_ratio用SCAN遍历key并用OBJECT idletime查看空闲时间。如果发现大量过期key堆积可以手动执行scan expire遍历清理或者调高定期删除的频率hz但频率过高会增加CPU开销要注意权衡。6. Redis事务与Lua脚本原子性的真相6.1 MULTI/EXEC的原子性边界Redis事务用MULTI开启EXEC执行中间的命令用QUEUE排队。很多人以为事务内的所有命令“要么都执行要么都不执行”但其实Redis事务的原子性只限于“命令在EXEC期间连续执行不会被其他客户端的命令插入”并不保证“命令执行失败时回滚”。如果事务中第2条命令语法错误EXEC时会报错但第1条命令已经执行了不会回滚。如果命令是运行时错误比如对String执行LPUSH同样不会回滚。所以面试题会这么问“Redis事务和关系型数据库事务有什么区别”你要回答Redis事务不具备原子性严格说只是“隔离性”保证不保证回滚也没有“一致性”保证比如某个命令失败后数据可能处于中间状态。Redis作者的设计理念是用简单高效的方式保证“没有别人插队”但业务正确性需要你自己通过命令设计和WATCH来保证。6.2 WATCH命令与乐观锁WATCH可以在EXEC之前监视一个或多个key如果在EXEC之前这些key被其他客户端修改了EXEC会返回失败事务不执行。这实现了类似乐观锁的效果。典型的应用是“库存扣减”读取库存、INCR/LPUSH处理、WATCH库存key如果库存被修改则重试或放弃。实际项目中我很少用WATCH因为失败重试会带来额外的请求开销。更多情况下用Lua脚本保证“读写”的原子性比如扣减库存时直接执行一段Lua脚本在脚本内完成“检查库存是否充足→扣减→写入”三个步骤因为Lua脚本在Redis中是原子执行的不存在竞态条件。6.3 Lua脚本的妙用不只是事务替代品Lua脚本在Redis中的执行是原子的因为Redis会用单线程执行整个脚本。这意味着你不需要WATCH和事务也能保证一段逻辑的完整性。常见使用场景分布式锁的释放比较value删除、限流滑动窗口计数、秒杀扣库存判断库存减库存记录用户是否已抢过。写Lua脚本时要注意脚本中不要出现耗时操作如KEYS *因为脚本执行期间Redis不会处理其他命令脚本卡住整个Redis就卡住了。另外脚本里访问的key要尽量固定用KEYS[]参数传入避免在脚本内动态拼接key名因为这样会影响后续的集群模式迁移槽位计算。一个常见的脚本例子扣库存if redis.call(GET, KEYS[1]) 0 then return 0 end redis.call(DECR, KEYS[1]) redis.call(SADD, KEYS[2], ARGV[1]) return 1脚本返回0表示库存不足返回1表示扣减成功并记录用户。这样用Redis单命令的原子性解决了超卖问题。7. Redis分布式锁实现原理与常见坑7.1 SET NX EX最基础的分布式锁的完整写法分布式锁的经典命令是SET key value NX EX timeout。NX表示只有在key不存在时才能设置成功EX表示设置过期时间。这样一条命令就实现了“加锁”和“自动过期”两个特性。很多人在旧代码里用的是SETNX EXPIRE两条命令这会导致如果SETNX成功但EXPIRE失败锁永远不会过期这是严重bug。正确做法是SET NX EX一条命令完成。锁的value一定要设置成唯一标识比如UUID或业务流水号不能是固定值。因为释放锁时需要先GET锁的value判断是否是自己持有的锁再DEL删除。如果value是固定值线程A可能释放掉线程B的锁。释放锁这“两步走”也不是原子的需要Lua脚本包装比较value与当前值一致然后DEL。示例# 加锁 SET lock:order:1001 550e8400-e29b-41d4-a716-446655440000 NX EX 30 # 释放锁Lua脚本 if redis.call(GET, KEYS[1]) ARGV[1] then return redis.call(DEL, KEYS[1]) else return 0 end7.2 锁过期了业务没执行完怎么解决这是面试官必问的“灵魂拷问”。如果锁的过期时间设成30秒但业务执行了50秒那么第31秒时锁自动过期另一个线程获取到同一把锁并发执行数据就乱了。标准解法是“续期”给锁配置一个后台守护线程每过一段时间比如锁过期时间的三分之一判断业务是否还在执行如果还在就调用EXPIRE续期。这个机制Redisson的watch dog已经实现了默认锁的超时时间是30秒每隔10秒就会自动续期直到业务完成释放锁。如果你不想引入Redisson也可以手动实现续期在加锁时设置锁过期时间为10秒然后起一个定时任务每5秒执行一次EXPIRE业务结束后取消定时任务。但要注意定时任务和业务逻辑共享线程池别让续期线程被业务阻塞。另外续期是“雪中送炭”但不能解决“Redis主从切换导致锁丢失”的问题。7.3 RedLock分布式锁的争议与到底要不要用RedLock是Redis作者提出的分布式锁算法原理是向多个独立的Redis节点申请锁只有超过半数节点成功才算加锁成功。目的是在Redis主从故障时避免锁丢失。但RedLock在业界一直有争议Martin Kleppmann和作者辩论过多次。面试时被问“RedLock有没有问题”你需要说出关键点RedLock依赖多个节点但如果你只有3个节点且同一机房故障切换时依然可能同时宕机并不比单节点可靠太多。RedLock中的重试机制和一致性窗口可能造成“锁的有效期被延长”从而破坏互斥性。如果在业务代码里为了等锁而阻塞分布式锁的“互斥”意义不大因为性能瓶颈在DB而不是锁。我个人观点是对绝大多数业务使用Redisson提供的单节点Redis分布式锁就够了。只有在你对可用性和安全性要求都非常极端的系统中才去考虑RedLock或更专业的依赖如ZooKeeper的临时顺序节点。面试时可以说“我们用的Redisson自带看门狗续期并设置了合理的锁等待超时”然后补充“如果业务允许短暂的不安全用Redis锁是性价比最高的”。8. Redis集群主从复制、哨兵与Cluster8.1 主从复制原理全量复制与增量复制主从复制是Redis高可用的基础。一个Redis主节点可以有多个从节点从节点只读主节点写。主从之间通过SYNC旧版和PSYNC新版命令同步数据。PSYNC支持部分重同步partial resynchronization即如果断线时间不太长从节点可以只接收断开期间的增量命令而不是全量复制。全量复制过程从节点发送PSYNC主节点执行BGSAVE生成RDB快照将快照传给从节点再从缓冲区发送新增写命令。期间如果RDB传输过慢或缓冲区不足可能触发全量复制一直重试。这里有个常见坑主节点为了生成RDB会fork子进程如果内存很大fork可能耗时较长导致主节点短暂阻塞。所以不要在内存超大的Redis上频繁切换主从。增量复制用复制积压缓冲区repl_backlog实现。默认大小1MB如果你的业务写量大断线重连后增量缓冲不足会退化为全量复制。建议调大repl-backlog-size比如64MB尤其在高写入场景。8.2 哨兵模式怎么实现自动故障转移哨兵Sentinel是一组独立运行的进程监视主节点和从节点的状态。如果主节点客观下线多数哨兵都判定不可达哨兵会执行故障转移选择一个从节点提升为主节点并把其他从节点指向新主节点同时通知客户端更新主节点地址。面试题会问“哨兵怎么判断master挂了”哨兵会每隔一段时间默认1秒向主节点发送PING。如果超过down-after-milliseconds未响应哨兵会主观标记下线。随后哨兵之间通过订阅发布互相确认达到quorum数量后判定客观下线再触发故障转移。故障转移过程中有一个“领导者选举”流程简单理解是哨兵们投票选出谁负责执行故障转移保证不会多个哨兵同时操作。实际使用中要注意哨兵模式解决的是高可用不是大数据量存储。如果单节点内存已达上限比如几十GB需要的是Cluster分片而不是哨兵。8.3 Cluster模式数据分片与槽位计算Redis Cluster将数据分为16384个哈希槽每个节点负责一部分槽位。写入时客户端对key做CRC16计算取模16384得到槽位再根据槽位找到对应节点。如果客户端使用的key没有用Hash Tag同一个业务下的key可能分布在不同的节点上导致无法用mget、事务、Lua脚本等跨节点操作。这正是“Redis Cluster的坑”。解决方案是Hash Tag用{}把key的某一部分包起来比如{user}:1001:{info}这样Redis计算槽位时只用{}里的内容所以{}相同的key就在同一个槽位。Hash Tag会牺牲一些负载均衡性但能保证跨key操作可用。Cluster还有一个细节主从在Cluster内为“主节点负责读写、从节点负责备份”但默认从节点不转发读请求需要开启readonly命令才能均衡读压力。还有大量key迁移时reshard会对客户端有影响最好在低峰期操作。9. Redis缓存一致性先更新DB还是先删缓存9.1 四种策略的选型对比缓存与数据库的一致性问题在面试里是争论的焦点。主流策略有四种策略操作顺序风险适用场景Cache Aside旁路缓存读先读缓存miss再读DB并回写写先写DB再删除缓存写DB成功但删缓存失败会导致短暂脏数据最通用先删缓存再写DB写先删缓存再写DB删除缓存后、写DB前其他线程读旧数据回写不推荐单独使用先写DB再写缓存写先写DB再写缓存两个线程交叉写可能出现缓存旧值覆盖新值并发写场景有风险延迟双删先删缓存写DB隔一段时间再删缓存延迟窗口内仍有脏数据且多一次删除操作高一致性要求场景实际操作中用得最多的还是Cache Aside即“更新DB, 删除缓存”。为什么不是“更新缓存”因为更新缓存意味着要额外维护一份数据和缓存的映射容易出错。删除缓存则是“让缓存失效下次读时再回写”更简单。9.2 删除缓存失败的兜底方案消息队列与Binlog如果“写DB成功删缓存失败”怎么办常见方案是引入消息队列或订阅MySQL binlog。例如写DB成功后把“需要删除的缓存key”发送到MQ消费者接收到消息后执行删除如果删除失败则重试。成熟的工具如Canal监听Binlog把数据变更同步到Redis很多人用过“Canal Redis”做缓存同步。但引入MQ和Canal会增加系统复杂度中小项目不一定值得。更简单的折中是“设置合理的缓存过期时间”比如缓存5分钟即使某次删缓存失败5分钟后缓存也会过期自动从DB回写。这是用“最终一致”换取“简化实现”。面试时这样答最稳妥“我们以DB数据为准采用Cache Aside 缓存过期时间兜底。对于必须立即一致的场景使用删除缓存失败重试MQ的方案。”这样既有主流方案又有兜底机制。9.3 为什么“更新DB后直接更新缓存”会翻车假设线程A和线程B同时更新同一条数据但是A先写DB新值2B后写DB新值3由于线程调度B可能先更新了缓存值为3然后A再更新缓存值为2最终缓存里的值是2但DB里的值是3脏数据了。如果删除缓存而不是更新缓存即使B先删缓存、A后删缓存也不会产生这种覆盖问题因为缺失缓存只是多一次查询DB不会写入错误的值。所以“删缓存”比“更新缓存”在并发上更安全。当然如果业务允许缓存短暂不一致比如商品库存降低后几秒内看到旧数量可接受直接设置一个很短的过期时间甚至可以简化成“写DB后延迟删除缓存”。不必为了完美一致性把所有系统都搞复杂。10. Redis实战分布式限流、排行榜与缓存治理10.1 基于ZSet的滑动窗口限流限流是Redis非常经典的应用。最常见的算法有固定窗口和滑动窗口。固定窗口有临界问题假设每分钟限流100次在第59秒用完100次第60秒到第1分钟结束的1秒内又可以有100次导致两倍流量突发。滑动窗口用ZSet实现能很好地解决这个问题。思路把每个请求的时间戳作为scoremember用唯一标识可以是时间戳随机串。限流时用ZREMRANGEBYSCORE移除当前时间窗口之前的旧记录再用ZCARD统计当前窗口的请求数超过阈值则拒绝。# 当前窗口起始时间 ZREMRANGEBYSCORE limit:user:1001 0 (window_start ZCARD limit:user:1001 ZADD limit:user:1001 (current_timestamp (unique_id EXPIRE limit:user:1001 60要注意每次请求都执行ZREMRANGEBYSCORE会带来额外开销但窗口很小比如10秒数据量不大性能没问题。用Lua脚本封装整个流程可以避免并发下的窗口计算竞态。10.2 排行榜ZSet的降级策略与热点处理ZSet天然适合排行榜用ZADD写入分数ZREVRANGE 0 9取出前十名。但如果排行榜数据量很大比如百万用户频繁更新会导致ZSet内部跳表的更新开销。常见优化将排行榜分桶比如把用户按分数段切分到不同的ZSet查询时从高分段往下合并或者异步更新分数比如用户完成一次操作先在内存里累加每5秒批量同步到Redis。另外如果只有一个全局排行榜天天被读取会形成热点。我们当时用了多级缓存第一层本地缓存Caffeine过期2秒第二层Redis ZSet第三层DB兜底。查榜时先走本地缓存大幅降低Redis压力。10.3 大key与热key的治理从发现到解决大key和热key是Redis运维中最常遇到的性能问题。大key指单个key的value过大比如一个Hash有几百万字段一个List有几百万元素。大key会导致读取慢、网络传输大、删除卡顿、甚至触发Redis阻塞。排查方法用redis-cli --bigkeys扫描实例的大key。用DEBUG OBJECT key查看encoding和serializedlength。用SCAN STRLEN/LEN/HLOG批量检测。发现大key后处理方案分两类一类是拆分比如把一个大Hash拆成多个小Hash命名规则用hash:1、hash:2查询时取模定位另一类是压缩比如用gzip序列化大value但会耗费CPU需要权衡。如果是为了删除不要直接用DEL否则会阻塞Redis应用UNLINK命令异步删除。热key指某个key的读取量特别大比如秒杀商品。热key的解决思路是本地缓存把最热的少量数据放到应用进程内减少Redis请求。还可以用“多副本”方式即在Redis中为热key创建多个不同后缀的副本比如item:1001、item:1001:1然后随机读取一个副本把负载分散到不同slot。我在实际面试候选人时最欣慰的并不是对方背出所有答案而是能结合自己做过的项目说出“当时的约束和选择”。上面的十组题每一组都可以展开成一场完整的半小时追问。如果你能把它们当成一个知识地图来复习而不是当成标准答案来背诵我相信你会比大多数面试者更深一层。还有个小建议面试前最好自己起一个本地Redis亲手跑一遍SET NX EX、用SCAN遍历100万个key、写个Lua脚本扣减库存。手指过一遍印象会比看十篇文章都牢。
返回列表