1. 为什么我坚持把Redis知识画成一张导图第一次接触Redis思维导图是很多年前带一个新人他背命令背得很熟SET、HSET、ZADD张口就来可真到了线上出现内存告警、主从延迟、缓存数据对不上的时候他完全不知道从哪儿下手。问题不在于他记性差而在于他脑子里的Redis是一堆散落的珠子没有一根线把它们串起来。后来我花了两个周末把当时能查到的资料和自己的笔记全部摊开画了一张包含详细知识点的Redis思维导图从那以后无论是排查问题、做技术方案评审还是给团队做内部分享我都从这张图上找线索。这张图我前后改了七八版删掉过一些只在旧版本成立的内容也补进了新版本里才有的结构今天把它整理出来的目的很简单给正在学Redis但越学越乱的人一条主线也给需要做技术沉淀的同行一个可以照着做的模板。先说清楚这张导图能解决什么。它不是命令手册的目录树也不是某个版本的更新日志清单而是一张“问题定位地图”。你拿到一个现象比如“某个接口偶发超时”顺着图从上往下走先看是不是慢命令、再看是不是大key、再看是不是内存淘汰触发了抖动、再看是不是主从切换或者集群槽位迁移导致的短暂不可用。每一步都能在图上的具体分支里找到对应的排查入口和参数。适合谁看我的判断是三类人一是刚把基础命令过完一轮、但知识还是碎片状态的同学二是有一定使用经验、但只熟悉自己那一小块业务场景的开发者比如只写过缓存没碰过集群的三是需要给别人讲Redis、得自己先有一套完整框架的讲师和技术负责人。还有一个常被忽略的价值导图逼你做取舍。你在纸上画的时候会发现“主从复制”和“哨兵”到底该并列还是该嵌套、“持久化”该放在“数据安全”下面还是单独一层这些结构问题本质上就是你对知识关系的理解问题。我第一版导图把“持久化”和“高可用”画成了平级后来发现不对持久化解决的是单机数据不丢高可用解决的是服务不停两者目标不同但都会影响重启后的恢复速度和数据完整性于是改成在“持久化”下面挂一个“对主从切换的影响”的交叉引用节点。这种反复调整的过程比你抄十遍笔记都管用。2. 导图的三层骨架怎么定2.1 我用“数据怎么放、数据怎么不丢、服务怎么不停”做主干导图最容易犯的错是按官方文档的章节顺序画结果画出来就是一棵文档目录树看着整齐用起来找不到东西。我的做法是先定三个主干对应三个最朴素的问题数据怎么放数据模型与命令、数据怎么不丢内存管理与持久化、服务怎么不停高可用与扩展。这三根主干之下再挂内容剩下的工程实践类知识比如缓存穿透、分布式锁、限流作为“横向切面”单独一层因为它们不属于某一个主干而是横跨多个模块的组合问题。这么分层的好处是任何一个线上现象都能被快速归位。举个例子“写入成功了但读出来是旧值”先往主干一归位这是数据一致性问题不在“数据怎么放”里而在“横向切面”下的缓存一致性分支再具体一点是删除缓存失败还是主从延迟读到了旧数据。如果按文档顺序画你大概会先去翻“主从复制”章节方向就偏了。我实测下来用问题导向分层排障时找节点的速度比按文档顺序快得多因为你的大脑是按现象回忆的不是按章节回忆的。主干拆完之后还有一层“索引层”就是给每个关键节点打标签标明它属于“必背”“会用就行”“知道有这回事”三档。这一层是我后来加的非常有用。Redis知识点太多了全背下来不现实比如OBJECT ENCODING的具体返回值你不一定记得住但你必须知道有这么个命令能查编码而像SET key value NX PX 3000这种必须闭着眼写出来。导图上用不同颜色或者前缀标记这三档复习的时候按档位过效率会高很多。我的图里“必背”那一档大概只占全部节点的两成剩下八成都是理解和检索级别的。2.2 每一层的粒度控制一屏能看完才算合格画导图最怕失控一个“数据结构”节点下面能挂出三十个叶子画完自己都不想看第二眼。我给自己定的规矩是任意一个父节点下面直接子节点不超过七个超过就说明层级没拆够。比如“String”这个节点下面挂的是“常用命令”“典型场景”“编码方式”“坑点”四个子节点具体命令不挂在导图上而是落在配套的笔记里。导图负责结构和索引笔记负责细节两者配合。这个分工很重要很多人把导图当成知识库恨不得把所有命令的参数都写上去结果图变得没法看也就失去了导图“一眼看全”的意义。另外我给每个叶子节点都配了可执行的动作而不是名词。以前我写的是“大key问题”这种节点看一百遍也不知道该干什么改成“大key用 scan 分批查 unlink 异步删”一眼就知道下一步动作。这个习惯是从排查手册里学来的凡是能在生产环境里执行的动作就写成动词开头的短语纯粹的概念就写成名词加一句判断标准。导图上的文字越短你回忆的时候越容易命中。3. 主干一数据怎么放3.1 五类基础结构重点不是命令而是选型判断String、Hash、List、Set、ZSet 这五类基础结构几乎所有教程都会讲命令但导图上真正值得记的是选型判断。我在导图上给每一类都写了一句“什么时候非它不可”。String 的核心优势是能整存整取、能做计数和分布式锁的值载体缺点是改局部字段要整串覆盖而且没有结构化的过期控制。Hash 解决的是对象部分字段更新HSET user:1 name x只动一个字段网络开销和序列化开销都小但 Hash 的字段没法单独设过期时间这是很多人踩过的坑。List 是唯一能原生做队列的结构LPUSH加BRPOP就是最简单的阻塞队列但它不能按位置随机访问中间删元素是 O(n)。Set 用来做去重和集合运算SINTER、SDIFF这类运算在数据量可控的时候非常爽量大了会阻塞。ZSet 是排行榜和延迟队列的主力跳表加哈希的双结构保证了范围查询和单点查询都是对数级。我在图上给这五类各配了一个真实场景比如“用户签到”用 Set 存一个月内的日期SADD sign:uid:202401 15判断连续签到就用SISMEMBER逐个查或者用BITCOUNT走位图再比如“排行榜”用 ZSetZINCRBY rank:daily 1 uid加分ZREVRANGE rank:daily 0 9 WITHSCORES取前十。把场景和结构绑在一起比背二十条命令管用。新手最容易犯的错是拿 String 存 JSON 去做对象更新读出来反序列化、改一个字段、再序列化写回去并发一高就丢更新这种场景换成 Hash 或者加乐观锁都能解决但前提是你得在选型阶段就想到。3.2 高级结构是拉开差距的地方BitMap、HyperLogLog、GEO、Stream 这四个我单独在导图上开了一个分支标题叫“用得少但用对了很省”。BitMap 本质是 String 的位操作签到、在线状态、布尔标记这类场景用位图能把内存压到极致。我算过一笔账一百万用户的日活标记用 Set 存用户 ID按每个 ID 平均十几个字节算一天就是十几兆用 BitMap一百万位就是 125KB差距是三个数量级。HyperLogLog 用于基数统计标准误差 0.81%PFADD、PFCOUNT就能做 UV 统计代价是不能取回具体元素也不能做交并之外的复杂运算。GEO 底层是 ZSetGEOADD存经纬度GEOSEARCH找附近的人注意它用的是球面距离计算精度够用但不是测绘级。Stream 我要单独说因为它是很多人知识体系里的空白。Stream 补上了 Redis 在消息队列场景的能力支持消费组、ack 机制、pending 列表XADD生产、XREADGROUP消费、XACK确认、XAUTOCLAIM处理超时未确认的消息。它比 List 做的简易队列强在“消费组”和“回溯”比你引入一个重量级消息中间件轻得多。但它不是万能的消息不落盘持久化保证有限堆积能力受内存限制所以我的导图上 Stream 节点旁边写了一句判断标准轻量异步任务、允许极低概率丢失的场景可以用核心交易链路别用。这种“什么时候不用”的标注比“什么时候用”更值钱。3.3 底层编码理解它才知道什么时候会突然变慢导图上有一块专门讲底层编码因为这是“为什么同样的命令有时候快有时候慢”的答案。String 有 int、embstr、raw 三种编码数字类型的值走 int短字符串44 字节以内走 embstr一次内存分配搞定长字符串走 raw。Hash 在小数据量时用 listpack老版本是 ziplist字段数或单个值长度超过阈值就转成 hashtable转换是单向的转过去就回不来了。List 现在是 quicklist本质是链表套 listpack兼顾内存和访问效率。Set 在元素全是整数且数量不多时用 intset否则用 hashtable。ZSet 在元素少的时候用 listpack多了就转 skiplist 加 dict。这些转换阈值是可以在配置里看到的比如hash-max-listpack-entries默认 128hash-max-listpack-value默认 64。我在导图上把这些默认值和“转换不可逆”这四个字加粗了因为踩过坑有个业务把用户画像存在一个 Hash 里字段数从几十涨到几百编码从 listpack 变成 hashtable内存直接翻倍监控上看到内存曲线在某个时间点突然抬升查了半天才定位到是编码转换。知道这回事之后你设计 key 的时候就会主动控制字段数量或者干脆拆成多个 key。结构小数据量编码大数据量编码转换是否可逆Stringint / embstrraw是按长度Hashlistpackhashtable否Listquicklist内嵌 listpackquicklist 分片变大不涉及Setintsethashtable否ZSetlistpackskiplist dict否4. 主干二数据怎么不丢4.1 过期删除和内存淘汰是两套机制别混为一谈这两件事在我的导图上是两个并列节点因为混淆它们是最常见的理解偏差。过期删除解决的是“设置了 TTL 的 key 到点该消失”用的是惰性删除加定期删除的组合你访问这个 key 的时候发现过期了就删掉同时后台每 100 毫秒左右抽样一批带过期时间的 key删掉过期的如果过期比例高就加快循环。这意味着过期 key 不一定准时被清理内存里可能残留一批“已经过期但还没被删”的 keyINFO memory里的expired_keys能看出删除量DBSIZE和实际内存占用的差异有时候就来自这里。内存淘汰解决的是“内存到上限了该扔谁”触发条件是达到maxmemory配置。八种策略里noeviction是默认值写满之后写入直接报错读还能继续allkeys-lru和volatile-lru是按最近最少使用淘汰区别是只淘汰带过期时间的还是全量淘汰LFU 版本按访问频率淘汰适合有明显热点的场景volatile-ttl优先淘汰剩余存活时间短的。我在图上把allkeys-lru和allkeys-lfu的差异写清楚了LRU 是“最近没被访问”LFU 是“总共没被访问几次”前者对突发流量友好后者对长期热点更准。LRU 在 Redis 里是近似实现靠随机采样若干个 keymaxmemory-samples默认 5挑最久未用的采样数越大越准但越耗 CPU这个参数我一般不动除非监控上明显看到淘汰不准。注意如果你把 Redis 当纯缓存用务必显式配置maxmemory和淘汰策略默认的noeviction在生产环境里等同于埋雷写满那一刻所有写请求全部失败。4.2 RDB 和 AOF 的取舍本质是恢复速度和数据完整性的权衡RDB 是快照BGSAVE会 fork 一个子进程靠写时复制把内存页复制出去主进程继续服务子进程把数据写成二进制文件。优点是文件紧凑、恢复快缺点是两次快照之间的数据会丢快照期间如果写入量大fork 和 COW 会造成内存额外占用甚至抖动。AOF 是追加日志把每条写命令记下来appendfsync决定刷盘策略always每条都刷最安全但性能最差everysec每秒刷一次最多丢一秒数据这是绝大多数场景的选择no交给操作系统性能和安全性都不保证。AOF 文件会越来越大所以要重写。重写不是读旧文件而是根据当前内存数据重新生成一份最小命令集同样靠 fork 子进程完成。重写期间的新写入会同时进旧 AOF 缓冲和新 AOF 缓冲保证不丢。混合持久化aof-use-rdb-preamble yes是现在的主流选择重写时把当前数据以 RDB 格式写到 AOF 文件头部后续增量用命令格式追加这样恢复时先加载 RDB 部分再回放增量兼顾了速度和安全。我自己的经验是缓存场景用 RDB 或者干脆不持久化都行有数据可靠性要求的场景必须开混合持久化并且appendfsync everysec。对比项RDBAOF混合持久化数据丢失窗口分钟级秒级秒级恢复速度快慢回放命令较快文件体积小大中对主进程影响fork 时有抖动刷盘有开销重写时有抖动还有一个细节常被忽略持久化失败会直接影响写入。如果stop-writes-on-bgsave-error yes默认开启后台快照失败后主进程会拒绝写入。这个默认值本意是保护数据但在磁盘紧张的环境里容易造成“莫名其妙全部写不进去”我的导图上在这个节点旁边标了排查第一步就是看INFO persistence里的rdb_last_bgsave_status。5. 主干三服务怎么不停5.1 主从复制和哨兵先搞清楚它们在解决什么主从复制解决的是读扩展和数据冗余哨兵解决的是主节点挂了之后自动切换。复制的流程是从节点连上主节点发PSYNC主节点做一次全量同步生成 RDB 发给从节点期间的写命令放进缓冲区之后进入增量同步靠复制偏移量和复制积压缓冲区repl-backlog-size续传。网络断一下能不能续上取决于断开期间的增量是否还在积压缓冲区里缓冲区太小就得重新全量同步这会带来明显的延迟和带宽开销所以生产环境我一般把积压缓冲区调到几十兆。哨兵是一组独立进程监控主从节点主观下线是单个哨兵认为节点不通客观下线是超过法定数量quorum的哨兵都认为不通。确认主节点客观下线后哨兵之间选一个 leader 执行故障转移选新主的规则大致是比较优先级、比较复制偏移量、比较 runid。转移完成后会通知客户端新地址客户端通过哨兵获取当前主节点。我在图上特别标了一个坑哨兵切换期间有短暂的写入失败窗口客户端必须做好重试另外如果主从之间存在大量延迟切换后可能丢掉尚未同步的数据这个在导图上归到“切换的数据代价”节点下。5.2 集群分片16384 个槽和它带来的一堆细节集群把数据按 CRC16 算出的槽位分到 16384 个槽上每个主节点负责一部分槽槽可以在节点间迁移。客户端访问到不负责该槽的节点时会收到MOVED重定向如果槽正在迁移中会收到ASK临时重定向。这两个重定向的区别我在导图上写得很直白MOVED是永久性的客户端应该更新本地槽位缓存ASK是临时性的只对本次请求有效不能更新缓存。集群模式下有几个必须知道的限制多 key 操作要求所有 key 在同一个槽解决办法是用 hash tag把 key 里同一段用花括号包起来比如{user:1}:name和{user:1}:age会落到同一个槽事务和 Lua 脚本也只能操作同槽的 key集群没有自动的跨节点数据合并。脑裂问题也值得记一笔网络分区时旧主还在接受写入新主已经选出来了等网络恢复旧主降为从节点那段时间的写入就丢了。缓解手段是配置min-replicas-to-write和min-replicas-max-lag让主节点在健康的从节点数量不足时拒绝写入用可用性换一致性。6. 横向切面缓存工程里的那些坑6.1 穿透、击穿、雪崩三个词对应三种不同的处理这三个概念经常被混着说我在导图上把它们拆成三个并列节点每个节点下面写“现象、原因、解法”。穿透是查一个数据库里也不存在的 key缓存永远不命中请求全打到存储层。解法是缓存空值设短一点的 TTL比如几十秒或者用布隆过滤器提前拦掉不存在的 key注意布隆过滤器有误判率只能保证“说不存在就一定不存在”。击穿是某个热点 key 过期的瞬间大量并发请求同时回源。解法是加互斥锁只让一个请求回源其他请求等待或者返回旧值或者用逻辑过期value 里存过期时间物理上不设 TTL发现逻辑过期后异步刷新。雪崩是大量 key 同时过期或者缓存整体不可用。解法是给 TTL 加随机抖动、做多级缓存本地缓存加远端缓存、对回源做限流和降级。这三个解法不是互斥的实际项目里我通常组合用TTL 加随机偏移是基础操作热点 key 单独加逻辑过期入口处再加一层限流兜底。导图上我给每个解法都标了代价比如空值缓存会占用内存且可能被恶意构造的 key 撑爆布隆过滤器需要额外维护并且不支持删除互斥锁会增加响应时间还可能死锁。问题触发条件首选解法代价穿透查询不存在的 key空值缓存 / 布隆过滤器内存占用 / 误判击穿热点 key 过期互斥锁 / 逻辑过期延迟增加 / 实现复杂雪崩批量过期或整体故障TTL 抖动 / 多级缓存 / 限流一致性变复杂6.2 一致性和大 key日常最容易积累的技术债缓存和数据库的一致性我的导图上只有一句核心结论只要用了缓存就一定有窗口期不一致区别是窗口多长、业务能不能忍。最常见的做法是先更新数据库再删除缓存删除失败就重试或者投递到消息队列补偿延迟双删是在更新前后各删一次中间留一段延迟用来覆盖并发读导致的旧值回填更彻底的做法是订阅数据库的变更日志异步删缓存把耦合降到最低。每种做法我都标了适用场景强一致性要求高的走变更日志加版本号普通业务用先更新库再删缓存就够别为了理论上的完美把系统搞得太复杂。大 key 是另一个高频问题。判断标准我在图上写了两条单个 value 超过 10KB 就要警惕集合类元素超过 5000 个就要评估。危害主要在三块网络传输阻塞一次读几十兆直接把带宽打满、删除阻塞DEL大集合是同步的会卡住主线程、集群迁移卡顿。处理办法是拆分把一个大 Hash 按字段前缀拆成多个小的删除用UNLINK走异步回收配合lazyfree-lazy-user-del配置排查用redis-cli --bigkeys扫一遍或者用SCAN游标分批遍历千万别用KEYS *这个命令在生产环境基本等同于事故。提示SCAN只保证遍历期间一直存在的元素一定会被返回不保证不重复也不保证返回顺序。做批量处理的时候必须自己处理重复别假设拿到的是唯一集合。7. 动手画工具、命名和维护节奏7.1 工具选型和“文本化优先”的原则工具这块我试过不少。桌面导图工具画起来顺手、排版漂亮适合对外分享缺点是不好做版本管理改一版就得存个新文件时间长了分不清谁是最新。后来我改成文本化优先先用 Markdown 的标题层级把整棵树写出来H2 是主干、H3 是分支、列表项是叶子然后用一类能把 Markdown 直接渲染成导图的轻量工具生成图片代码仓库里只存那份 Markdown。这样做的好处是每次修改都能看 diff谁在什么时候加了哪个节点一清二楚团队协作也不会出现“你改你的我改我的”最后合并出一堆冲突文件。渲染出来的图只用于分享和打印真正的知识载体是那份文本。我在文本里还加了约定每个叶子节点后面用括号标难度档位必背、会用、知道这样导出图片时能一眼看出重点。排版上尽量不用长句一个节点不超过二十个字超过就再拆一层。刚开始画的时候会觉得拆得太碎画到第三版你就会感谢自己因为拆碎之后每个节点都能独立检索。7.2 命名规范和维护节奏决定这张图能用多久命名规范听起来是小事实际上决定导图能不能长期用。我的规则是主干用“问题式短语”比如“数据怎么不丢”分支用“类别名词”比如“持久化”“内存管理”叶子用“动作 对象”比如“用 UNLINK 异步删大 key”。坚决不写“其他”“杂项”这种垃圾桶节点一旦出现就说明你的分类有问题宁可再开一层。另外同一层级的节点尽量保持同一词性别一半是动词短语一半是名词看着别扭回忆的时候也容易串。维护节奏上我给自己定的规矩是每季度过一遍重点看三类内容版本变化导致失效的、线上新踩的坑、以及团队新人问得最多的问题。第三类是宝藏因为新人问的问题往往就是导图上表达不清的地方。我还会在图的最外层挂一个“待验证”分支看到不确定的说法先扔进去等自己在测试环境验证过再挪到正式节点。这个习惯救过我很多次网上流传的不少“最佳实践”其实是特定版本的特定场景直接抄进图里会误导人。8. 常见问题与排查速查8.1 我实际遇到过的高频问题第一个是内存莫名增长。排查顺序我在图上写死了先看INFO memory里的used_memory和used_memory_rss的比值碎片率高就考虑开activedefrag再看是不是有大量过期 key 没被清掉看expired_keys的增长曲线再看是不是编码转换导致的内存翻倍最后才怀疑业务写入。这个顺序能挡掉八成的误判很多人一上来就怀疑业务代码结果查了两天发现是碎片。第二个是主从延迟突然变大。先看INFO replication里的master_repl_offset和从节点的slave_repl_offset差值差值持续扩大说明从节点跟不上。原因可能是从节点执行了慢命令、网络带宽不够、或者主节点写入太猛。我在图上标了一个容易被忽略的点从节点上跑KEYS、SORT、大范围ZRANGE这类命令会直接拉高复制延迟因为从节点回放也是单线程的。第三个是集群某次扩容后出现间歇性超时。大概率是槽迁移期间客户端还在用旧的路由表收到MOVED之后没及时刷新缓存也可能是迁移期间大量ASK重定向放大了延迟。处理办法是客户端开启拓扑刷新迁移尽量在低峰做控制单次迁移的 key 数量。现象第一排查点常见原因内存持续上涨INFO memory碎片率编码转换、过期未清、大 key写入报错maxmemory-policy淘汰策略为 noeviction从节点数据落后复制偏移量差值从节点慢命令、带宽不足间歇性超时慢查询日志大 key、集群重定向、阻塞命令快照失败rdb_last_bgsave_status磁盘空间、fork 失败8.2 我给自己定的几条实操心得慢查询日志一定要提前开slowlog-log-slower-than默认是 10000 微秒也就是 10 毫秒我一般在核心业务里调到 5000 微秒甚至更低因为 Redis 是单线程执行命令一条 10 毫秒的命令已经足够把整体吞吐拉下来了。日志本身占内存很小slowlog-max-len留个几百条足够回溯。别等到出问题才开那时候现场已经过去了。用Pipeline批量操作的时候注意批次大小我试过一次性塞几千条命令结果单个包太大网络层反而变慢还容易把服务端输出缓冲区顶爆。一般几百条一批比较稳具体看命令大小。另外Pipeline不等于原子中间插了别的客户端命令是完全可能的需要原子性就老老实实用 Lua。分布式锁这块导图上我只保留了最稳的那套SET key value NX PX 毫秒数value 放一个唯一标识释放的时候用 Lua 脚本比对标识再删避免误删别人的锁。业务执行时间不确定就加续期但续期本身要处理进程挂掉的情况。锁的过期时间、续期间隔、最大持锁时长这三个参数必须一起想清楚只设过期不续期会被提前释放只续期不设上限会在异常时永久占锁。Lua 脚本要短。脚本执行期间整个实例是阻塞的我见过有人把一段循环上千次的逻辑塞进 Lua执行几百毫秒直接把一个分片的 QPS 打到底。脚本里也不要做随机性操作之外的非确定性写入复制和持久化都可能出问题。写完脚本在测试环境用SCRIPT DEBUG或者直接压测看耗时超过几毫秒就该重新设计。最后分享我自己定的一条自检线每当我在导图上新增一个节点我都会问自己三个问题——这个节点解决什么现象、对应的命令或者参数是什么、用错了会有什么后果。三个问题答不全说明这个节点还没想清楚先扔到“待验证”分支去。这张图到现在还在改很多节点是我在线上踩过坑之后才补上去的它更像一份个人排障履历而不是一份教材。你也一样从自己遇到过的第一个问题开始画半年之后回头看会发现这条线索比任何一份现成的资料都更贴合你的实际工作。
阅读完成 · 觉得有帮助?