[{"data":1,"prerenderedAt":388},["ShallowReactive",2],{"\u002Fjava-interview\u002Fredis":3,"java-interview-search:\u002Fjava-interview\u002Fredis":343},{"id":4,"title":5,"body":6,"category":329,"cover":330,"date":331,"description":332,"extension":333,"group":334,"meta":335,"navigation":336,"order":337,"path":338,"seo":339,"sitemap":340,"stem":341,"__hash__":342},"javaInterview\u002Fjava-interview\u002Fredis.md","Redis：数据结构、持久化与缓存一致性",{"type":7,"value":8,"toc":315},"minimark",[9,13,18,21,24,32,35,39,42,45,68,79,83,90,109,112,115,119,122,125,152,159,163,166,169,172,175,179,182,185,188,191,195,214,217,220,223,227,241,244,254,257,273,276,279],[10,11,12],"p",{},"Redis 题要避免停留在「有什么数据类型」。真正有区分度的是：为什么这个场景选这个结构、数据丢了最多丢多少、缓存和数据库不一致时怎么办。",[14,15,17],"h2",{"id":16},"_1-redis-有哪些数据类型底层编码是什么","1. Redis 有哪些数据类型？底层编码是什么？",[10,19,20],{},"基础类型有 String、List、Hash、Set、ZSet，扩展类型有 Bitmap、HyperLogLog、GEO 和 Stream。",[10,22,23],{},"底层编码会随数据量自动切换：String 有整数编码、短字符串编码和 SDS；Redis 7 起，小 List 直接用 listpack，元素变多后转为 quicklist（由多个 listpack 组成的双向链表）；Hash 和 ZSet 在小数据量下用 listpack，超过阈值后分别转为哈希表和跳表加哈希表；Set 在纯整数时用 intset，否则用 listpack 或哈希表。",[10,25,26,27,31],{},"选型要贴合访问模式：计数器用 String 的 ",[28,29,30],"code",{},"INCR","；排行榜用 ZSet；去重计数用 HyperLogLog（有误差）；签到用 Bitmap；消息队列可以用 Stream，但要评估消费组和持久化语义。",[10,33,34],{},"记住一个原则：结构选错，后面靠加机器补不回来。例如用 String 存整个大 JSON 再整体更新，会放大网络和序列化开销。",[14,36,38],{"id":37},"_2-redis-为什么快单线程模型是怎么回事","2. Redis 为什么快？单线程模型是怎么回事？",[10,40,41],{},"Redis 快在几点：数据在内存；数据结构针对场景优化；用 epoll 做 IO 多路复用；命令执行单线程，避免了锁竞争和上下文切换；协议简单。",[10,43,44],{},"「单线程」指命令执行是单线程的。Redis 6 起引入了多线程 IO，用来处理网络读写和协议解析，命令仍在主线程串行执行，所以不需要为数据结构加锁。",[10,46,47,48,51,52,55,56,59,60,63,64,67],{},"代价是任何慢命令都会阻塞所有请求。生产上要避免 ",[28,49,50],{},"KEYS *","、大 key 的 ",[28,53,54],{},"HGETALL","、对大集合做 ",[28,57,58],{},"SORT","、长时间运行的 Lua 脚本，以及直接 ",[28,61,62],{},"DEL"," 超大 key（用 ",[28,65,66],{},"UNLINK"," 异步释放）。",[10,69,70,71,74,75,78],{},"排查慢请求可以用 ",[28,72,73],{},"SLOWLOG GET"," 和 ",[28,76,77],{},"LATENCY DOCTOR","，并在监控里关注命令耗时和阻塞型命令的出现频率。",[14,80,82],{"id":81},"_3-rdb-和-aof-怎么选会丢数据吗","3. RDB 和 AOF 怎么选？会丢数据吗？",[10,84,85,86,89],{},"RDB 是某一时刻的全量快照，",[28,87,88],{},"BGSAVE"," 通过 fork 子进程写盘，借助写时复制减少阻塞。文件紧凑、恢复快，但两次快照之间的数据会丢。",[10,91,92,93,96,97,100,101,104,105,108],{},"AOF 记录写命令，",[28,94,95],{},"appendfsync"," 有三个档位：",[28,98,99],{},"always"," 最安全但性能最差；",[28,102,103],{},"everysec"," 是默认值，最多丢约一秒；",[28,106,107],{},"no"," 交给操作系统刷盘，丢得更多。AOF 文件会持续增长，需要重写压缩。",[10,110,111],{},"Redis 4 起支持混合持久化：重写时先生成 RDB 格式的基线，再追加增量命令，兼顾恢复速度和数据完整度。",[10,113,114],{},"实际选择通常是两者都开：RDB 用于备份和快速恢复，AOF 用于缩小丢失窗口。但无论怎么配，Redis 都不是强一致存储，关键数据必须有数据库或消息队列兜底。",[14,116,118],{"id":117},"_4-过期键是怎么删除的内存满了怎么办","4. 过期键是怎么删除的？内存满了怎么办？",[10,120,121],{},"过期删除是「惰性 + 定期」组合：访问键时检查是否过期；同时后台周期性随机抽样，把过期比例降到阈值以下。这样避免为每个键建定时器，但也意味着过期键可能短暂占内存。",[10,123,124],{},"从节点通常不独立删除过期键，而是等待主节点的删除命令同步，保证主从数据一致。",[10,126,127,128,131,132,135,136,139,140,143,144,147,148,151],{},"内存达到 ",[28,129,130],{},"maxmemory"," 后按淘汰策略处理：",[28,133,134],{},"noeviction"," 直接报错；",[28,137,138],{},"allkeys-lru"," \u002F ",[28,141,142],{},"allkeys-lfu"," 在所有键里淘汰；",[28,145,146],{},"volatile-*"," 只淘汰设置了过期时间的键；",[28,149,150],{},"volatile-ttl"," 优先淘汰剩余时间短的。",[10,153,154,155,158],{},"LRU 是近似实现，靠采样若干键选最久未用的，",[28,156,157],{},"maxmemory-samples"," 越大越精确。LFU 更适合「曾经热、现在冷」的访问分布，但计数器需要衰减。",[14,160,162],{"id":161},"_5-缓存穿透击穿雪崩分别怎么解决","5. 缓存穿透、击穿、雪崩分别怎么解决？",[10,164,165],{},"穿透是查不存在的数据，请求每次都打到数据库。可以用布隆过滤器提前拦截，或缓存空值并设置较短过期时间；接口层还要做参数校验和限流。",[10,167,168],{},"击穿是某个热点 key 过期瞬间，大量请求同时回源。常见做法是互斥重建（只放一个请求查库，其他等待）或逻辑过期（值不过期，异步刷新），前者简单但会阻塞，后者体验好但会读到旧值。",[10,170,171],{},"雪崩是大量 key 同时失效，或 Redis 整体不可用。解决思路包括给过期时间加随机抖动、做多级缓存、集群化部署、加熔断降级和限流，以及准备本地兜底数据。",[10,173,174],{},"三者容易混：穿透是「数据不存在」，击穿是「单个热点失效」，雪崩是「大面积同时失效或整体故障」。",[14,176,178],{"id":177},"_6-如何保证缓存和数据库的一致性","6. 如何保证缓存和数据库的一致性？",[10,180,181],{},"常用的是旁路缓存：读时先查缓存，未命中查库并回填；写时先更新数据库，再删除缓存。",[10,183,184],{},"为什么是删除而不是更新缓存：并发更新时，两个写请求的缓存更新顺序可能与数据库提交顺序相反，留下脏数据；删除让下一次读自然回填。",[10,186,187],{},"即使先更新库再删缓存，仍有极小概率不一致：读请求在删除前拿到旧值，删除后又把旧值写回。可以用延迟双删、给缓存加版本号，或订阅 binlog 异步删除来收敛。",[10,189,190],{},"要接受「最终一致」而不是「强一致」。对一致性要求极高的场景，应让缓存只承载可容忍短暂陈旧的数据，或在读路径上做版本校验。",[14,192,194],{"id":193},"_7-redis-分布式锁怎么做有哪些坑","7. Redis 分布式锁怎么做？有哪些坑？",[10,196,197,198,201,202,205,206,209,210,213],{},"基本写法是 ",[28,199,200],{},"SET lock_key unique_value NX PX 30000","：",[28,203,204],{},"NX"," 保证互斥，",[28,207,208],{},"PX"," 设置过期防止死锁，",[28,211,212],{},"unique_value"," 标识持有者。",[10,215,216],{},"释放时必须用 Lua 先比较 value 再删除，避免误删别人的锁；业务执行时间可能超过锁租期，需要看门狗续期，Redisson 就封装了这套逻辑。",[10,218,219],{},"仍有两个本质问题：主从异步复制下，主节点加锁成功后宕机，锁可能丢失；锁过期与业务完成之间存在时间竞态。前者需要接受「Redis 锁不是强一致」或改用共识系统，后者可以用 fencing token 让下游拒绝旧持有者的写。",[10,221,222],{},"所以 Redis 锁适合「防重复、降并发」这类场景，不适合转账扣款这种必须绝对互斥的核心链路。Redlock 也有广泛争议，不要把「多数节点加锁成功」等同于强一致。",[14,224,226],{"id":225},"_8-主从哨兵cluster-有什么区别热-key-和大-key-怎么处理","8. 主从、哨兵、Cluster 有什么区别？热 key 和大 key 怎么处理？",[10,228,229,230,233,234,139,237,240],{},"主从解决读扩展和备份，复制是异步的，主节点故障需要人工切换。哨兵在主从之上做监控和自动故障转移，靠多数派投票选主，仍是一个主节点写。Cluster 做分片，16384 个槽分布在多个主节点上，客户端按 ",[28,231,232],{},"CRC16(key) mod 16384"," 路由，遇到 ",[28,235,236],{},"MOVED",[28,238,239],{},"ASK"," 重定向；多 key 操作要求落在同一槽，可用 hash tag 强制同槽。",[10,242,243],{},"热 key 指单个 key 的访问量过大，会打满某个节点。可以做本地缓存、把 key 拆成多个副本分散读、增加只读副本，或在代理层做请求合并。",[10,245,246,247,249,250,253],{},"大 key 指 value 过大或集合元素过多，会造成网络阻塞、删除卡顿和迁移困难。应拆分结构、分页读取、用 ",[28,248,66],{}," 删除，并通过 ",[28,251,252],{},"redis-cli --bigkeys"," 和慢日志定期发现。",[14,255,256],{"id":256},"易错点与自测",[258,259,260,264,270],"ul",{},[261,262,263],"li",{},"单线程指的是命令执行，不是整个进程只有一个线程。",[261,265,266,267,269],{},"AOF ",[28,268,103],{}," 仍可能丢约一秒，Redis 不能替代持久化数据库。",[261,271,272],{},"缓存一致性只能做到最终一致，方案要配套业务容忍度和监控。",[10,274,275],{},"自测：① 说明 ZSet 在小数据量和大数据量下的编码差异；② 比较互斥重建与逻辑过期在击穿场景的优劣；③ 写出分布式锁的加锁与释放命令，并解释 value 为什么必须唯一。",[14,277,278],{"id":278},"参考资料",[258,280,281,291,299,307],{},[261,282,283,290],{},[284,285,289],"a",{"href":286,"rel":287},"https:\u002F\u002Fredis.io\u002Fdocs\u002Flatest\u002Fdevelop\u002Fdata-types\u002F",[288],"nofollow","Redis 官方文档：数据类型"," — 各类型的语义与典型场景",[261,292,293,298],{},[284,294,297],{"href":295,"rel":296},"https:\u002F\u002Fredis.io\u002Fdocs\u002Flatest\u002Foperate\u002Foss_and_stack\u002Fmanagement\u002Fpersistence\u002F",[288],"Redis 官方文档：持久化"," — RDB、AOF 与混合持久化",[261,300,301,306],{},[284,302,305],{"href":303,"rel":304},"https:\u002F\u002Fredis.io\u002Fdocs\u002Flatest\u002Fdevelop\u002Fuse\u002Fpatterns\u002Fdistributed-locks\u002F",[288],"Redis 官方文档：分布式锁"," — 单实例锁与 Redlock 的说明",[261,308,309,314],{},[284,310,313],{"href":311,"rel":312},"https:\u002F\u002Fredis.io\u002Fdocs\u002Flatest\u002Fdevelop\u002Fclients\u002Fclient-side-caching\u002F",[288],"Redis 官方文档：客户端缓存"," — 多级缓存与失效通知",{"title":316,"searchDepth":317,"depth":317,"links":318},"",2,[319,320,321,322,323,324,325,326,327,328],{"id":16,"depth":317,"text":17},{"id":37,"depth":317,"text":38},{"id":81,"depth":317,"text":82},{"id":117,"depth":317,"text":118},{"id":161,"depth":317,"text":162},{"id":177,"depth":317,"text":178},{"id":193,"depth":317,"text":194},{"id":225,"depth":317,"text":226},{"id":256,"depth":317,"text":256},{"id":278,"depth":317,"text":278},"java-interview",null,"2026-10-24","从底层编码、单线程模型到 RDB\u002FAOF、淘汰策略和分布式锁，系统梳理 Redis 面试题与线上故障的应对方式","md","框架与数据库",{},true,"90","\u002Fjava-interview\u002Fredis",{"title":5,"description":332},{"loc":338},"java-interview\u002Fredis","yHXdZtk_Aojf-8OglrYewWd--oWMETFu30fKxA9HXrM",[344,348,352,356,360,364,368,372,376,380,384],{"id":338,"title":5,"titles":345,"content":346,"level":347},[],"从底层编码、单线程模型到 RDB\u002FAOF、淘汰策略和分布式锁，系统梳理 Redis 面试题与线上故障的应对方式 Redis 题要避免停留在「有什么数据类型」。真正有区分度的是：为什么这个场景选这个结构、数据丢了最多丢多少、缓存和数据库不一致时怎么办。",1,{"id":349,"title":17,"titles":350,"content":351,"level":317},"\u002Fjava-interview\u002Fredis#_1-redis-有哪些数据类型底层编码是什么",[5],"基础类型有 String、List、Hash、Set、ZSet，扩展类型有 Bitmap、HyperLogLog、GEO 和 Stream。 底层编码会随数据量自动切换：String 有整数编码、短字符串编码和 SDS；Redis 7 起，小 List 直接用 listpack，元素变多后转为 quicklist（由多个 listpack 组成的双向链表）；Hash 和 ZSet 在小数据量下用 listpack，超过阈值后分别转为哈希表和跳表加哈希表；Set 在纯整数时用 intset，否则用 listpack 或哈希表。 选型要贴合访问模式：计数器用 String 的 INCR；排行榜用 ZSet；去重计数用 HyperLogLog（有误差）；签到用 Bitmap；消息队列可以用 Stream，但要评估消费组和持久化语义。 记住一个原则：结构选错，后面靠加机器补不回来。例如用 String 存整个大 JSON 再整体更新，会放大网络和序列化开销。",{"id":353,"title":38,"titles":354,"content":355,"level":317},"\u002Fjava-interview\u002Fredis#_2-redis-为什么快单线程模型是怎么回事",[5],"Redis 快在几点：数据在内存；数据结构针对场景优化；用 epoll 做 IO 多路复用；命令执行单线程，避免了锁竞争和上下文切换；协议简单。 「单线程」指命令执行是单线程的。Redis 6 起引入了多线程 IO，用来处理网络读写和协议解析，命令仍在主线程串行执行，所以不需要为数据结构加锁。 代价是任何慢命令都会阻塞所有请求。生产上要避免 KEYS *、大 key 的 HGETALL、对大集合做 SORT、长时间运行的 Lua 脚本，以及直接 DEL 超大 key（用 UNLINK 异步释放）。 排查慢请求可以用 SLOWLOG GET 和 LATENCY DOCTOR，并在监控里关注命令耗时和阻塞型命令的出现频率。",{"id":357,"title":82,"titles":358,"content":359,"level":317},"\u002Fjava-interview\u002Fredis#_3-rdb-和-aof-怎么选会丢数据吗",[5],"RDB 是某一时刻的全量快照，BGSAVE 通过 fork 子进程写盘，借助写时复制减少阻塞。文件紧凑、恢复快，但两次快照之间的数据会丢。 AOF 记录写命令，appendfsync 有三个档位：always 最安全但性能最差；everysec 是默认值，最多丢约一秒；no 交给操作系统刷盘，丢得更多。AOF 文件会持续增长，需要重写压缩。 Redis 4 起支持混合持久化：重写时先生成 RDB 格式的基线，再追加增量命令，兼顾恢复速度和数据完整度。 实际选择通常是两者都开：RDB 用于备份和快速恢复，AOF 用于缩小丢失窗口。但无论怎么配，Redis 都不是强一致存储，关键数据必须有数据库或消息队列兜底。",{"id":361,"title":118,"titles":362,"content":363,"level":317},"\u002Fjava-interview\u002Fredis#_4-过期键是怎么删除的内存满了怎么办",[5],"过期删除是「惰性 + 定期」组合：访问键时检查是否过期；同时后台周期性随机抽样，把过期比例降到阈值以下。这样避免为每个键建定时器，但也意味着过期键可能短暂占内存。 从节点通常不独立删除过期键，而是等待主节点的删除命令同步，保证主从数据一致。 内存达到 maxmemory 后按淘汰策略处理：noeviction 直接报错；allkeys-lru \u002F allkeys-lfu 在所有键里淘汰；volatile-* 只淘汰设置了过期时间的键；volatile-ttl 优先淘汰剩余时间短的。 LRU 是近似实现，靠采样若干键选最久未用的，maxmemory-samples 越大越精确。LFU 更适合「曾经热、现在冷」的访问分布，但计数器需要衰减。",{"id":365,"title":162,"titles":366,"content":367,"level":317},"\u002Fjava-interview\u002Fredis#_5-缓存穿透击穿雪崩分别怎么解决",[5],"穿透是查不存在的数据，请求每次都打到数据库。可以用布隆过滤器提前拦截，或缓存空值并设置较短过期时间；接口层还要做参数校验和限流。 击穿是某个热点 key 过期瞬间，大量请求同时回源。常见做法是互斥重建（只放一个请求查库，其他等待）或逻辑过期（值不过期，异步刷新），前者简单但会阻塞，后者体验好但会读到旧值。 雪崩是大量 key 同时失效，或 Redis 整体不可用。解决思路包括给过期时间加随机抖动、做多级缓存、集群化部署、加熔断降级和限流，以及准备本地兜底数据。 三者容易混：穿透是「数据不存在」，击穿是「单个热点失效」，雪崩是「大面积同时失效或整体故障」。",{"id":369,"title":178,"titles":370,"content":371,"level":317},"\u002Fjava-interview\u002Fredis#_6-如何保证缓存和数据库的一致性",[5],"常用的是旁路缓存：读时先查缓存，未命中查库并回填；写时先更新数据库，再删除缓存。 为什么是删除而不是更新缓存：并发更新时，两个写请求的缓存更新顺序可能与数据库提交顺序相反，留下脏数据；删除让下一次读自然回填。 即使先更新库再删缓存，仍有极小概率不一致：读请求在删除前拿到旧值，删除后又把旧值写回。可以用延迟双删、给缓存加版本号，或订阅 binlog 异步删除来收敛。 要接受「最终一致」而不是「强一致」。对一致性要求极高的场景，应让缓存只承载可容忍短暂陈旧的数据，或在读路径上做版本校验。",{"id":373,"title":194,"titles":374,"content":375,"level":317},"\u002Fjava-interview\u002Fredis#_7-redis-分布式锁怎么做有哪些坑",[5],"基本写法是 SET lock_key unique_value NX PX 30000：NX 保证互斥，PX 设置过期防止死锁，unique_value 标识持有者。 释放时必须用 Lua 先比较 value 再删除，避免误删别人的锁；业务执行时间可能超过锁租期，需要看门狗续期，Redisson 就封装了这套逻辑。 仍有两个本质问题：主从异步复制下，主节点加锁成功后宕机，锁可能丢失；锁过期与业务完成之间存在时间竞态。前者需要接受「Redis 锁不是强一致」或改用共识系统，后者可以用 fencing token 让下游拒绝旧持有者的写。 所以 Redis 锁适合「防重复、降并发」这类场景，不适合转账扣款这种必须绝对互斥的核心链路。Redlock 也有广泛争议，不要把「多数节点加锁成功」等同于强一致。",{"id":377,"title":226,"titles":378,"content":379,"level":317},"\u002Fjava-interview\u002Fredis#_8-主从哨兵cluster-有什么区别热-key-和大-key-怎么处理",[5],"主从解决读扩展和备份，复制是异步的，主节点故障需要人工切换。哨兵在主从之上做监控和自动故障转移，靠多数派投票选主，仍是一个主节点写。Cluster 做分片，16384 个槽分布在多个主节点上，客户端按 CRC16(key) mod 16384 路由，遇到 MOVED \u002F ASK 重定向；多 key 操作要求落在同一槽，可用 hash tag 强制同槽。 热 key 指单个 key 的访问量过大，会打满某个节点。可以做本地缓存、把 key 拆成多个副本分散读、增加只读副本，或在代理层做请求合并。 大 key 指 value 过大或集合元素过多，会造成网络阻塞、删除卡顿和迁移困难。应拆分结构、分页读取、用 UNLINK 删除，并通过 redis-cli --bigkeys 和慢日志定期发现。",{"id":381,"title":256,"titles":382,"content":383,"level":317},"\u002Fjava-interview\u002Fredis#易错点与自测",[5],"单线程指的是命令执行，不是整个进程只有一个线程。 AOF everysec 仍可能丢约一秒，Redis 不能替代持久化数据库。 缓存一致性只能做到最终一致，方案要配套业务容忍度和监控。 自测：① 说明 ZSet 在小数据量和大数据量下的编码差异；② 比较互斥重建与逻辑过期在击穿场景的优劣；③ 写出分布式锁的加锁与释放命令，并解释 value 为什么必须唯一。",{"id":385,"title":278,"titles":386,"content":387,"level":317},"\u002Fjava-interview\u002Fredis#参考资料",[5],"Redis 官方文档：数据类型 — 各类型的语义与典型场景 Redis 官方文档：持久化 — RDB、AOF 与混合持久化 Redis 官方文档：分布式锁 — 单实例锁与 Redlock 的说明 Redis 官方文档：客户端缓存 — 多级缓存与失效通知",1793263559747]