
第一次被 Redis 救场是在一个商品详情接口上。那时候接口里塞了七八个联表查询单次响应稳定在 700 毫秒往上大促压测直接崩。把热点商品对象挪进 Redis 之后同样的机器数量扛住了十倍流量响应掉到 30 毫秒以内。从那天起我才真正理解Redis 不是给数据库加个缓存这么简单它是一套围绕内存数据结构做出来的独立技术体系用对了是性能杠杆用错了就是线上事故的发源地。这篇内容围绕 Redis 的介绍展开但不会照着命令手册念一遍。我会从为什么要在项目里引入它讲起把五种核心数据结构的选择逻辑、持久化与高可用的配置取舍、缓存三大经典故障的落地解法以及线上排障的真实手法一层层拆开。适合刚接触 Redis、能敲几个命令但不知道生产环境怎么用的同学也适合已经用了两三年、想把知识从会用推到敢扛线上的开发者。读完你应该能独立完成一次 Redis 的选型、部署、接入和故障排查而不是只会set和get。1. 引入 Redis 之前先把它的定位想清楚很多项目引入 Redis 的理由是别人都在用这非常危险。Redis 把数据放在内存里换取极低的读写延迟代价是内存成本高、容量受机器限制、数据可靠性需要额外机制保障。理解这个基本交换关系后面的所有决策——存什么、存多久、要不要持久化、要不要做集群——才有依据。1.1 从一次接口超时说起它到底替你扛了哪几类活我通常把 Redis 在项目里的角色归成四类这四类对应完全不同的设计约束混在一起想很容易出问题。第一类是热点数据缓存。数据库擅长持久化和复杂查询不擅长高频重复读。一个商品对象被一秒读几千次每次都走数据库连接池很快被占满。这类场景的关键是容忍短暂不一致缓存里的数据旧几秒钟可以接受业务上要有兜底。第二类是计数器与频次控制。比如接口限流、文章阅读数、短信验证码的发送间隔。这类操作的特点是原子性要求高、写入频繁、单条数据极小。Redis 的单线程命令执行模型天然适合INCR一条命令就能保证并发下的正确性不用你自己加锁。第三类是临时状态存储。比如登录会话、购物车、分布式锁的持有标记。这类数据的共同点是丢了可以重建或者过期即失效天然契合 Redis 的 TTL 机制不必占用关系型数据库的表空间。第四类是轻量消息与排行。List 可以做简单的队列ZSet 可以做实时排行榜、延迟队列。注意我说的是轻量一旦涉及消息可靠投递、消费确认、堆积告警这些诉求专业消息中间件才是正解别硬拿 List 顶。提示把项目里每处 Redis 的用途标注成上面四类之一你会发现很多设计争议当场就能收敛。缓存类允许不一致状态类必须有过期计数类必须原子队列类必须考虑堆积。这四类用法的共同前提是访问模式以按键取整块数据为主。如果你的查询是复杂的多条件聚合、模糊匹配、事务性多表更新Redis 不是合适的战场。我见过有团队把订单列表的所有筛选组合都塞进 Rediskey 数量爆炸维护成本远高于收益最后还是退回数据库加索引解决。1.2 和本地缓存、关系型数据库比边界到底在哪儿选型这件事最怕的不是选错而是不知道自己在选什么。下面这张表是我在方案评审时常用的对照把三者的定位摆在一起看边界会清楚很多。维度本地缓存进程内 MapRedis关系型数据库访问延迟纳秒到微秒级亚毫秒到毫秒级含网络毫秒到几十毫秒容量上限受单机堆内存限制受集群内存总量限制受磁盘限制多实例一致性各实例各自一份需广播失效天然共享一份强一致数据可靠性进程重启即丢取决于持久化配置高适合的数据形态极热、体量小、变更少热、中等体量、需共享全量、需事务与关联查询运维复杂度几乎为零中等需监控与容量规划高实际项目里这三者往往是组合用的不是二选一。一个典型的分层是极热且几乎不变的配置数据放本地缓存热点业务对象放 Redis全量数据落数据库。请求进来先查本地未命中查 Redis再未命中回源数据库并回填。我踩过的一个坑是某项目把所有缓存都放在本地服务扩容到八个实例后每个实例各自回源数据库压力不降反升而且数据更新后需要逐个通知失效实现得一团糟。改成 Redis 共享一份之后问题当天就消失了。判断标准很简单数据需不需要跨实例共享。需要就别用本地缓存硬扛。另一个常见误解是觉得 Redis 可以当数据库用。理论上配合 AOF 每命令刷盘能接近不丢但那样吞吐会掉到普通磁盘数据库的水平失去了用它的意义。我的态度很明确Redis 里放的数据业务上要能接受极端情况下丢失一部分或者能从容灾源重建。1.3 环境准备与最小可用验证先用 Docker 起一个单实例把环境跑通再谈别的。生产环境当然不会这么部署但本地验证足够了。docker run -d --name redis-dev \ -p 6379:6379 \ -v /data/redis/conf:/usr/local/etc/redis \ redis:7.2 redis-server /usr/local/etc/redis/redis.conf起容器之前先准备一份最小配置别用默认配置直接跑默认配置里protected-mode、持久化策略、最大内存都是偏保守或缺失的。# redis.conf 最小可用版本 bind 0.0.0.0 port 6379 requirepass your_strong_password_here maxmemory 2gb maxmemory-policy allkeys-lru appendonly yes appendfsync everysec save 900 1 save 300 10 save 60 10000逐条说一下为什么这么配。maxmemory必须设不设的话 Redis 会一直吃内存直到被系统 OOM Killer 干掉进程被杀时数据全丢比主动淘汰糟糕得多。一般建议设成机器物理内存的 60% 到 70%剩下的留给操作系统、AOF 重写时的 fork 子进程和内存碎片。appendfsync everysec是持久化和性能的折中点最多丢一秒的数据绝大多数业务能接受。save那三行是 RDB 的触发条件含义是900 秒内有 1 次写300 秒内有 10 次写60 秒内有 10000 次写任意一条满足就落盘。验证环节敲几个命令就够了redis-cli -a your_strong_password_here 127.0.0.1:6379 PING PONG 127.0.0.1:6379 SET user:1001:name A同学 EX 60 OK 127.0.0.1:6379 TTL user:1001:name (integer) 60 127.0.0.1:6379 INFO memory # Memory used_memory_human:1.02MTTL返回 -1 表示没设过期返回 -2 表示 key 不存在这两个返回值经常被混淆排查问题时要注意区分。我把这个验证流程做成了一个脚本每次新环境部署完先跑一遍确认密码、过期、内存信息都正常再交给业务接入。注意requirepass一定要设且不要用弱密码。Redis 默认没有认证监听到公网端口就是敞开的大门。同时要用防火墙限制来源 IP只允许应用服务器网段访问。2. 五种核心数据结构选对了事半功倍Redis 的价值一半在内存另一半在数据结构。同样一个需求用 String 和用 Hash 实现内存占用可能差一倍可维护性差得更远。这一节把五种常用类型的适用边界讲透每种都配上真实场景和容易踩的细节。2.1 String 与 Hash缓存对象和计数的正确姿势String 是最基础的类型可以存文本、数字、二进制序列化后的对象。SET key value EX 3600 NX这一条命令里就包含了过期设置和不存在才写入的语义原子性由服务端保证比先EXISTS再SET安全得多。缓存整个对象时我一般会把对象序列化成 JSON 再存 String。理由是读写简单、一次网络往返、反序列化成本低。缺点是更新对象里的某个字段必须整体读出来改完再写回并发下容易互相覆盖。Hash 恰好补上这个短板。把对象拆成HSET user:1001 name A同学 age 28 city 某市更新单个字段只动那一个天然避免整体覆盖。代价是多字段读取要么用HMGET一次取多个要么用HGETALL全取如果字段特别多几百个以上HGETALL会返回大量数据可能撞上大 key 问题。我的经验判断是这样的对象字段少于 50 个、经常整体读取用 String 存 JSON字段多、更新频繁且只改局部用 Hash。不要为了看起来优雅把所有对象都拆成 Hash多字段对象的序列化和反序列化在客户端做一次往往比多次网络往返更快。计数场景是 String 的主场。INCR和INCRBY是原子的不需要额外加锁127.0.0.1:6379 SET article:2001:views 0 OK 127.0.0.1:6379 INCR article:2001:views (integer) 1 127.0.0.1:6379 INCRBY article:2001:views 10 (integer) 11限流也是这个思路。做一个每个用户每分钟最多请求 100 次的限制用INCR配合EXPIRE127.0.0.1:6379 MULTI OK 127.0.0.1:6379 INCR rate:1001:202405201030 QUEUED 127.0.0.1:6379 EXPIRE rate:1001:202405201030 60 QUEUED 127.0.0.1:6379 EXEC 1) (integer) 1 2) (integer) 1第一次请求时计数为 1 且设置 60 秒过期后续请求只走INCR。这里有个细节每次EXPIRE都会重置过期时间如果每次请求都执行key 会一直不失效计数就永远不归零。正确做法是用 Lua 脚本把计数为 1 时才设过期和递增合并成一个原子操作或者在应用层判断返回值是否为 1 再决定是否设过期。-- rate_limit.luaKEYS[1] 是限流 keyARGV[1] 是窗口秒数 local current redis.call(INCR, KEYS[1]) if current 1 then redis.call(EXPIRE, KEYS[1], ARGV[1]) end return current这个脚本返回当前计数应用层拿它和阈值比较即可。用脚本的好处是避免递增成功但设过期失败导致计数永不过期这种漏网 key 在生产环境里会慢慢堆积属于典型的慢性病。2.2 List、Set、ZSet队列、去重和排行榜的落地细节List 是双向链表两端插入删除都是 O(1)适合做简单队列。LPUSH进、RPOP出就是先进先出反过来就是后进先出。但RPOP有个老问题列表空了会立刻返回 nil消费者只能轮询白白消耗 CPU。Redis 6.2 之后有了BLPOP/BRPOP阻塞等待直到有数据或超时轮询问题解决。# 生产者 127.0.0.1:6379 LPUSH queue:order order-1001 (integer) 1 # 消费者最多阻塞 5 秒 127.0.0.1:6379 BRPOP queue:order 5 1) queue:order 2) order-1001必须清楚的局限是BRPOP取出即从列表删除如果消费者取到消息后进程崩了这条消息就永久丢了。需要至少消费一次的语义得用BRPOPLPUSH把消息转到处理中的备份列表处理完再从备份列表删除。很多团队做到这一步就发现与其自己拼装这套机制不如直接上专业消息中间件。我的建议是任务不重、丢失可容忍、量不大用 List其余情况别硬撑。Set 是无序去重集合做交集、并集、差集特别方便。典型场景是共同关注SINTER user:1001:follows user:1002:follows直接得到两人的共同关注列表。另一个常见用法是做抽奖或去重统计SADD返回 1 表示新加入返回 0 表示已存在这个返回值就能用来判断是否是首次。ZSet 是有序集合每个元素带一个分数按分数排序。排行榜就是它的教科书场景127.0.0.1:6379 ZADD rank:202405 980 user:1001 1020 user:1002 870 user:1003 (integer) 3 127.0.0.1:6379 ZREVRANGE rank:202405 0 2 WITHSCORES 1) user:1002 2) 1020 3) user:1001 4) 980 5) user:1003 6) 870 127.0.0.1:6379 ZREVRANK rank:202405 user:1001 (integer) 1ZREVRANK返回的是从 0 开始的名次返回 1 就是第二名展示时记得加一。分数相同时 Redis 会按成员字典序排列如果业务要求同分先到者在前就得把时间戳编进分数比如用分数 * 10^10 (最大值 - 时间戳)的方式组合避免同分乱序引发用户投诉。还有个高频场景是延迟队列把到期执行时间戳作为分数存进 ZSet后台任务用ZRANGEBYSCORE key 0 当前时间戳 LIMIT 0 10取出到期任务处理成功后ZREM。这个方案简单有效缺点是精度依赖轮询间隔以及没有重试和死信机制业务上要自己补。2.3 过期策略、内存淘汰与 key 命名规范过期这件事Redis 用的是惰性删除加定期删除的组合。惰性删除指访问这个 key 时才检查是否过期好处是不浪费 CPU坏处是没人访问的过期 key 会一直占着内存。定期删除指每秒若干次随机抽查一部分设了过期时间的 key删掉过期的。两者配合能覆盖大部分场景但极端情况下仍会有过期 key 滞留INFO stats里的expired_keys能看出删除总量。内存达到maxmemory之后的淘汰策略常用的有这几种策略含义适用场景noeviction不淘汰写入直接报错数据不能丢、宁可拒绝服务allkeys-lru所有 key 里淘汰最久未使用的纯缓存场景最常用allkeys-lfu所有 key 里淘汰访问频率最低的有明显热点、长短尾混杂volatile-lru只在设了过期时间的 key 里淘汰缓存与持久数据混存allkeys-random随机淘汰访问分布均匀时volatile-ttl优先淘汰剩余存活时间短的有明确新鲜度要求选错策略的后果很直接。曾经有个项目把配置数据不该被淘汰和业务缓存放在同一个实例用的是allkeys-lru流量高峰时配置数据被淘汰业务读取失败排查了半天才发现是淘汰策略的问题。原则是不能丢的数据不要和可以丢的缓存混在同一个实例里实在要混就用volatile-lru并且保证不能丢的数据不设过期时间。Key 命名是我特别想强调的一点。生产环境里INFO keyspace看到几十万个user_1001、user_1002这种裸 key前缀混乱、无业务标识排查问题时根本不知道谁在用。我推行的规范是业务域:对象类型:唯一标识:可选维度例如order:detail:202405200001 user:profile:1001 rate:api:1001:202405201030 lock:stock:sku8801冒号分层不是强制的但好处是SCAN匹配时方便比如SCAN 0 MATCH order:detail:* COUNT 100。再强调一遍生产环境永远不要用KEYS *它会阻塞主线程直到遍历完所有 key百万级 key 的实例上执行一次可能卡住好几秒足以引发雪崩。用SCAN游标迭代虽然可能重复返回但对主线程友好。3. 持久化与高可用数据到底能丢多少新手最容易忽略的一节。单机 Redis 重启数据去哪了主节点挂了业务多久恢复这两个问题的答案全在持久化和高可用配置里。3.1 RDB 与 AOF 的原理差异和参数取舍RDB 是把某一时刻的全量数据快照写成二进制文件。触发方式有两种配置里的save规则自动触发或者手动执行BGSAVE。执行时 Redis 会 fork 一个子进程子进程负责写文件主进程继续处理请求靠的是操作系统的写时复制机制。RDB 的优点是文件紧凑、恢复速度快几个 G 的数据加载通常比 AOF 快一个数量级。缺点是快照之间写入的数据会丢。比如save 60 10000意味着最多可能丢 60 秒内的数据。另一个隐性成本是 fork数据量越大fork 耗时越长且写时复制期间内存可能翻倍如果maxmemory设得太接近物理内存fork 时可能触发 OOM。经验值是单实例数据量控制在 10GB 以内内存预留 30% 以上余量。AOF 是把每条写命令追加到日志文件重启时重放。appendfsync有三个取值always每条命令都刷盘最安全但吞吐极低everysec每秒刷一次最多丢一秒是绝大多数场景的选择no交给操作系统决定性能最好但丢失窗口不可控。AOF 文件会越来越大所以有重写机制。auto-aof-rewrite-percentage 100和auto-aof-rewrite-min-size 64mb表示文件大小比上次重写后翻倍且超过 64MB 时触发重写。重写同样靠 fork 子进程同样有内存翻倍风险。Redis 4.0 之后支持混合持久化aof-use-rdb-preamble yes打开后AOF 文件前半部分是 RDB 格式的快照后半部分是增量命令兼顾了恢复速度和数据完整性。我现在的新项目基本都用这个配置。appendonly yes appendfsync everysec aof-use-rdb-preamble yes auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb注意appendfsync everysec在磁盘 IO 压力大时主线程可能被阻塞表现为周期性延迟毛刺。可以观察INFO persistence里的aof_delayed_fsync计数如果持续增长说明磁盘跟不上要么换更快的盘要么调大no-appendfsync-on-rewrite。关于到底能丢多少我给业务方的回答通常是这样开everysec加混合持久化正常宕机丢一秒内的数据机器整机损毁且没有从库丢的是最后一次 fsync 之后的数据。如果业务连一秒都丢不起那就不该只用 Redis 存这份数据必须落数据库。3.2 主从复制与哨兵的部署要点单机 Redis 有两个硬伤一是容量受单机内存限制二是挂了就完全不可用。主从复制解决读扩展和备份问题哨兵解决自动故障转移。主从复制的流程分两步首次全量同步从库连上主库后主库执行BGSAVE生成 RDB 发给从库从库加载完再接收期间的增量命令之后进入增量同步主库每写一条命令就异步发给从库。复制是异步的所以主库挂了还没来得及同步到从库的那部分数据会丢。配置从库只要一行replicaof 主库IP 6379 masterauth 主库密码 replica-read-only yes哨兵至少部署三个节点且要是奇数因为它靠投票决定是否判定主库客观下线。quorum配置决定几个哨兵认为主库挂了才触发选举通常设为节点数的一半加一。sentinel monitor mymaster 主库IP 6379 2 sentinel down-after-milliseconds mymaster 30000 sentinel failover-timeout mymaster 180000 sentinel parallel-syncs mymaster 1down-after-milliseconds 30000表示 30 秒连不上才判定主观下线这个值别设太小网络抖动会导致误切换切换期间写入全部失败损失比多等 20 秒大得多。parallel-syncs 1表示故障转移后一次只让一个从库去同步新主库避免新主库被多个从库的全量同步同时压垮。哨兵的局限也要清楚它只解决主库挂了自动选新主不解决容量扩展数据量超过单机内存还是装不下。另外哨兵切换期间有几十秒的写入不可用窗口业务必须做好重试和降级不能假设 Redis 永远在线。3.3 集群分片的槽位计算与容量规划数据量超过单机内存就得上集群。Redis Cluster 把整个键空间切成 16384 个槽每个主节点负责一部分key 落在哪个槽由CRC16(key) mod 16384决定。三主三从的典型部署每个主节点分到大约 5461 个槽。客户端连接任意节点节点会告诉客户端该 key 在哪个节点上客户端据此路由必要时重定向。# 查看集群状态和槽位分布 redis-cli -c -h 节点IP -p 6379 cluster info redis-cli -c -h 节点IP -p 6379 cluster nodes集群模式有几个硬约束接入前必须知道多 key 操作必须在同一个槽。MGET、SUNION、事务、Lua 脚本涉及多个 key 时如果这些 key 不在同一槽直接报CROSSSLOT错误。解决办法是用 hash tag把 key 里用{}包裹的部分作为计算槽位的依据例如order:{1001}:detail和order:{1001}:items会落到同一个槽。不支持跨槽的多数据库集群模式只有 db 0SELECT命令不可用。批量操作要拆分。一次MGET几百个 key如果不保证同槽得在客户端按槽分组再分别请求。容量规划我一般这么算。假设日活 20 万每人缓存一个用户对象序列化后平均 1.5KB全部在线用户同时在缓存里数据量约 300MB。加上商品、配置、会话等其他数据粗估 2GB。再考虑 Redis 自身的内存开销哈希表、过期字典、jemalloc 的分配粒度实际占用通常是原始数据的 1.2 到 1.5 倍取 3GB。然后留出 fork 和碎片的余量单节点规划 6GB三主集群总容量 18GB可以支撑未来一两年的增长。规划项计算方式示例结果原始数据量对象数 × 平均大小20万 × 1.5KB ≈ 300MB加上其他业务数据分类累加约 2GB内存开销系数原始 × 1.2~1.5约 3GB预留 fork 与碎片上值 × 2约 6GB/节点集群规模按三主三从总可用约 18GB这个算法不精确但足够做初始规划。上线后用INFO memory里的used_memory_rss和mem_fragmentation_ratio持续校准碎片率长期高于 1.5 就考虑重启整理或开启activedefrag。4. 缓存实战穿透、击穿、雪崩怎么真正解决前面三节是基础这一节是大多数人真正会踩坑的地方。缓存设计不当引发的线上问题比 Redis 自身故障多得多。4.1 缓存读写模式与一致性的处理顺序最常见的模式是旁路缓存Cache-Aside读的时候先查缓存未命中查数据库并回填写的时候先更新数据库再删除缓存。为什么是删除缓存而不是更新缓存因为并发写场景下两个请求同时更新数据库再更新缓存顺序可能错乱缓存里留下旧值。删除则简单得多下次读自然回填最新值。为什么是先更新库再删缓存而不是反过来假设先删缓存再更新库删完到更新完之间有个窗口其他请求读到未命中就回源查库此时库里还是旧值回填进缓存等第一个请求更新完库缓存里就是脏数据而且会一直脏下去。先更新库再删缓存虽然也有窗口删缓存前有请求回填了旧值但概率小得多而且可以通过延迟双删进一步降低。# 简化的读取流程用 Python 示意 import json import redis r redis.Redis(host127.0.0.1, port6379, password***, decode_responsesTrue) def get_user(user_id): cache_key fuser:profile:{user_id} cached r.get(cache_key) if cached is not None: return json.loads(cached) # 回源数据库这里是伪代码 user query_user_from_db(user_id) if user is None: # 空值也缓存防止穿透过期时间设短一些 r.set(cache_key, json.dumps({}), ex60) return None r.set(cache_key, json.dumps(user), ex1800) return user延迟双删的做法是更新库之前删一次缓存更新完再延迟几百毫秒删第二次。第一次删是为了让后续读请求尽量回源第二次删是为了清掉窗口期内可能被回填的旧值。延迟时间取一次读请求回源加回填的耗时的经验值一般 300 到 500 毫秒。这套方案能覆盖绝大多数场景但它不是强一致。如果业务真的要求缓存和数据库强一致那就不该用缓存或者只能用读时加锁 版本号校验这类重方案。我的观点是先问业务能不能容忍秒级不一致能容忍就用 Cache-Aside不能容忍就重新设计数据流别指望缓存层给你强一致。4.2 穿透、击穿、雪崩的具体解法与代码这三个词经常被混用其实成因完全不同解法也不同。缓存穿透查一个根本不存在的 key缓存永远不命中请求全部压到数据库。典型的攻击手段或者业务上的脏 ID 查询。解法一缓存空值。查不到也往缓存里写一个空对象TTL 设短一点比如 60 秒。上面那段代码已经体现了。缺点是如果攻击者每次用随机 ID缓存里会塞满空值占用内存所以要把 TTL 压短。解法二布隆过滤器。把所有存在的 ID 提前写进布隆过滤器请求进来先判断不存在直接返回。布隆过滤器的特点是说不存在一定不存在说存在可能误判误判率可以调到 1% 以内对业务无影响。# 基于 redis-py 的布隆过滤器示意实际可用 redisbloom 模块 from redisbloom.client import Client bloom Client(host127.0.0.1, port6379) bloom.bfCreate(user:ids, 0.001, 1000000) bloom.bfAdd(user:ids, 1001) def exists_user(user_id): # 返回 False 表示一定不存在可以直接拒绝 return bloom.bfExists(user:ids, str(user_id))布隆过滤器的短板是删除麻烦标准布隆过滤器不支持删除元素用户注销后 ID 还留在里面会导致误判。有删除需求的场景可以用计数布隆过滤器或者定期重建。缓存击穿某个热点 key 刚好过期大量并发请求同时回源数据库瞬间被打爆。注意击穿针对的是单个热点 key。解法一互斥锁。只让一个请求回源其他请求等待。用SET key value NX PX 30000抢锁抢到的去查库回填没抢到的短暂休眠后重试读缓存。import time def get_hot_data(key): cached r.get(key) if cached is not None: return cached lock_key flock:{key} # 抢占锁值为唯一标识防止误删别人的锁 got_lock r.set(lock_key, worker-1, nxTrue, px5000) if got_lock: try: data query_from_db(key) r.set(key, data, ex600) return data finally: r.delete(lock_key) else: # 没抢到短暂等待后重试 time.sleep(0.05) return get_hot_data(key)这段代码有两个细节。锁的过期时间要大于回源耗时否则回源还没结束锁就失效其他请求又涌进来。finally里删锁要校验值严格做法是用 Lua 脚本比较值再删避免删掉别人后来抢到的锁。解法二逻辑过期。缓存里存的数据带一个过期时间字段但不给 key 设 TTL。读到数据后判断逻辑时间是否过期过期了就异步触发更新当前请求先返回旧数据。这个方案的优点是没有请求等待缺点是会有一段时间读到旧数据。缓存雪崩大量 key 在同一时刻集中过期或者 Redis 整体不可用导致请求全部打到数据库。解法一过期时间加随机。不要用统一的 1800 秒改成 1800 加上 0 到 300 的随机数把过期时间打散。import random def set_with_jitter(key, value, base_ttl1800): ttl base_ttl random.randint(0, 300) r.set(key, value, exttl)解法二多级缓存加降级。本地缓存挡一层Redis 挡一层都挂了还有限流和熔断兜底。数据库前面挂连接池上限真被打爆时快速失败而不是雪崩式排队。解法三高可用部署。主从加哨兵或者集群避免单点故障。这一条其实是所有解法的前提。问题成因首选解法备选穿透查不存在的 key布隆过滤器缓存空值击穿单个热点 key 过期互斥锁重建逻辑过期雪崩大量 key 同时过期或实例故障TTL 加随机 高可用多级缓存 限流降级4.3 分布式锁的实现与那些绕不过去的坑分布式锁是 Redis 的高频用法也是最容易写出 bug 的地方。最基础的实现是SET lock_key unique_value NX PX 30000三个要素缺一不可NX保证互斥PX保证不会因为持有者崩溃而永久死锁unique_value保证释放时不会误删别人的锁。释放锁必须用 Lua 脚本做原子校验-- unlock.lua if redis.call(GET, KEYS[1]) ARGV[1] then return redis.call(DEL, KEYS[1]) else return 0 end如果分两步先GET再DEL中间可能锁过期被其他请求抢到你删掉的就是别人的锁。单实例锁有个理论缺陷主库写入锁之后还没同步到从库主库挂了从库升主锁就丢了两个客户端同时持锁。要彻底解决需要 Redlock 算法向多个独立实例加锁多数成功才算成功。但 Redlock 本身也有争议它对时钟漂移敏感运维成本高。我的实际建议是锁保护的操作用数据库唯一约束、状态机、乐观锁版本号这类方案能解决就别用分布式锁。真要用单实例加唯一值加过期时间已经能覆盖 99% 的场景。如果业务对锁的安全性要求极高比如资金扣减那应该用数据库行锁或专业的协调服务而不是赌 Redis 的可靠性。还有一个常见坑是锁的过期时间设置。设短了业务没执行完锁就失效设长了持有者崩溃后要等很久才释放。工程上的做法是看门狗机制后台线程定期给锁续期只要业务还在执行就不断延长。自己实现要注意续期线程和主线程的生命周期绑定别出现主线程已经结束、续期线程还在跑的尴尬局面。5. 线上排障实录与避坑清单前面讲的都是应该怎么做这一节讲出问题了怎么查。Redis 的故障有个特点表象往往是业务接口变慢根因却在几个特定指标上。5.1 慢查询、大 key、热 key 的定位手法慢查询的排查入口是 slowlog。先确认阈值默认slowlog-log-slower-than 10000是 10 毫秒低于这个不记录。127.0.0.1:6379 CONFIG GET slowlog-log-slower-than 127.0.0.1:6379 SLOWLOG GET 10 1) 1) (integer) 128 2) (integer) 1716200000 3) (integer) 15200 4) 1) KEYS 2) user:* 5) 10.0.0.12:52344输出里第三项是耗时微秒第四项是命令和参数第五项是客户端地址。看到KEYS、FLUSHALL、大范围的SMEMBERS、HGETALL基本就能定位问题。我处理过的一次线上抖动slowlog 里全是KEYS config:*追下去是一个定时任务写错了改成SCAN之后恢复。大 key的判定没有绝对标准我的经验线是String 超过 10KB集合类元素超过 5000 个或者单个 key 占用内存超过 1MB。大 key 的危害有三层删除时阻塞主线程DEL一个大 Hash 可能卡几百毫秒、网络传输占用带宽、集群下导致数据倾斜。排查命令# 扫描大 key-i 0.1 表示每 100 毫秒扫描一次降低对主线程的影响 redis-cli -h 节点IP -p 6379 --bigkeys -i 0.1 # 抽样分析内存占用输出前 10 名 redis-cli -h 节点IP -p 6379 --memkeys --memkeys-samples 10000--bigkeys用的是SCAN加TYPE对线上影响可控但也会消耗一些 CPU建议在低峰期跑。发现大 key 后处理方式分两种能拆就拆比如把一个大 Hash 按字段范围拆成多个不能拆就用UNLINK异步删除避免阻塞。热 key指某个 key 的访问量远超其他单节点可能被打到 CPU 瓶颈。排查方式# 需要 maxmemory-policy 为 lfu 系列才有效 redis-cli -h 节点IP -p 6379 --hotkeys输出会列出访问频率最高的 key。热 key 的解法通常是本地缓存兜一层或者把 key 复制多份打散到不同节点比如hot:item:8801:0到hot:item:8801:9读的时候随机选一个。后者会增加一致性的复杂度只在极端场景用。5.2 常见报错速查表下面这些报错我在不同项目里都遇到过整理成表方便对照。报错信息常见原因处理方式OOM command not allowed when used memory maxmemory内存打满且淘汰策略为 noeviction调整淘汰策略或扩容或清理无用 keyMISCONF Redis is configured to save RDB snapshots...后台落盘失败通常是磁盘满或权限问题检查磁盘空间和目录权限确认stop-writes-on-bgsave-error配置READONLY You cant write against a read only replica写请求打到了从节点检查客户端路由配置或哨兵切换后客户端未刷新拓扑CROSSSLOT Keys in request dont hash to the same slot集群模式下多 key 操作跨槽使用 hash tag 让相关 key 落同一槽或拆分请求LOADING Redis is loading the dataset in memory实例重启后正在加载数据客户端加重试运维侧关注加载耗时NOAUTH Authentication required未带密码或密码错误检查连接配置ERR max number of clients reached连接数超限调大maxclients排查连接泄漏BUSY Redis is busy running a scriptLua 脚本执行超时检查脚本逻辑SCRIPT KILL终止只读脚本注意MISCONF那个报错最容易被误判成网络问题。它的本质是上次 RDB 落盘失败了为了数据安全我拒绝写入。先把磁盘空间查一遍八成能找到原因。5.3 生产环境的配置基线与运维心得最后把我这些年沉淀下来的一套基线配置分享出来。不同业务会有调整但方向是一致的。# 内存 maxmemory 6gb maxmemory-policy allkeys-lru maxmemory-samples 5 # 持久化 appendonly yes appendfsync everysec aof-use-rdb-preamble yes save 900 1 save 300 10 save 60 10000 # 安全 requirepass 强密码 rename-command FLUSHALL rename-command FLUSHDB rename-command KEYS # 连接 maxclients 10000 timeout 300 tcp-keepalive 300 # 慢查询 slowlog-log-slower-than 10000 slowlog-max-len 256 # 客户端缓冲防止输出缓冲撑爆内存 client-output-buffer-limit normal 0 0 0 client-output-buffer-limit replica 256mb 64mb 60 client-output-buffer-limit pubsub 32mb 8mb 60几条重点解释。rename-command把危险命令重命名成空字符串相当于禁用FLUSHALL和KEYS是生产环境最不该被随手敲的两个命令禁掉能省很多麻烦。client-output-buffer-limit那三行是防从库同步慢导致主库内存暴涨的关键从库跟不上时主库会一直攒缓冲不设上限可能直接把主库撑死。监控方面我关注的核心指标就下面这几个其他的都是衍生used_memory_rss和mem_fragmentation_ratio碎片率长期大于 1.5 要处理connected_clients突增通常是连接泄漏instantaneous_ops_per_sec结合业务量判断是否有异常流量keyspace_hits / keyspace_misses命中率低于 90% 要检查缓存策略rejected_connections出现就说明连接数不够latest_fork_usecfork 耗时超过 200 毫秒要考虑拆分实例master_link_status从库视角down表示复制断了一次真实的排查经历值得说一下。某天下午接口 P99 从 30 毫秒跳到 400 毫秒业务量没变。查监控发现latest_fork_usec从 8 毫秒涨到了 350 毫秒再看used_memory_rss已经到 14GB而机器只有 16GB。原因是数据量涨上来之后每次BGSAVE的 fork 都要复制大量页表加上写时复制的内存开销系统开始换页。最后的处理是把实例拆成两个各自控制在 6GB 以内延迟立刻恢复。这件事给我的教训是监控不能只看 QPS 和命中率内存指标和 fork 耗时才是 Redis 的体检报告。很多看起来莫名其妙的延迟毛刺根因都在内存和持久化上。另外一个小技巧排查某个时间段突然变慢时先对一下SLOWLOG的时间戳和持久化触发时间是否重合。如果重合八成是 fork 或 AOF 重写导致的这时候优化业务代码没用得从持久化配置和实例规格下手。到这里Redis 从定位、数据结构、持久化、缓存设计到排障的主干就串完了。我自己用了这么多年最大的体会是它的命令很简单难的是判断什么该放进去、放多久、丢了会怎样。把这三个问题在方案评审时问清楚后面百分之八十的事故都能提前避开。剩下的就交给监控和演练去兜底了。