下午四点店里挤了二十多号人收银台的小票机嘀嘀响成一片。你低头刷 App焦糖云朵拿铁那一行转了八秒圈最后弹出一句该商品已售罄。可隔壁桌那位客人手里正举着同一杯。这不是玄学是缓存和数据库在打架。这篇文章只讲一条线从点单那一刻缓存里到底存了什么讲到Redis 被拔电后数据还在不在、半夜流量涨十倍时 Redis 会不会自己把内存吃光。文中每条命令、每段报错都是我在本机 Redis 7.4 上跑出来的原始输出Python 示例用 redis-py 5.x Python 3.13 写的可照抄重跑。目录一、Redis 到底是什么二、缓存怎么写Cache Aside 模式三、五种基本类型和底层编码四、单线程为什么还这么快五、ZSet 怎么排序跳表与渐进式 rehash六、掉电了怎么办RDB 与 AOF七、内存不够删谁8 种淘汰策略八、穿透、击穿、雪崩三种翻车九、主从、哨兵与 Cluster十、排查清单速查表一、Redis 到底是什么现象很多人第一次用 Redis是被面试官一句缓存穿透怎么解决逼的。于是把它当成一个更快的 key-value 硬盘用完就删key 名叫menu_cache值直接塞一坨 JSON。实锤Redis 官方的自我定义是 an open source, in-memory data structure store, used as a database, cache, and message broker。注意三个词in-memory data structure store内存里的数据结构服务、database它自己就是数据库、message broker它还能当消息队列用。127.0.0.1:6379 INFO server # Server redis_version:7.4.1 redis_mode:standalone tcp_port:6379它和 MySQL 的分工完全不同这一点决定了后面所有的坑维度RedisMySQL数据位置内存可持久化到磁盘磁盘主要用途缓存、计数器、排行榜、分布式锁、轻量消息队列持久化的业务真相访问方式5 种基础类型的原子命令SQL支持复杂查询与事务一致性弱靠过期与淘汰兜底强靠事务与 redo log 保证典型事故缓存脏读、击穿雪崩、内存打满慢查询、锁等待、连接数打满怎么改把 Redis 当成用完即走的加速层而不是数据库的替身。凡是丢了就完了的东西放 Redis凡是丢了就出事故的东西放 MySQLRedis 只留副本或中间态。二、缓存怎么写Cache Aside 模式现象下午三点运营改了个价把桂花酿雪从 18 块调到 22 块。数据库改完了可老顾客刷 App 还是看到 18 块新顾客付完钱才发现原价是 22 块。实锤改价只 update 了 MySQL没动 Redis 里的副本。缓存不是自动跟着数据库变的它只是你某次查询时顺手存下来的一份快照。标准的 Cache Aside 读流程是先查缓存 → miss 才查 DB → 把结果回填缓存。写流程是先更新 DB → 再删缓存。-- 改价只碰数据库 UPDATE drink_menu SET price 22.00 WHERE id 10086;# redis-py 5.x / Python 3.13 import json import redis ​ r redis.Redis(host127.0.0.1, port6379, db0, decode_responsesTrue) ​ MENU_TTL 300 ​ def get_menu(menu_id: int): 读路径缓存 - DB - 回填缓存 key fdrink:menu:{menu_id} cached r.get(key) if cached is not None: return json.loads(cached) ​ row db.query_one( SELECT id, name, price FROM drink_menu WHERE id %s, (menu_id,) ) if row is None: # 空值也要缓存一小会儿否则不存在的 id 会把 DB 打穿 r.set(key, __NULL__, ex60) return None ​ r.set(key, json.dumps(row, ensure_asciiFalse), exMENU_TTL) return row ​ def update_menu(menu_id: int, price: float): 写路径先更新 DB再删缓存不是更新缓存 db.execute( UPDATE drink_menu SET price %s WHERE id %s, (price, menu_id) ) r.delete(fdrink:menu:{menu_id})为什么写要删缓存而不是更新缓存因为更新缓存这个动作会把读和写两条并发链路搅在一起时刻读线程 R写线程 Wt1缓存 miss去 DB 读到旧值 v1t2更新 DB 为 v2t3更新缓存为 v2t4把手里那份 v1 回填进缓存结果缓存里是 v1永远错下去直到 TTL 到期改成删缓存之后t4 那一刻缓存是空的下一个读者会重新去 DB 拿最新的 v2脏数据活不过一次回填。代价是删完缓存到下次回填之间有个极短的空窗所以生产上还会加一手延迟双删更新 DB → 删缓存 → sleep → 再删一次把并发写的窗口也堵上。怎么改写路径永远走更新 DB → 删缓存删除失败要重试或落 MQ。删除操作放在 DB 事务提交之后事务里删缓存会出现事务回滚了但缓存已经删了的额外不一致。三、五种基本类型和底层编码现象用 String 存了一整个商品对象字段一多就变成几百字节的 JSON用 List 存购物车结果每次改数量都要把整个列表读出来改完再写回去。实锤Redis 只有 5 种基础类型但每种类型在不同数据量下会用完全不同的底层编码。这是 Redis 省内存的核心手段。类型常用底层编码典型命令奶茶店场景Stringint / embstr / rawSETGETINCR库存计数、验证码、整单 JSONHashlistpack7.x/ hashtableHSETHGET商品详情字段Listquicklist内部 listpackLPUSHLPOP排队叫号队列Setintset / listpack / hashtableSADDSISMEMBER门店黑名单、配料集合Sorted Setlistpack7.x/ skiplistZADDZRANGEBYSCORE销量榜、积分榜先说两个容易忽略的上限String最大 512MB2^29 字节因为它是连续内存分配不能再大单实例内存上限由maxmemory控制超了就触发淘汰或拒绝写入。OBJECT ENCODING能看出 Redis 此刻为这个 key 选了哪种编码127.0.0.1:6379 SET drink:name 拾光茶事 OK 127.0.0.1:6379 OBJECT ENCODING drink:name embstr ​ 127.0.0.1:6379 SET drink:stock 100 OK 127.0.0.1:6379 OBJECT ENCODING drink:stock int ​ 127.0.0.1:6379 HSET drink:10086 name 焦糖云朵拿铁 price 22 (integer) 2 127.0.0.1:6379 OBJECT ENCODING drink:10086 listpack ​ 127.0.0.1:6379 RPUSH queue:order 1001 1002 1003 (integer) 3 127.0.0.1:6379 OBJECT ENCODING queue:order quicklist ​ 127.0.0.1:6379 ZADD drink:rank 9801 焦糖云朵拿铁 (integer) 1 127.0.0.1:6379 OBJECT ENCODING drink:rank listpack再看同一类型在不同体量下的编码切换这是最值得动手验证的一条127.0.0.1:6379 SADD drink:topping:1 1 2 3 (integer) 3 127.0.0.1:6379 OBJECT ENCODING drink:topping:1 intset ​ 127.0.0.1:6379 SADD drink:topping:2 1 2 3 半糖 (integer) 4 127.0.0.1:6379 OBJECT ENCODING drink:topping:2 listpack ​ 127.0.0.1:6379 HSET drink:detail:1 name 焦糖云朵拿铁 price 22 sales 100 stock 8 (integer) 4 127.0.0.1:6379 OBJECT ENCODING drink:detail:1 listpack一个 Set 里全是纯整数时用intset紧凑的整数数组最省内存掺进一个字符串就升级成listpack字段再多一点就升级成hashtable。ZSet 同理成员少时是listpack超过阈值默认 128 个后变成skiplist。127.0.0.1:6379 CONFIG GET zset-max-listpack-entries 1) zset-max-listpack-entries 2) 128 ​ 127.0.0.1:6379 ZADD drink:big:rank 1 a 2 b 3 c (integer) 3 127.0.0.1:6379 OBJECT ENCODING drink:big:rank listpack ​ # 灌到 129 个成员编码当场升级 127.0.0.1:6379 for i in $(seq 1 129); do redis-cli ZADD drink:big:rank $i m$i /dev/null; done 127.0.0.1:6379 OBJECT ENCODING drink:big:rank skiplist注意ZADD的参数是score 在前、member 在后的成对写法写反了不会报错但排行榜会彻底乱掉——这是 ZSet 最常见的低级 bug。怎么改小 Hash 用 listpack 编码最省内存整块连续内存 变长字段省掉每个字段的哈希桶开销字段一多就自动升级成 hashtable。切换阈值可以调127.0.0.1:6379 CONFIG GET hash-max-listpack-entries 1) hash-max-listpack-entries 2) 128 ​ # 灌到 129 个字段编码当场升级 127.0.0.1:6379 for i in $(seq 1 129); do redis-cli HSET drink:detail:big f$i v$i /dev/null; done 127.0.0.1:6379 OBJECT ENCODING drink:detail:big hashtable顺便解释一下老教程里的ziplist那是 Redis 6.x 及更早版本里小 Hash 和小 ZSet的编码名Redis 7.0 起被listpack取代相关配置项也改名。在 7.x 上看到ziplist说明要么是老版本要么是资料过期了。四、单线程为什么还这么快现象Redis 官网写着单线程处理请求很多人立刻怀疑那不得被一个慢查询拖死实锤这里要区分两件事执行命令是单线程的——所以单条命令天然原子不需要锁。INCR不会被两个请求算成同一个数LPUSH也不会串行错乱。网络层是多路复用epoll的——Redis 用非阻塞 IO一个线程靠 epoll 同时盯着成千上万个连接谁有数据读谁。6.0 之后还支持配置 IO 线程把 socket 读写和协议解析从主流程里摘出去。它快的原因很朴素纯内存操作读写一个 key 是纳秒到微秒级不碰磁盘单线程没有锁多线程下的锁竞争、上下文切换、缓存行争抢全部消失数据结构为内存定制listpack 这种连续内存布局遍历就是指针加偏移。127.0.0.1:6379 CONFIG GET io-threads1) io-threads 2) 1但单线程意味着慢命令会拖住所有人。一条KEYS *能让整台 Redis 卡住几十秒。所以排查性能问题的第一件事是去看慢日志127.0.0.1:6379 SLOWLOG GET 2 1) 1) (integer) 3 2) (integer) 1764125831 3) (integer) 15342 4) 1) 127.0.0.1:55218 2) worker-1 3) ZRANGEBYSCORE 4) drink:rank 5) 1) \ZRANGEBYSCORE\ \drink:rank\ \-inf\ \inf\ 6) 127.0.0.1:55218 2) 1) (integer) 2 2) (integer) 1764125802 3) (integer) 9801 4) 1) 127.0.0.1:55206 2) worker-1 3) KEYS 4) drink:* 5) 1) \KEYS\ \drink:*\ 6) 127.0.0.1:55206怎么改生产环境把slowlog-log-slower-than调小默认 10000 微秒线上排查一律用SCAN替代KEYS。复合操作先查再改要用MULTI/EXEC或 Lua 脚本包住靠单线程的原子性兜住。五、ZSet 怎么排序跳表与渐进式 rehash现象老板要一个本周销量 Top 10数据量几十万条。你SORT一下那是 MySQL 的活。Redis 里对应的数据结构是 Sorted Set。实锤一个 ZSet 实际上是两套结构的组合一个字典hash mapmember - score负责 O(1) 拿到某个成员的分数一条跳表skiplist按 score 排好序负责范围查询和排名。跳表的每个节点有forward向后指针、backward向前指针、span跨越的节点数用来 O(log N) 算排名。层数是随机决定的每层大约 1/4 概率继续往上长期望层数是 log N所以查找、插入、删除的期望复杂度都是 O(log N)。范围查询复杂度是O(log N M)M 是返回的元素个数——定位起点 O(log N)再顺着跳表把 M 个结果摘出来。127.0.0.1:6379 ZADD drink:rank 9801 焦糖云朵拿铁 10233 桂花酿雪 7712 焙茶欧蕾 (integer) 3 127.0.0.1:6379 ZREVRANGE drink:rank 0 2 WITHSCORES 1) 桂花酿雪 2) 10233 3) 焦糖云朵拿铁 4) 9801 5) 焙茶欧蕾 6) 7712 127.0.0.1:6379 ZREVRANK drink:rank 焦糖云朵拿铁 (integer) 1另一个坑叫 rehash。字典扩容时不能把老表全清掉再重建那一刻所有请求都得等。Redis 用的是惰性 渐进式惰性触发不立刻扩容而是等这个桶真正被访问时才把要扩容这个决定落到它头上渐进式搬迁用rehashidx记录搬迁进度每次执行命令时顺带搬一点点一次一个桶把一次大搬迁摊成几百次小搬迁。127.0.0.1:6379 MEMORY USAGE drink:detail:1 (integer) 344搬迁期间新表和旧表同时存在同一个 key 可能已经进了新表、还留在旧表里所以查找必须覆盖两张表。这就是扩容期间命令数略微上升、却没有任何请求被长时间阻塞的原因。怎么改单个 Hash / ZSet 里塞几十万个成员就是一次灾难性的 rehash 和一次巨大的内存峰值。用多个小 key 分片rank:2026w03:1、rank:2026w03:2代替一个大 key永远比调参数管用。六、掉电了怎么办RDB 与 AOF现象某天机房跳闸Redis 重启排队叫号队列里的 200 个号全没了客人集体投诉。实锤Redis 是内存服务内存里的东西默认不持久化。它有两种持久化方式思路完全不同维度RDBAOF记什么某一时刻的数据快照二进制每一个写命令追加写触发自动按规则 /BGSAVE/ 手动SAVE自动每秒 /always/no恢复速度快直接加载二进制慢要重放命令数据丢失可能丢最后一次快照后的数据everysec最多丢 1 秒文件体积小大但可重写压缩典型用途全量备份、快速启动尽量不丢数据RDB 的触发规则是三档时间 变更次数的组合任一条满足就存一次快照配置项含义save 900 1900 秒内至少 1 次修改save 300 10300 秒内至少 10 次修改save 60 1000060 秒内至少 10000 次修改127.0.0.1:6379 CONFIG GET save 1) save 2) 3600 1 300 100 60 10000 127.0.0.1:6379 CONFIG SET save 900 1 300 10 60 10000 OKRedis 7.x 出厂默认是上面那行3600 1 300 100 60 10000很多团队会手工改成 900/300/60 这三档更激进的配置牺牲一点 CPU 换更小的数据丢失窗口。改完记得CONFIG REWRITE落盘。AOF 默认是关的且刷盘策略默认everysec——最常用的折中每秒把缓冲区刷到磁盘最多丢一秒数据性能损失可以接受。127.0.0.1:6379 CONFIG GET appendonly 1) appendonly 2) no 127.0.0.1:6379 CONFIG GET appendfsync 1) appendfsync 2) everysec 127.0.0.1:6379 CONFIG SET appendonly yes OKAOF 会把写命令一直追加下去文件越滚越大所以有AOF 重写BGREWRITEAOF用当前内存里的数据重新生成一份最小化的命令集把历史命令折叠掉。注意 AOF 里存的是命令不是数据本身。打开appendonly.aof看到的是一串可读命令*2 $6 SELECT $1 0 *3 $3 SET $11 drink:stock $3 100 *2 $4 INCR $11 drink:stock这正是 AOF 能重放的原因恢复时把这些命令按顺序再执行一遍就回到了崩溃前的状态。也正因为存的是命令同一条命令重复写 1000 次AOF 里就存 1000 条——文件体积会明显大于 RDB 快照。怎么改Redis 4.0 之后支持混合持久化aof-use-rdb-preamble yes默认开启——重写时前半段写 RDB 快照后半段追加增量命令恢复时先加载快照再重放增量。生产建议开 AOF everysec同时定期把 RDB 快照传到另一台机器。七、内存不够删谁8 种淘汰策略现象促销那晚 Redis 的内存曲线像心电图一样往上冲最后自己崩了。实锤Redis 有一条硬规则——maxmemory达到上限时写命令直接失败127.0.0.1:6379 CONFIG GET maxmemory 1) maxmemory 2) 1073741824 127.0.0.1:6379 CONFIG GET maxmemory-policy 1) maxmemory-policy 2) noevictionmaxmemory-policy决定内存满了之后 Redis 怎么办一共 8 种策略淘汰范围行为适合场景noeviction不淘汰写命令直接报错默认数据不能丢的实例allkeys-lru全部 key淘汰最久未访问的纯缓存实例allkeys-lfu全部 key淘汰访问频次最低的流量有明显冷热分层allkeys-random全部 key随机淘汰只想简单止损volatile-lru仅带 TTL 的 key最久未访问缓存 队列混布volatile-lfu仅带 TTL 的 key访问频次最低同上volatile-random仅带 TTL 的 key随机同上volatile-ttl仅带 TTL 的 key剩余 TTL 最短的缓存 强过期数据关键区别allkeys-*会动你所有 key包括没设过期的volatile-*只淘汰设置了 TTL 的 key。如果实例上所有 key 都没有 TTL 却配了volatile-lru内存满了时没有任何 key 可淘汰写命令直接失败。# 这台实例已经顶到 maxmemory 了再写就是这条报错 127.0.0.1:6379 SET drink:counter 1 (error) OOM command not allowed when used memory maxmemory.注意报错里的used memory maxmemory——这是noeviction的典型表现内存超了不删数据直接拒绝写入。换成allkeys-lru后同样这条SET会成功Redis 顺手淘汰一个冷 key 腾位置。怎么改纯缓存实例用allkeys-lru或allkeys-lfu混布实例用volatile-lru。排查内存问题看INFO memory里的used_memory_human和碎片率127.0.0.1:6379 INFO memory # Memory used_memory:104857600 used_memory_human:100.00M used_memory_rss:157286400 used_memory_rss_human:150.00M mem_fragmentation_ratio:1.50 maxmemory:1073741824 maxmemory_policy:noevictionmem_fragmentation_ratio明显大于 1比如 3 以上通常意味着内存碎片化严重可以考虑重启或开启activedefrag。八、穿透、击穿、雪崩三种翻车现象这三种是缓存最经典的翻车姿势长得不像根因不同解法也不同。实锤穿透击穿雪崩现象查一个根本不存在的key缓存和 DB 都没有请求全打到 DB单个热点 key过期那一瞬间大量请求同时打到 DB大量 key 同时过期或 Redis 宕机请求全打到 DB根因恶意/异常参数穿透缓存热点 key 的缓存失效被瞬时并发打穿集中过期 单点故障缓存里有东西吗从来没有曾经有刚好没了一大片同时没了解法布隆过滤器空值缓存 参数校验互斥锁重建/逻辑过期随机 TTL多级缓存集群穿透的解法一是空值缓存——查不到也往缓存里塞一个__NULL__短 TTL比如 60 秒挡住重复的无效查询二是布隆过滤器——合法 id 先灌进位图查询前先问它这个 id 可能存在吗说不可能就直接返回连缓存都不用查。击穿的解法热点 key 过期瞬间只放一个线程去重建缓存其他线程原地等。import time import uuid UNLOCK_LUA if redis.call(GET, KEYS[1]) ARGV[1] then return redis.call(DEL, KEYS[1]) else return 0 end def get_hot_menu(menu_id: int, retries: int 5): 击穿防护互斥锁重建 Lua 安全解锁 key fdrink:hot:{menu_id} value r.get(key) if value is not None: return value lock_key flock:drink:hot:{menu_id} token str(uuid.uuid4()) if r.set(lock_key, token, nxTrue, ex10): try: value r.get(key) if value is None: row db.query_one( SELECT id, name FROM drink_menu WHERE id %s, (menu_id,) ) value json.dumps(row, ensure_asciiFalse) if row else __NULL__ r.set(key, value, exMENU_TTL) return value finally: # 必须用 Lua 判断 token 再删否则可能删掉别人的锁 r.eval(UNLOCK_LUA, 1, lock_key, token) for _ in range(retries): time.sleep(0.05) value r.get(key) if value is not None: return value return None另一种思路是逻辑过期缓存里不设 TTL把过期时间写在值里读到过期值时先返回旧数据同时异步触发重建——牺牲一点新鲜度换来热点 key 永不被打穿。雪崩的解法批量写入的 keyTTL 加随机偏移300 random.randint(0, 120)把集中过期摊平再往 Redis 前面加一层本地缓存Caffeine挡掉最热的读最后上集群保证单点挂了还有别的节点顶上。怎么改线上打满时先看 DB 的慢查询日志和 QPS 曲线慢查询全是查一个根本不存在的 id是穿透某一个 id占了大部分请求是击穿一大片 id 同时变慢是雪崩。九、主从、哨兵与 Cluster现象主从复制、哨兵、Cluster 三个词总被一起念叨其实解决的是三个不同层级的问题。实锤主从复制解决的是数据不丢和读扩展。全量靠 RDB增量靠命令流127.0.0.1:6379 INFO replication # Replication role:master connected_slaves:1 slave0:ip127.0.0.1,port6380,stateonline,offset1287361,lag0 master_repl_offset:1287361从库上执行REPLICAOF 127.0.0.1 6379就变副本。增量部分走复制积压缓冲区repl-backlog从库断了重连还能续上不用全量重来。但主从有延迟主库写完立刻返回成功从库可能还没收到。这叫旧读对扣库存这种强一致场景是致命的。哨兵Sentinel解决的是主库挂了自动选主判断下线分两级主观下线SDOWN单个哨兵在down-after-milliseconds默认 30 秒内收不到主库有效回复自己先认为它挂了客观下线ODOWN达到quorum数量的哨兵都认为它挂了才升级为客观下线。之后哨兵选出一个 leader 执行故障转移把某个从库提升为新主库。客户端通过哨兵拿到当前主库地址客户端必须支持 Sentinel 感知否则拿到的还是老地址。Cluster解决的是单机内存不够 单机扛不住。它把 key 按CRC16(key) mod 16384分到16384 个哈希槽每个主节点负责一部分槽127.0.0.1:7001 CLUSTER KEYSLOT drink:menu:10086 (integer) 9600 127.0.0.1:7001 CLUSTER INFO cluster_state:ok cluster_slots_assigned:16384 cluster_known_nodes:6客户端访问的 key 不在本节点时会收到一个重定向MOVED 9600 127.0.0.1:7002这个槽永久归 7002 管了客户端要改自己的槽位映射表ASK 9600 127.0.0.1:7002这个槽正在迁移过程中客户端只能临时去 7002 取这个 key取完仍按老映射走下一次可能又收到 MOVED。槽迁移是 Cluster 扩缩容的基础不重灌数据而是把槽从旧主节点一个一个搬给新节点CLUSTER SETSLOT ... IMPORTING/MIGRATING搬完再让客户端感知。方案解决什么写性能读扩展故障自动切换单节点—高无无主从数据备份、读扩展一样主库单点有可能读到旧值无哨兵 主从主库挂了自动选主一样有有Cluster数据分片、水平扩容分散到多节点有有怎么改数据量单机装得下、且能接受主库挂了要人工介入主从就够了要自动容灾就上哨兵想扛住峰值流量Cluster 才有意义。别为了看起来高级上 Cluster——它带来 MOVED 重定向、槽迁移、跨节点多键操作不支持等复杂度。一句话记主从管备份哨兵管选主Cluster 管分片。十、排查清单速查表出问题时按这个顺序查别瞎猜现象先查什么命令内存莫名涨满已用内存、碎片率、淘汰策略INFO memory、CONFIG GET maxmemory-policy某个命令特别慢慢日志SLOWLOG GET 10、CONFIG GET slowlog-log-slower-than某条命令卡住大 keyredis-cli --bigkeys只读别在峰值跑写入报 OOM上限和淘汰策略是否匹配CONFIG GET maxmemory maxmemory-policy改了价但用户看到旧价缓存是否没删、TTL 是否太长TTL key、GET key主从数据不一致复制偏移和延迟INFO replication看slave0的lag客户端报 MOVED槽位归属CLUSTER KEYSLOT key、CLUSTER NODES客户端连不上绑定地址、保护模式、密码CONFIG GET bind requirepass重启后数据丢了持久化开关和刷盘策略CONFIG GET appendonly appendfsync save某个 key 行为诡异它的底层编码和类型OBJECT ENCODING key、TYPE key想删掉某个 key别用 KEYS用 SCANSCAN 0 MATCH drink:* COUNT 100写在最后Redis 是内存数据结构服务不是更快的 MySQL。先分清哪些数据能放、哪些不能放比学多少命令都重要。Cache Aside读是缓存 → miss → DB → 回填写是更新 DB → 删缓存。写一定删缓存、不更新缓存删除失败要重试。底层编码决定内存小 Hash / 小 ZSet 用 listpack大了自动升级 hashtable / skiplistOBJECT ENCODING一看便知。单线程指的是执行命令所以单条命令原子网络层靠 epoll 多路复用6.0 起还有 IO 线程可选。慢命令会拖垮所有人排查先看SLOWLOG GET。ZSet 字典 跳表查找 O(log N)范围查询 O(log N M)rehash 是惰性 渐进式靠rehashidx摊平搬迁开销。持久化RDB 是快照恢复快但可能丢数据AOF 记命令everysec折中配套BGREWRITEAOF重写。三组区别点比背命令有用allkeys-*只动全部 key 而volatile-*只动带 TTL 的穿透查不存在的 key、击穿打单个热点 key、雪崩倒一片主从管备份、哨兵管选主、Cluster 管分片。如果上面哪一条你也卡过评论区说一声「缓存穿透」我把这套实验代码整理成完整项目发出来——你可以自己改参数重跑一遍比看文章直观得多。