
做技术这几年我越来越有个体会只会用工具的人遇到问题只能靠猜懂底层原理的人遇到问题能直接定位。今天想聊的“进阶技巧与底层原理”其实可以用 Redis 作为一条主线来讲透。为什么选 Redis因为它是目前后端系统里几乎绕不开的组件缓存、分布式锁、限流、排行榜、消息队列都在用但很多人用了一两年还停留在 get/set 的阶段遇到大 key、热 key、持久化丢数据、集群扩容卡顿这类问题时往往一头雾水。这篇文章不是入门教程而是围绕那些“命令会用但不知道为什么”的场景展开适合已经用过 Redis、想进一步理解它底层工作机制的同学。1. 为什么说“进阶技巧”必须建立在“底层原理”之上1.1 从一次线上事故说起不懂原理的代价去年帮一个朋友排查线上问题过程印象很深。他们的系统每到整点就会卡顿几秒缓存命中率从 95% 掉到 40% 左右数据库压力直接被打满。一开始大家都以为是流量突增加机器、加带宽全都没用。后来我让他们打开SLOWLOG一看发现很多keys *命令每次执行都会扫描全量键赶上整点定时任务一跑Redis 直接卡住后面所有请求全部排队。这个问题的根源其实不是命令写错而是调用方完全不了解keys *在底层是 O(N) 的全表遍历执行期间会阻塞单线程的事件循环其他正常请求全部得不到响应。这类事故其实特别常见。另一个高频事故是把 Redis 当数据库存了大量 key 之后RDB 持久化时fork子进程导致主线程卡顿。我们当时就遇到过实例内存 20G写入频率很高RDB 每次落地都要卡 2 到 3 秒一开始以为机器性能不行后来查资料才发现fork需要复制父进程页表内存越大、写入越频繁copy-on-write 触发得越厉害卡顿就越明显。这两件事给了我一个很深的教训进阶用法不是背几个命令参数而是要理解命令背后到底做了什么不然线上出了事故你连排查方向都是错的。1.2 原理、技巧与场景的三角关系打个比方开车。驾校教的是踩油门、打方向、刹车这叫基础操作。进阶技巧是什么是知道什么时候该降档超车、什么时候该提前松油门滑行、下长坡时怎么用发动机减速。但如果你完全不懂发动机和变速箱的配合原理这些技巧就只能靠死记硬背换个路况就不会用了。Redis 也是一样基础操作就是get、set、expire进阶技巧是知道什么场景该用zset而不是list、什么时候该开混合持久化、什么时候该拆 key。而所有这些技巧的取舍最后都要回到几个底层原理上数据结构、内存分配、IO 模型。我自己的经验是学习底层原理不是为了让你去重写一个 Redis而是让你在选型、排障、调优的时候有判断依据。比如你知道了 Redis 是单线程事件循环就会明白为什么keys *、hgetall、lrange这类 O(N) 命令要慎用你知道了 ziplist 是小哈希的特殊编码就会明白为什么少量字段的 hash 比 string 拼接更省内存你知道了 RDB 是 fork 子进程做快照就会明白内存越大 fork 卡顿风险越高。这些都不是靠死记硬背能背下来的而是原理一层层推出来的。所以这篇文章的所有内容都会先讲底层机制再讲具体怎么用。2. 底层数据结构从 SDS 到跳表选型背后的原理2.1 字符串的真相SDS 不是 C 字符串很多人以为 Redis 的 String 就是 C 语言里的char*这是一个很大的误解。C 字符串以\0结尾获取长度需要遍历整个字符数组而且中间不能包含空字符想存二进制数据很容易被截断。Redis 为了高性能自己封装了一个数据结构叫 SDSSimple Dynamic String简单动态字符串。SDS 的结构大致是len alloc flags buf[]其中len直接记录了字符串长度所以获取长度是 O(1)buf里存真实数据允许中间出现\0所以它是二进制安全的存图片、序列化对象都没问题。SDS 还有一个很重要的优化叫空间预分配。当你要append一段内容导致字符串扩容时SDS 不仅会分配这次需要的内存还会额外预留一部分空间这样下次append时就可以直接写入而不用再次触发系统调用。这个优化听着不起眼但在高频写入场景下很关键因为内存分配是耗时操作能省一次是一次。实操上的启发是什么第一不要用 string 去存超大的数据块。SDS 再优化单 key 数据过大也会带来内存分配和网络传输的压力一般建议单个 string 控制在几 KB 到几十 KB再大就该考虑拆分成 hash 或者走对象存储。第二学会用SETBIT、GETBIT做位图。比如在线用户状态、用户签到记录一个用户一天只需要一个 bit1000 万用户也就 1.2MB 左右。这就是利用字符串底层是字节数组这个特性做出来的进阶技巧不懂底层原理根本想不到还能这么玩。2.2 哈希、列表、有序集合紧凑编码与跳表的选择逻辑再来说说集合类数据结构。Redis 的 hash 在字段少、值小的时候会用 ziplist新版本里逐渐演进为 listpack字段多、值大的时候会切换成 hashtable。ziplist 本质上是一段连续内存把所有字段名和字段值紧凑地排在一起好处是内存占用极低、缓存命中率高坏处是插入删除可能引发内存搬移。所以当你确定一个 hash 字段数量很少时用它非常省内存但如果一个 hash 膨胀成 big key频繁更新字段就会带来性能问题这点后面性能排查部分还会展开。list 也有类似的设计早期是 ziplist 加 linkedlist 的组合后来改成了 quicklist再后来又出现了 listpack。quicklist 的思路是把数据切成多个节点每个节点内部仍然使用压缩编码既保留连续内存的优点又避免单一大链表带来的内存碎片问题。zset 则更有意思它同时使用了 dict 和 skiplist 两种结构dict 用来根据 member 以 O(1) 时间复杂度查分数skiplist 用来按分数排序和范围查询。很多人问为什么不直接用平衡树因为跳表实现更简单、调试更方便而且范围查找的效率和平衡树差距不大对 Redis 这种追求极致简单的系统来说更合适。理解了这些底层结构你才会知道有些“高级技巧”为什么是这么设计的排行榜用 zsetskiplist 天然支持按分数范围取数据ZREVRANGE可以直接取 Top N。对象存储用 hash字段数量小时走压缩编码内存比存 JSON 字符串低很多。列表做消息队列要小心生产者用LPUSH消费者用BRPOP但 list 过长时节点链表会很长性能和内存都可能不理想建议按业务维度拆分 key。还有几个内存相关的参数值得关注比如hash-max-listpack-entries、list-max-listpack-size、zset-max-listpack-entries。默认值在不同版本里略有差异你可以根据业务字段数量去调整。比如你知道一个 hash 最多 50 个字段每个字段值都很小就可以把hash-max-listpack-entries调大一点让更多 hash 保持紧凑编码内存能省不少。但注意别调得太大否则插入删除时的内存搬移代价会盖过收益。3. 持久化与可靠性RDB、AOF 到底怎么选3.1 RDB 与 AOF 的底层机制Redis 是内存数据库所有数据默认只在内存里进程一旦退出数据就没了。所以要保证数据可靠必须开启持久化。RDB 是生成一份二进制快照文件底层机制是主进程fork出一个子进程子进程把内存里的全量数据写入临时文件写完后 rename 成正式的 dump.rdb。fork的时候用到了操作系统的 copy-on-write 机制子进程刚 fork 出来时和父进程共享同一份内存只有父进程后续修改了某个内存页才会复制那个页。这个机制的优点是子进程写快照时不会阻塞主进程的读操作缺点是如果写频率很高COW 会复制大量内存页加上fork本身要复制页表内存越大、fork 越频繁卡顿风险就越高。AOF 则是另一种思路把每次写命令追加到文件末尾重启时通过重放这些命令来恢复数据。AOF 的可靠性取决于appendfsync的配置always每次写都调用 fsync最安全但性能最差everysec每秒落盘一次性能和安全折中no交给操作系统决定性能最好但可能丢数据。生产环境一般建议everysec极端情况下最多丢一秒数据。AOF 还有一个问题时间长了文件会越来越大所以需要 AOF rewrite 机制把内存里的当前状态重新生成一份最小的命令集合避免历史命令无限堆积。给你一份我在用的基础配置作为参考# RDB 触发策略900秒内1个key变化 / 300秒内10个key变化 / 60秒内10000个key变化 save 900 1 save 300 10 save 60 10000 # AOF 配置 appendonly yes appendfsync everysec # 混合持久化 aof-use-rdb-preamble yes3.2 混合持久化与备份恢复避坑很多团队只开 RDB或者只开 AOF其实各有各的坑。只开 RDB两次快照之间的数据一旦宕机就全丢了。比如你配置 900 秒内至少 1 个 key 改变才触发一次快照那可能 15 分钟内所有新写入的数据在宕机后都没了。只开 AOF 的话如果appendfsync是everysec最多丢一秒如果磁盘性能差AOF 写入可能成为性能瓶颈。Redis 4.0 之后提供了混合持久化通过aof-use-rdb-preamble yes开启。它会先以 RDB 格式写入当前内存快照再把快照之后的新命令以 AOF 格式追加在后面。这样重启时先加载 RDB 再重放增量命令恢复速度比纯 AOF 快数据安全性又比纯 RDB 好。我在生产环境基本都是这个配置RDB 做冷备AOF 做热恢复两者都开AOF 用everysec。如果你的业务对数据丢失零容忍那就要考虑always或者引入其他层面的同步方案而不是指望 Redis 单点解决。实操上还有一个很容易被忽略的点重启 Redis 时如果 AOF 文件存在Redis 会优先用 AOF 恢复数据而不是 RDB。但很多人定期备份只拷贝了 dump.rdb却没同时备份 appendonly.aof结果恢复的时候发现数据是旧的这就是因为没搞清楚两者的优先级。备份策略建议 RDB 和 AOF 都保留而且一定要定期做恢复演练别等到真出故障了才发现备份文件是坏的或者格式不兼容。真实环境里我见过有人恢复时直接 load 一个损坏的 AOFRedis 直接拒载最后只能靠前一天晚上手动拷贝的 RDB 顶着损失了一整天的数据。提示修改 AOF 相关配置时最好先确认磁盘 IO 能力。如果磁盘本身性能一般appendfsync always会让写入吞吐下降一个量级不一定适合所有业务。4. 高可用与扩缩容主从复制和集群的底层逻辑4.1 主从复制全量同步、增量同步与 backlog 调优主从复制看起来就是配置一个replicaof地址但底层有一套完整的协议。最开始从节点会向主节点发送PSYNC命令带上自己的 replication ID 和 offset。如果是第一次同步主节点会触发全量同步先 fork 子进程生成 RDB 快照同时把生成 RDB 期间的写命令写入复制积压缓冲区repl_backlog然后把这部分数据都发给从节点。从节点加载完 RDB 后再接收 backlog 里的增量命令两边就达到一致了。理解这个流程对排查问题很重要。全量同步是很重的操作生成 RDB 要 fork、要写文件、要传输主节点压力会明显上升。如果从节点经常断连重连后又触发全量同步主节点很容易被打垮。这个时候你可以调大repl-backlog-size默认是 1MB让增量同步能覆盖更长的断连时间避免频繁全量同步。我见过一个案例从节点因为网络抖动断开几秒由于 backlog 太小重连后直接走了全量同步主节点在高峰期 CPU 暴增最终影响了线上读写。从节点默认是只读的但你可以用它做备份、做慢查询分析、做异步计算甚至临时挂一个新的从节点来替代问题实例。做故障转移的时候最好选一个数据最接近主节点的从节点来提升判断依据就是看它的master_repl_offset跟主节点差多少差距越小越优先。很多自动选主脚本只检查存活不检查 offset结果切过去丢了一堆数据这个坑在实际运维里真的非常常见。4.2 Redis Cluster槽位、hash tag 与重分片实践集群模式下数据不是随机分布的而是按 key 做 CRC16 校验对 16384 取模得到一个槽位每个节点负责一部分槽。客户端访问某个 key 时先计算槽位再路由到对应节点。如果 key 不在当前节点节点会返回 MOVED 错误里面带了正确的节点地址智能客户端会缓存这个路由信息下次直接访问正确节点。这个槽位机制是理解集群很多问题的钥匙。比如为什么集群模式下keys *不能用了因为 key 分散在不同节点没有哪个节点能返回全量数据。为什么多 key 操作有那么多限制因为不同 key 可能在不同节点上跨节点的MGET、事务、Lua 脚本无法保证原子性只能借助 hash tag 把相关的 key 强制放到同一个槽位。hash tag 的用法是 key 里加花括号比如{user:1}:profile和{user:1}:ordersCRC16 只对花括号里的内容计算所以这两个 key 会落到同一个节点。# 查看某个 key 的槽位和节点 redis-cli -c -h 127.0.0.1 -p 7000 CLUSTER KEYSLOT user:10086 redis-cli -c -h 127.0.0.1 -p 7000 CLUSTER NODES集群扩容和缩容时核心操作是重新分片reshard把一部分槽从源节点迁移到目标节点。迁移过程并不是瞬间完成的而是一个槽一个槽地搬。搬槽的时候旧节点可能还在服务请求客户端会根据返回的 ASK 错误跳转到目标节点。如果单个 key 太大迁移会变得非常慢甚至导致集群卡顿所以在使用集群之前必须严格控制最大 key 的尺寸。我见过有人往集群里塞了一个 500MB 的 hash重分片时那个槽迁移了 20 多分钟期间相关读写全部超时。解决方法是提前用--bigkeys扫描把大 key 拆掉再扩容。5. 性能排查与调优大 key、热 key 和慢查询5.1 大 key 和热 key 的定位与处理大 key 和热 key 是 Redis 运维里最常遇到的两类问题。大 key 指的是单个 key 的数据量过大比如一个 hash 有上百万字段一个 list 有几千万元素一个 string 有几十 MB。大 key 的危害是多方面的读写时内存复制开销大可能阻塞单线程持久化时容易造成主从延迟和 fork 卡顿集群迁移时拖慢整个 reshard。定位大 key 可以用redis-cli --bigkeys它内部用scan遍历所有 key统计每种数据类型里最大的几个 key。这个命令比keys *安全一些但如果实例特别大遍历过程也会消耗一定资源建议在低峰期执行。热 key 是某个 key 的 QPS 特别高比如爆款商品、热点新闻大量请求猛打同一个 key可能把单节点 CPU 打满。定位热 key 相对麻烦一些常见手段有用redis-cli --hotkeys需要开启相关配置不同版本支持度不一样或者用MONITOR命令观察。但MONITOR在线上会显著降低 Redis 性能一定要慎用。更好的方案是在客户端侧做访问统计或者用代理层做热点探测。处理大 key 的做法核心思路是拆。一个 big hash 可以按时间、按用户 ID 取模拆成多个小 hash一个 big list 可以按业务类型拆成多个 list一个大的 string 如果存的是 JSON可以拆成多个字段用 hash 保存或者压缩后再存。处理热 key 的做法最常见的是加本地缓存把热 key 的内容在应用服务器内存里缓存几十秒直接减少对 Redis 的访问其次是做副本读把热 key 的访问分散到多个从节点再次是做 key 的动态分散比如把同一个 key 的读请求随机改写为key:0到key:9再在应用层合并。5.2 慢查询与延迟异常排查Redis 的慢查询日志是排查性能问题的第一站。你可以通过配置slowlog-log-slower-than指定阈值单位是微秒超过阈值的命令会被记录到内存里用SLOWLOG GET查看。重点看几个字段命令名称、执行耗时、命令参数、执行时间。如果大量慢查询集中在某个命令上比如hgetall、lrange、smembers那基本可以判定对应的 key 太大了如果慢查询是各种命令随机出现那要怀疑是 Redis 自身卡顿。# 配置慢查询阈值并查看最近 20 条慢日志 CONFIG SET slowlog-log-slower-than 10000 SLOWLOG GET 20Redis 自身卡顿的原因很多常见的有fork 阻塞内存大、写入频繁、AOF 刷盘导致磁盘 IO 抖动、内存碎片率高导致内存分配慢、系统开启了大页内存导致 fork 后 COW 放大等。Redis 4.0 之后提供了 latency monitor通过CONFIG SET latency-monitor-threshold 100开启然后执行LATENCY LATEST查看最近的事件。还有一个很有用的命令是INFO commandstats它会统计每种命令的调用次数和总耗时你可以算平均耗时快速发现那些调用频率不高但耗时极高的“隐形杀手”。线上巡检时INFO里几个指标要养成定期看的习惯connected_clients、used_memory、mem_fragmentation_ratio、evicted_keys、rejected_connections。如果mem_fragmentation_ratio长期大于 1.5说明内存碎片率偏高考虑低峰期重启节点或者用memory purge整理。如果evicted_keys一直在增长说明缓存淘汰策略已经频繁触发要么是容量不够要么是 key 的 TTL 设置不合理。可以把这些指标接到监控系统里设置阈值告警别等用户反馈才去看。6. 常见问题速查与实测避坑下面这张表是我在真实项目里遇到过的问题和排查经验不一定覆盖所有场景但遇到类似情况时可以先对着检查一遍。问题现象底层原因解决方案线上执行keys *后延迟飙升keys *是 O(N) 全量遍历阻塞单线程事件循环用scan分批遍历生产环境禁用keys开启 RDB 后定期出现卡顿fork 复制页表 copy-on-write 复制内存页控制单实例内存、调整save策略、低峰期手动触发只开了 AOF重启后数据不对AOF 文件损坏或未正确配置 fsync 策略同时开启 RDB AOF定期备份并做恢复演练主从断连后频繁全量同步repl-backlog-size太小增量同步覆盖不了断连窗口调大 backlog监控master_repl_offset差距集群扩容时 key 迁移超时大 key 导致单个槽迁移过慢先拆大 key再执行 reshard热点 key 把单节点 CPU 打满请求集中在单一 key单线程模型无法并发处理加本地缓存、做副本读、动态分散 key内存碎片率长期偏高频繁更新大对象不同大小内存分配交错低峰期重启节点、调整 jemalloc 相关配置、拆分 key有些问题看起来是 Redis 的问题但查到最后其实是使用姿势的问题。比如cache penetration导致大量请求打到数据库表面上是 Redis 缓存缺失本质上是没有做空值缓存或者布隆过滤器再比如cache avalanche导致某个时间点大量 key 同时过期表面上是 Redis 出问题本质上是过期时间设置完全一样导致的。这类问题不属于 Redis 本身的缺陷但如果你熟悉 Redis 的过期机制和单线程模型就很容易猜到原因然后通过给 TTL 加随机值、做多级缓存等手段规避。还有一个实践细节值得多说一句不要过度依赖 Redis 的淘汰机制来“自动清理”大 key。volatile-lru、allkeys-lru这些淘汰策略确实能控制内存上限但大 key 被淘汰时如果数据结构很复杂释放内存也可能卡顿。所以更好的做法是在写入端就控制 key 的大小和数量设置合理的 TTL定期扫描清理无用数据而不是把淘汰策略当成万能药。写到最后说点个人体会。我见过不少同学收藏各种 Redis 面试题和实战技巧但问到底层为什么往往答不上来。其实进阶技巧不是靠背就能学会的它是在踩坑、看源码、做实验的过程中长在自己身上的东西。我自己推荐两个笨办法一是搭一个本地 Redis把配置文件里的每一个参数都查一遍文档改一改、压一压观察性能变化二是有空读一读源码不用全读把 sds、dict、ziplist 这几个核心数据结构看明白很多原理就是一层窗户纸。这篇文章把数据结构、持久化、主从复制、集群、性能排查这几个方向都过了一遍每个方向展开都是一个大块内容你可以挑自己最近踩过坑的方向深入研究下去。用 Redis 这么多年我的感觉是它看起来简单但真正用得好的人一定是对它底层足够敬畏的人。