去年帮一家电商团队排查线上故障,现象很经典:Redis 内存告警、慢查询刷屏、数据库连接被打满。我打开他们的 Redis 一看——大 key、永不过期的 key、还有大量用 String 存的复杂结构,遍地都是雷。这几乎是每个没定过使用规范的团队,在 Redis 上的共同结局。
这篇博客把我这些年整理、验证过的一套 Redis 最佳实践完整放出来。以 7 个维度、43 条使用规范为主线,覆盖键值设计、数据类型选型、内存治理、持久化与高可用、缓存三大经典问题、分布式锁、监控运维,最后附带一份可直接抄走的实践清单。适合正在维护生产 Redis 的工程师,也适合准备给团队定规范的技术负责人。以下每条都是我在真实环境里踩过坑、验证过有效才留下的,不是从网上抄来的八股文,但面试时能把这 43 条讲透,基本也够用了。
1. 键值设计:从源头避免滥用与冲突
1.1 六条硬规范,先把键名和值管住
规范 1-1:键名统一前缀,用业务域 + 冒号分层。这是最基础也最容易被忽视的一条。没有前缀的键名,在集群和多个业务共用一套 Redis 时,就是一场灾难:你不知道user:1001是订单服务的还是会员服务的,也不敢随便清理。我这边要求所有键名必须是业务域:模块:实体:ID:属性的结构,比如order:user:1001:info。这样无论用SCAN维护、做数据迁移还是定位问题,都能靠前缀快速过滤。另外,冒号分层不是给 Redis 看的,是给人看的,格式统一了,后续所有脚本和文档都能省一大半精力。
规范 1-2:键名带时间或版本维度,给冷数据留退路。很多团队把缓存键写成固定名字,上线后数据格式一变,旧缓存全部失效,或者新老数据混在一起,排查起来极难。现在我会在关键业务的键名里加上版本号或日期,比如promo:20250601:config。这样做的好处很实在:灰度发布时新老版本可以共存,出问题能快速回退,每天的统计类缓存也能按天自动过期,不用写定时清理任务。
规范 1-3:键名长度控制在可读与性能之间。Redis 的键是二进制安全的,理论上多长都行,但键本身是要占内存的,超长键名在千万级 key 量下浪费的内存非常可观。我通常要求键名不超过 128 字节,能表达清楚业务含义即可。反例是有人把整个 URL 当 key,动辄几百字节,一个 key 比 value 还大,完全没有必要。正确做法是对 URL 做 MD5 或 SHA1 哈希后用前 16 位做键,原始 URL 放 value 里。
规范 1-4:严禁大 key,这是内存和延迟的头号杀手。大 key 没有一个绝对标准,但业界普遍接受的阈值是:单个 String 值超过 10KB,或者集合类型元素数量超过 5000 个、总大小超过 10MB,都要列入大 key 治理清单。大 key 的危害有几个层面:读写阻塞(Redis 单线程,操作大 key 耗时上升)、网络带宽压力(一次 get 拉几十 MB)、复制延迟(主从同步一个大 key 要传很久)、迁移困难(集群扩容时一个 key 没法拆 slot)。我见过一个 800 万元素的 Hash,每次 hgetall 直接让 Redis 卡顿 2 秒以上,下游服务全部超时。
规范 1-5:所有 key 必须设置过期时间,除非你明确知道它要永久存在。永不过期的 key 是最隐蔽的内存泄漏。很多团队上线时图省事,set 完就忘,结果 Redis 内存一天天涨,最后 maxmemory 触发淘汰,把那些还在用的热 key 全给淘汰了,引发缓存雪崩。我的建议是:所有缓存类 key 必须带 TTL,业务上确实需要长期保留的数据(比如分布式锁的 key 本身),也要单独评审。TTL 也不是越长越好,要根据数据更新频率来,一个订单状态缓存设 24 小时和一个商品详情缓存设 5 分钟,背后的合理性完全不同。
规范 1-6:统一序列化方案,禁止多种序列化混用。这个坑在 Spring 项目中尤其常见。有人用 JDK 序列化,有人用 Jackson,有人用 Fastjson,结果同一个 key 写入和读取的序列化方式不一致,反序列化直接报错。更隐蔽的是,序列化方式一变,老缓存全部变成"看不懂的字节",读不到就回源数据库,Redis 瞬间退化成一个昂贵的摆设。我们团队定死了一条规矩:缓存 value 统一 JSON 字符串,复杂对象用 ProtoBuf 或 Hessian 可以,但必须在配置中心里明确标注,禁止在代码里各自为政。另外还要注意,String 类型的 value 如果是数字,用整数存,不要用带引号的字符串,否则想用 INCR 做计数器的时候会直接报错。
1.2 大 key 扫描与拆分实战
规范定了,怎么落地?先做一次体检。Redis 自带的redis-cli --bigkeys可以扫描全库,把最大的 key 列出来。这个命令本质上是在跑 SCAN,不会阻塞线上,可以放心用。但要注意它统计的是运行期间的瞬时值,对于持续增长的 key,最好在低峰期跑,连续跑几天把趋势拉出来。
redis-cli -h 127.0.0.1 -p 6379 --bigkeys扫描出来之后,按规范 1-4 处理。以 Hash 大 key 为例,最常用的方案是哈希分片:把一个大 Hash 按业务 ID 或时间切分成多个小 Hash。比如用户行为数据原来是user:behavior:{uid}一个 Hash 存所有操作记录,可以拆成user:behavior:{uid}:20250601、user:behavior:{uid}:20250602,每个 Hash 天然控制在一个月的量级。ZSet 大 key 则考虑按分数段拆分,或者用多个 ZSet 存不同时间窗口的数据。List 大 key 尽量直接裁剪,超过长度限制的用LTRIM截断。如果一时半会改不动代码,可以先在客户端做逻辑拆分:读的时候拆成多次请求,写的时候也拆,至少把单请求的阻塞降下来。
2. 数据类型选型:用对的姿势操作 Redis
2.1 七种结构怎么选,场景对照表先收藏
规范 2-1:String 只放简单值和计数器。String 是 Redis 最基础的结构,适合缓存一个对象的 JSON、简单的值、Session。它最大的优势是原子自增自减:INCR、DECR、INCRBY是实现计数器、限流、库存扣减的天然工具。但我不建议把复杂对象塞进 String 后靠客户端去 parse,比如一个用户的多个属性拆成十几个 String key,性能和内存都很差,这种场景应该用 Hash。
规范 2-2:Hash 是对象存储的首选。一个用户的昵称、头像、等级、积分,用user:1001:info这个 Hash 的多个 field 来存,比 4 个 String key 省内存,而且可以只更新某一个 field,不需要整个对象序列化反序列化。Hash 的 field 数量控制在 1000 以内,性能最好。如果 field 超过这个量级,说明你需要的是分片 Hash 或其他结构。这里有个容易被小看的好处:Hash 在做字段级 TTL 时虽然不支持,但可以通过整体过期加存储冗余解决,比 String 拆 key 优雅得多。
规范 2-3:List 用于消息队列和时间线要有限度。List 的 LPUSH + BRPOP 组合是一个简单可靠的队列。但注意,List 作为队列一旦消费者挂了,消息会无限堆积,变成大 key。时间线场景也一样,用户发帖后把帖子 ID LPUSH 到 List 里,如果用户发帖频繁,List 会膨胀。我在规范里强制要求:List 必须设置长度上限(用 LTRIM 裁剪)或者消费者必须监控队列深度。如果对消息可靠性要求高,别在这里硬扛,直接上 Stream 或专业消息队列。
规范 2-4:Set 擅长去重,但要注意运算成本。Set 天然去重,适合标签系统、好友关系、已读列表。SINTER、SUNION 这些集合运算很强大,但时间复杂度是 O(N),N 是集合大小。两个上百万元素的 Set 做交集,一次操作就能把 Redis 卡住。建议:大集合做交集放到离线任务里算好再写入,或者提前用位图、HyperLogLog 估算规模。另外,Set 的每个成员也有内存开销,几千万个短字符串的 Set,内存会远超你的想象。
规范 2-5:ZSet 是排序场景的万能选手,不只是排行榜。排行榜只是 ZSet 最基础的用法。拿分数做时间戳、成员做业务 ID,ZSet 就能变成延迟队列、滑动窗口限流、在线用户列表。比如延迟队列:任务到期时间戳作为 score,消费者ZRANGEBYSCORE取当前时间之前的成员。这个用法我用了很久,比定时扫描数据库靠谱得多。但要注意,ZSet 的 score 是双精度浮点,别拿它存超过精度的整数 ID,否则会出现精度丢失导致排序错乱。
规范 2-6:Bitmap 和 HyperLogLog 是内存极客的最爱。统计日活用户、用户签到这类布尔型数据,用 Bitmap 一个用户只占 1 bit,一亿用户也只要 12.5MB。HyperLogLog 做 UV 统计,每个 key 固定 12KB 左右,误差 0.81%,大多数场景完全够用。这两类结构的问题是:它们只能提供"有没有""大概多少个"这种信息,拿不到明细,所以用之前要确认产品不需要下钻。
规范 2-7:Stream 才是真正面向消息队列的结构。很多人还在用 List 当队列,但 Redis 5.0 之后推出的 Stream 才是正解。它支持消费组、消息确认、未读消息追踪、消息持久化,语义完整。我在实时数据管道里用它做轻量级消息中转,比 List 方案省掉了大量自研的 ack 逻辑。
2.2 反直觉的选型案例:ZSet 也能当延迟队列用
我实际做过一个订单超时关单的需求,最初方案是每笔订单一个 TTL key,TTL 到了触发 key 过期事件。但 Redis 的过期事件推送并不可靠,尤其在 key 量大的时候会丢。后来改用 ZSet 延迟队列:订单创建时间 + 超时时长作为 score,订单 ID 作为 member,消费者每 5 秒拉一次到期订单执行关单。这个方案跑了一年多,没有丢过一笔订单,而且实现只有几十行代码。这个案例说明,选型不能只盯着教科书上的"ZSet 就是排行榜",数据结构是死的,组合思路是活的。
3. 内存治理:让有限的内存花在刀刃上
3.1 淘汰策略与内存控制,六条规范一次说清
规范 3-1:必须设置 maxmemory,并明确淘汰策略。很多 Redis 不设 maxmemory,内存满了直接拒绝写入,业务表现为"Redis 怎么突然写不进去了"。缓存场景我推荐allkeys-lru,让 Redis 自动淘汰最久没用的 key;如果只是某个业务域用 Redis,且所有 key 都有明确的 TTL,可以用volatile-ttl优先淘汰即将过期的 key。禁止在需要 Redis 做持久存储的场景使用noeviction,更不要在使用纯缓存场景选择noeviction导致写入失败。这里有张决策表可以参考:
| 业务场景 | 推荐策略 | 原因 |
|---|---|---|
| 纯缓存,key 可丢失 | allkeys-lru | 自动淘汰冷数据 |
| 缓存为主,部分 key 持久化 | volatile-lru | 只淘汰带 TTL 的缓存 key |
| 预占内存,严格按 TTL 淘汰 | volatile-ttl | 优先移除最先过期的 key |
| 数据稀缺,不能丢 | noeviction | 内存满后拒绝写入,保护已有数据(需配合告警) |
规范 3-2:禁用危险命令,用 rename 锁死。KEYS、FLUSHALL、FLUSHDB、MONITOR这些命令在生产环境就是定时炸弹。一个手抖的FLUSHALL能让整个缓存全灭,一次不当的KEYS *能让 Redis 阻塞几秒。标准做法是在配置里把这些命令改名:
rename-command KEYS "" rename-command FLUSHALL "" rename-command FLUSHDB "" rename-command MONITOR "" rename-command SHUTDOWN ""注意,rename-command在 Redis 7.0 之后改成了rename-command配 ACL,但思路一样。如果业务确实需要KEYS,用SCAN替代,一次性拿完和分批拿完,对单线程的影响天差地别。
规范 3-3:合理利用小对象编码。Redis 的 Hash、List、ZSet 在小规模时用的是紧凑编码,比如 listpack、intset,内存可以省 80% 以上。默认参数一般不用动,但如果你的业务 Hash 总是恰好超过hash-max-listpack-entries(默认 128),可以按需调大。不要盲目把阈值调到几十万,那会让一次操作变成大 key 操作,性能适得其反。判断是否生效用OBJECT ENCODING key查看,listpack/intset开头说明是紧凑编码,hashtable开头说明已经转成常规结构了。
规范 3-4:管理内存碎片,关键指标是 mem_fragmentation_ratio。Redis 内存碎片率 = used_memory_rss / used_memory。正常情况下在 1.0 到 1.5 之间。如果持续大于 1.5,说明碎片过多,常见原因是大量 key 频繁创建删除。可以重启节点让 Redis 重新整理内存,或者用MEMORY PURGE手动整理 jemalloc 的空闲页。如果碎片率小于 1.0,说明发生了内存交换(swap),赶紧检查系统内存是不是不够了,Redis 一旦 swap,性能会惨不忍睹。
规范 3-5:建立内存监控和容量预警。最实用的三个指标:maxmemory使用率、mem_fragmentation_ratio、单节点 key 数量。我要求监控系统每小时记录一次INFO memory的used_memory,使用率超过 80% 触发告警。为什么是 80%?因为做内存扩容、迁移、rolling restart 都需要余量,等到 95% 才处理,业务早就开始抖了。容量规划上也别抠,一个核心业务 Redis 节点,预留 30% 的缓冲空间是底线,因为你永远不知道运营活动什么时候来。
规范 3-6:大 value 分拆才谈后续优化。内存治理的第一步永远是去掉大 value。一个 5MB 的 String 存在 Redis 里,压缩比再高也压不出太多空间。先定位,再拆分,然后才是考虑换编码、换结构这些细节。顺序不能反,否则你优化半天,一个大 key 就把收益清零了。
3.2 内存碎片率到底怎么读
有一次我们一个节点内存使用率只有 50%,但INFO memory显示 RSS 已经是物理内存的 90%,敏感一点的人立刻能定位到碎片问题上。当时排查发现是短信验证码这种短生命周期 key 产生的大量分配释放,jemalloc 没有及时把内存归还给操作系统。处理办法是:先调低maxmemory触发淘汰,再滚动重启节点,碎片率就从 1.8 降到了 1.1。如果不想重启,调jemalloc的MALLOC_CONF=dirty_decay_ms参数也能缓解,但要重启进程才生效,所以多数时候滚动重启反而更省事。
4. 持久化与高可用:数据安全从配置抓起
4.1 六条规范,把 RDB、AOF、主从、集群一次理清
规范 4-1:RDB 与 AOF 的双写策略要分场景。默认配置里 RDB 是开启的,AOF 默认关闭。缓存场景可以只开 RDB,丢一点数据无所谓;但如果是订单状态、账户余额这类数据,必须开 AOF。我的推荐配置是appendonly yes+appendfsync everysec,每秒刷一次盘,极端情况丢 1 秒数据,大部分业务都能接受。fflush 频率太高(always)会让写性能大幅下降,除非你对数据安全零妥协,否则不要用。
save 900 1 save 300 10 save 60 10000 appendonly yes appendfsync everysec规范 4-2:AOF 重写机制必须理解,否则会被"卡一下"坑到。AOF 文件持续增长到一定阈值会触发 BGREWRITEAOF,Redis fork 子进程做重写。fork 期间主进程要拷贝页表,内存越大 fork 越慢,期间会有短暂的阻塞。我见过一个 30GB 的节点,fork 花了将近 2 秒,所有写入请求一起超时。规避手段:调低auto-aof-rewrite-percentage(从默认 100 调整到 200),让重写不那么频繁;重写尽量放在低峰期;也可以手动在主从切换时做滚动重写。
规范 4-3:主从复制要做,但要有主从不一致的预案。主从同步是异步的,这意味着从节点数据一定比主节点旧,只是差异多少的问题。如果业务容忍度极高,可以配置min-replicas-to-write和min-replicas-max-lag来保证写入时有足够健康的从节点,否则主节点拒绝写入。这不是降低可用性,是防止主节点数据异步复制到从节点之前,主节点挂了导致数据丢失。读写分离场景,建议从节点只承担可容忍延迟的读请求,强一致读必须走主节点。
规范 4-4:哨兵模式至少三个节点,且部署在不同物理机。哨兵的定位是监控和自动故障切换。两个节点会存在"脑裂"问题:主节点假死时,一个哨兵认为主挂了,另一个不认为,无法达成 quorum,无法触发切换。三个哨兵允许一个哨兵失联,场景更稳。quorum 设置 2 比较常见:两个哨兵同意主节点不可用,才发起切换。另外,哨兵所在机器不能全和 Redis 实例在一台物理机上,否则宕机就是全军覆没。
规范 4-5:集群模式用官方 Cluster 或 Proxy 方案,按业务场景选。Redis Cluster 的优点是去中心化、支持自动故障转移,默认 16384 个 slot。它的缺点是:不支持多 key 操作跨 slot(MGET 多个 key 要确保 hash tag 在同一个 slot),批量 pipeline 也会受槽位限制。对业务侵入小的方案是 Codis 或代理层,对运维要求高的团队直接用原生 Cluster。我的经验是:key 量百万级以下、单节点内存能扛住的场景,哨兵模式已经够了;单节点内存超过 32GB 或者写入吞吐成为瓶颈,再考虑 Cluster,别为了"用集群"而上集群。
规范 4-6:容器和 K8s 中部署 Redis,持久化与网络是关键。在 Kubernetes 里部署 Redis 集群,很多人第一次就栽在"Pod 重启后数据全没"。因为默认容器是无状态的,必须在 StatefulSet 里挂 PV(PersistentVolume),并且 AOF 和 RDB 的路径必须指向持久化目录。另一个常见坑是:Pod 重启后 IP 变了,Redis Cluster 节点互相注册的还是旧 IP。Redis 6.2 之后有cluster-announce-ip和cluster-announce-port配置,配合 Headless Service 的稳定 DNS,才能保证集群拓扑不变。我也是折腾过两轮才把 K8s 里的 Redis 集群跑稳,强烈建议先小规模验证再全量迁移。
4.2 一次 AOF 重写引发的阻塞复盘
那次故障被我记到现在。现象是主节点 CPU 和延迟指标每 10 分钟跳一次尖峰,客户端时不时报READ TIMEOUT。一开始以为是流量波动,查完才发现INFO persistence里aof_last_bgrewrite_status:ok,但latest_fork_usec高达 190 万微秒,也就是 1.9 秒。整个节点 28GB 内存,每次 fork 都要拷贝页表,主进程完全卡住。当时 AOF 文件已经 25GB,触发了自动重写阈值。处理办法是:把auto-aof-rewrite-percentage抬到 200,给 AOF 重写设置了业务低峰窗口,同时把这些大内存节点的 RDB 备份改到从节点上执行。从那之后,我把所有持久化相关的 fork 耗时就加入了监控告警,latest_fork_usec超过 500 毫秒就开始人工关注。
5. 缓存三大经典问题:穿透、击穿、雪崩的治理套路
5.1 六条规范,把三座大山挨个拆掉
规范 5-1:缓存穿透——布隆过滤器 + 空值缓存,双保险。穿透的意思是查询一个必然不存在的数据,每次请求都打到数据库。典型的攻击手段就是遍历不存在的 ID。布隆过滤器能快速判断"肯定不存在",成本极低。但你得接受它会误判,也就是说"可能存在"不代表真的存在,最终还是要查一次库。误判率可以靠error_rate参数控制,通常设到 1% 以内。另一个兜底方案是空值缓存:查不到的数据也写一个空值到 Redis,TTL 设短一点,比如 2~5 分钟,防止恶意 ID 批量穿透。注意空值缓存要设置 TTL,否则会堆积成内存垃圾。
规范 5-2:缓存击穿——热点 key 加互斥锁。击穿和穿透不一样,击穿是热点 key 在过期瞬间,大量请求同时打到数据库。最经典的解法是互斥锁:缓存不存在时先抢锁,抢到锁的线程去查库、回填缓存,其他线程等待重试。用 Redis 实现就是:
SET lock:hotkey 1 NX PX 30000设置 30 秒过期,避免持锁线程崩溃导致死锁。但要注意,回填缓存的操作时间必须小于锁过期时间,否则先回填的线程还在跑,锁已经过期,又有线程进来了。如果业务查询特别慢,考虑用"逻辑过期"方案:缓存里存一个过期时间字段,读到过期数据时去抢锁刷新,这样不阻塞读请求。
规范 5-3:缓存雪崩——过期时间加随机值 + 多级缓存。雪崩是大批 key 同时过期,或者 Redis 节点宕机,所有请求一起落到数据库。加随机值是最便宜的防御:TTL 不只是固定时间,而是在基础值上加一个随机偏移量,比如 300 到 600 秒之间随机,让过期时间错开。代码里实现就是TTL = 300 + random.nextInt(300)。多级缓存在 Redis 之上再加一层本地缓存,即使 Redis 整体不可用,本地缓存也能挡住一部分请求,给故障恢复争取时间。
规范 5-4:缓存一致性——Cache Aside 模式,先更新库再删缓存。这是最常见的缓存更新模式:读的时候先读缓存,没有就读库回填;写的时候先更新数据库,再删缓存。为什么不直接更新缓存?因为并发写和并发读交错时,更新缓存容易出现旧值覆盖新值。删缓存虽然会产生一次 cache miss,但代价比一致性错误低得多。这里有个细节:删缓存失败怎么办?我的方案是延迟双删:先删缓存,等几百毫秒,再删一次,把第一次删除后可能回填的旧值清掉。如果对一致性要求再高,用消息队列给缓存删除加一个重试通道。
规范 5-5:缓存粒度要按需而定。很多团队习惯把整个用户对象塞进缓存,只改了一个积分字段也要刷新整个对象。这导致缓存失效的代价很大、更新很频繁。正确做法是:按读取频率拆分缓存粒度。高频读取且只需要部分字段的数据,拆成多个小缓存;低频全量读取的数据,才考虑整对象缓存。一条经验值:如果一个缓存字段一天更新超过 100 次,就不要缓存它,否则每次更新都会引发一次缓存刷新,成本已经超过了收益。
规范 5-6:热点 key 要有治理预案,不能等问题爆发。热点 key 不像穿透那些是"设计问题",而是"流量问题"。比如大促时一个商品瞬间来了几十万请求,单 key 的 QPS 上限会卡住。预案有三层:第一层是给热 key 加二级本地缓存,挡住大部分流量;第二层是给热 key 做分片,把数据复制到多个 key 上,比如hotkey:01到hotkey:20,请求随机打散;第三层是限流降级,超出阈值直接返回降级数据。我用得最多的是本地缓存 + 分片,因为成本低、见效快。
5.2 缓存一致性为什么这么难
很多人问我:先更新缓存再更新库行不行?答案是:在并发下几乎必出问题。A 线程先更新了缓存为新值,B 线程随后用旧值覆盖了缓存,然后 A 再更新数据库为新值,数据库和缓存就永远不一致了。Cache Aside 的删缓存之所以能成立,是因为"删"比"写"冲突概率低:删掉后读请求会回源查询数据库,拿到的就是最新值,旧值没有机会留在缓存里。这也是"先更新库再删缓存"和"延迟双删"的核心逻辑。我们团队因为这个坑付出过一次线上事故,之后我直接把规范 5-4 写进了 Code Review 检查项,谁写缓存更新逻辑都要走一遍这个流程。
6. 分布式锁与并发控制:锁不好写,但可以写得对
6.1 六条规范,把分布式锁写成生产级
规范 6-1:分布式锁的前提是锁必须最终可释放。最简单的锁实现是SETNX,但只 SETNX 不设过期时间,会出现持锁进程崩了锁永远不释放的问题。所以第一步必须是SET key value NX PX 30000,一个命令原子地完成"不存在才设置"+"设置过期时间"。不要分两步执行 SETNX 然后 EXPIRE,因为两步之间可能出事故,这属于分布式锁最基础的常识。
规范 6-2:锁的 value 必须是唯一的,防止误删。很多人写释放锁的代码是这样的:
DEL lock:xxx这是隐患。如果线程 A 的锁已经过期,线程 B 拿到了锁,然后线程 A 的业务才跑完,执行 DEL 就把 B 的锁删了。正确做法是 value 用唯一 ID(UUID 或业务流水号),释放前先比对再删,必须用 Lua 保证原子性:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end这段 Lua 脚本就是用"自旋"方式释放锁的唯一正解,网上你能看到的正确版本基本都是它。
规范 6-3:锁的过期时间要拍脑袋,但要拍得有依据。锁过期时间太短,业务没跑完锁没了,别的线程进来了;太长,一旦持锁线程卡住,后面的请求全得等。我给团队的经验是:先估算业务最坏耗时,乘以 3 到 5 倍作为锁的过期时间。如果业务时间波动很大,用 Redisson 的看门狗自动续期机制,默认每 10 秒续期一次,直到业务结束。
规范 6-4:可重入锁必须自己实现或用 Redisson。同一个线程多次进入同一个锁的业务场景很常见,比如一个方法加了锁,它内部又调用另一个加了同一个锁的方法。原生 SETNX 不支持可重入,会直接死锁。Redisson 的RLock支持可重入,而且底层用 Hash 记录重入次数。
RLock lock = redissonClient.getLock("order:pay:" + orderId); boolean locked = lock.tryLock(3, 30, TimeUnit.SECONDS);还有一点:Redisson 的看门狗默认 30 秒续期一次,业务结束不要忘了unlock(),否则会一直续期到超时上限。
规范 6-5:锁粒度能小则小,别锁整个业务域。我看到过很夸张的写法:做订单支付时锁的 key 是整个用户 ID,结果这个用户同时下两笔订单时,第二笔的加锁直接失败。锁粒度应该对应到具体资源:支付锁加在订单号上,库存锁加在 SKU ID 上,用户积分锁才加在用户 ID 上。锁粒度铺太大,并发能力骤降;锁粒度太小,锁的数量爆炸、管理复杂度上升。一条经验值:如果锁数量大概率超过每秒一万个,你要反思是不是把多个资源拆得太细了。
规范 6-6:RedLock 的争论有个务实结论:大多数业务用不上。RedLock 是 Redis 作者提的"红锁"方案,要求锁在多个独立 Redis 节点上同时获取成功才算拿到。理论上它防止了单点故障,但工程上争议很大:它依然依赖时钟、网络,而且性能开销很大。我的建议是:普通业务场景用单节点 Redis 锁 + 合理的过期时间 + 手动续期方案,完全够用;只有你确实需要跨多个 Redis 实例的强一致锁,再考虑 RedLock 或换成 ZooKeeper / etcd。别让团队为了一个"看起来更高级"的锁方案把复杂度抬上去。
6.2 一个典型的锁失效案例
有一次我们做秒杀,代码逻辑是:抢锁成功 → 查库存 → 扣减数据库 → 回写 Redis → 释放锁。结果上线没多久,库存超卖了。排查发现锁过期时间是 10 秒,但查库加扣减逻辑因为慢查询跑了 15 秒,锁早就没了,导致多个线程同时进入。修复方案是把锁过期时间从静态改成动态:根据最近一次慢查询的 P99 耗时动态计算,最低 30 秒,超时后自动续期。同时把查库存的 SQL 索引加上,从根上消除慢查询。这个案例告诉我们:分布式锁不是加上了就完事,它的过期时间、续期机制和业务执行时长必须联动考虑。
7. 监控、容量与故障排查:线上问题从"防"开始
7.1 六条监控运维规范,覆盖日常和应急
规范 7-1:慢查询日志必须开,而且要主动分析。Redis 的慢查询日志不是传统 SQL 那种慢日志,它记录的是超过阈值的命令执行时间。配置两个参数:
slowlog-log-slower-than 10000 slowlog-max-len 128这个10000单位是微秒,也就是命令执行超过 10 毫秒就记录。查询用SLOWLOG GET,默认返回最近 10 条。我要求开发环境的阈值设到 5 毫秒,生产设 10 毫秒,每天拉一次慢日志看有没有新命令上榜。慢日志的意义不只是性能问题,它往往暴露了数据结构选型错误——频繁出现的HGETALL和ZRANGE大范围操作,就是该做拆分的信号。
规范 7-2:可视化工具别乱连生产库,MONITOR 命令尤其危险。RedisInsight 和 Another Redis Desktop Manager 是目前用得最多的可视化管理工具,前者功能多、适合分析,后者轻便、适合快速查询。但无论是哪个工具,直接连生产 Redis 都要谨慎。特别是带 MONITOR 功能的工具,它会让 Redis 把所有命令实时打印出来,高流量时直接把 CPU 打满。我们规定:生产环境只能通过只读账号连接可视化工具,禁止使用管理员权限执行任何写操作。DBA 操作走命令行审计,操作记录留档。
规范 7-3:Redis 访问要做到"客户端连接可控"。客户端连接数突增是 Redis 挂掉的又一隐形原因。timeout参数如果为 0,空闲连接永不释放,连接数会持续累积到maxclients上限,然后新的客户端直接连接失败。建议设置timeout 300,让空闲 5 分钟以上的连接被服务端主动断开。客户端侧也要配置合理的连接池大小和超时时间。我特别想提醒的是 Java 里 Lettuce、Jedis 的默认配置:Lettuce 默认超时 60 秒,如果 Redis 卡顿,客户端线程就傻等 60 秒,任务堆积成雪崩。把超时时间调到 300~1000 毫秒,配合连接池的maxTotal和maxIdle设置,让失败快速暴露而不是无限等待。
规范 7-4:集群运维从在线迁移到自动化检查,都要有预案。Redis Cluster 在线扩缩容靠redis-cli --cluster reshard,但要严格控制批量迁移量,一次迁移的 slot 数不要超过 512 个,否则节点间数据复制占用大量网络带宽。迁移前必须检查每个节点的 key 数量分布,尽量均匀。迁移完成以后用redis-cli --cluster check验证集群状态,看到All 16384 slots covered才算完成。
规范 7-5:备份策略要测试恢复,而不是只在文档里写着。备份 = RDB 文件定时拷贝 + 定期演练恢复。很多团队的备份文件一堆,但没有一个人实际验证过能恢复。我的做法是:每周做一次恢复演练,随机挑一个备份文件在测试环境启动 Redis,用INFO keyspace校验 key 数量,用抽查 key 校验数据正确性。别等真出事了才发现备份文件损坏或者版本不兼容。
规范 7-6:版本升级走"先小后大"的路径。Redis 小版本升级一般兼容性较好,但从 5.x 到 6.x、6.x 到 7.x 要注意配置和行为变化。比如 Redis 7.0 对rename-command和 ACLE 的改动就不少,redis.conf里很多参数弃用。升级顺序:先在测试环境跑一遍回归,再在从节点上升级验证,逐步切换主节点。不要跳大版本,不要在生产直接升级后重启,这是最基础的红线。
7.2 一次 Lettuce 超时问题排查记录
有次下游反馈某个服务接口偶发抖动,日志里报RedisCommandTimeoutException,错误信息是典型的redis command timed out; nested exception is io.lettuce.core.rediscommandtimeoutexception。一开始以为是 Redis 性能问题,看监控发现 Redis CPU 和延迟都不高,问题在客户端侧。Lettuce 是 Netty 实现的异步客户端,如果事件循环线程阻塞,所有共享连接的请求都会超时。当时排查出的原因:连接池太小,高峰期线程竞争连接,加上一个慢查询把连接占住,后续所有请求排队超时。修复方案:调大maxTotal、调小maxIdle和minIdle,并给 Lettuce 设置了合理的读超时(比如 500ms)和连接池等待超时(比如 200ms)。从那以后我把这类客户端侧超时配置加入了上线前 Checklist,比出了事故再定位高效得多。
8. 落地执行:43 条规范如何转化为团队实践清单
8.1 建立可执行的检查清单,别让它停留在博客里
43 条规范读完容易,落地难。我的做法是把它们做成一张Redis 使用规范检查表,按照 7 个维度逐条列出来,每条包含:规范编号、具体说明、检查方式、责任人。比如:
| 编号 | 规范项 | 检查方式 | 责任人 |
|---|---|---|---|
| 1-4 | 严禁大 key | redis-cli --bigkeys 定期扫描 | 服务负责人 |
| 3-2 | 禁用危险命令 | 检查 redis.conf rename-command | DBA |
| 6-2 | 锁 value 唯一 | Code Review 检查 DEL 前校验 | 开发负责人 |
| 7-1 | 慢日志分析 | 每日拉取 slowlog 并归档 | DBA |
这张表不用一次性全量执行,可以先挑最容易出事的 10 条重点治理,比如大 key、危险命令、无 TTL、分布式锁 value 唯一性这四条,控制住以后再把范围扩大到全部 43 条。实践清单本身要放在团队 Wiki 或代码仓库里持续更新,我发现过了半年后大家慢慢都会忘掉细节,所以最好半年做一次全员复盘,把新踩的坑补进规范里。
检查方式也要尽可能自动化。我分享一下我们目前用的巡检脚本思路:用redis-cli和SCAN + TYPE + OBJECT ENCODING组合扫描 key 分布,统计每个业务域的 key 数量和总大小;用SLOWLOG GET拉慢查询;用INFO memory记录内存使用率和碎片率。整个巡检每天定时跑一次,输出 JSON 格式报告到监控平台。这套自动化巡检的价值在于:它把 43 条规范里能机器检查的部分全部转化为持续监控,而不是靠人肉翻命令。
8.2 从规范到习惯:Code Review 和上线评审要把 Redis 列为必查项
规范写进文档很容易,真正难的是让每个开发同学写代码时按规范来。我们在 Code Review 模板里增加了 Redis 使用自查清单,包括:是否设置 TTL、是否避开大 key、序列化是否统一、分布式锁释放前是否校验 value、缓存更新是否按 Cache Aside 模式。另一个卡点在上线评审:凡是涉及 Redis 操作的需求,都必须说明用了哪些 key、什么数据类型、TTL 多久、风险点是什么,评审现场过一遍。这听起来很重,但实际坚持半年后,团队里 Redis 相关的线上问题明显下降,而且新同学上手时多了一份直接参考的工程手册。
最后再分享一个我在多个团队验证过的小技巧:不要强推"所有人都精通 Redis",而是要有一个 Redis 规范负责人,他负责维护实践清单、巡检脚本、事故复盘。别人踩坑,他把坑写进规范,其他人就不用再踩一遍。这套机制,比任何一场培训都管用。