从“加个缓存”到稳定上线:把 Redis 放回真实业务里

下午四点,运营同事在群里发来一条消息:"晚上有场活动,首页会推这几件商品,接口能不能再快一点?"

假设你负责这个商品系统。打开监控,发现详情接口确实有些吃力:查商品、查店铺、拼装展示信息。同一只马克杯被反复浏览,数据库和应用也跟着重复劳动。

这时,很容易有人提出:"加个 Redis 吧。"

把结果存进去,再写一段读取逻辑,看起来并不复杂。但接下来的需求通常不会停在这里:价格改了,页面要及时更新;商品火了,缓存不能突然失效;活动结束了,数据要清理;服务重启了,关键状态还得找得回来。

Redis 的很多知识,正是在这些日常工作里连起来的。我们就沿着这个商品活动,把常用的数据结构、缓存策略、并发控制、持久化和高可用串成一条线。理解到足够做出合理选择,就已经很有价值。

1. 商品页慢下来时,先看系统在重复做什么

Redis 是一个以内存为主要工作空间、支持多种数据结构的数据存储系统。它可以用来做缓存,也可以承担计数、排行榜、会话存储和事件处理等工作。Redis 数据类型概览

在这个商品系统里,可以让 MySQL 保存权威业务数据,让 Redis 保存一份可以重新生成的商品展示结果。读取时先查 Redis,未命中再查数据库,并把结果写回缓存。这就是常见的 Cache Aside,也叫旁路缓存。Cache-Aside 模式

text 复制代码
用户打开商品页
      │
      ▼
应用查询 Redis ── 命中 ──→ 返回商品信息
      │
     未命中
      ▼
查询数据库 ──→ 写回 Redis,设置有效期 ──→ 返回商品信息

这个过程像把常用资料放到办公桌边。完整档案仍然妥善保存,只是不必每核对一个数字,都去重新翻找和整理。

这里需要避开一个常见误解:MySQL 查询并不总要访问磁盘。InnoDB 本身就有 Buffer Pool,会把数据页和索引页缓存在内存中。MySQL Buffer Pool

引入 Redis 的收益,还可能来自省掉重复 SQL、关联查询、结果拼装和业务计算。例如,商品页的展示对象由几张表共同生成,缓存这个最终结果,就能让后续请求少做一整段工作。

所以,要先找到值得缓存的数据:访问是否频繁,生成成本是否高,是否允许短暂陈旧,失效后能不能重建。一个几乎不会被再次读取的结果,缓存它未必划算;最终成交价和库存确认,则需要单独设计可靠的数据路径。

缓存是否有效,最终要在自己的请求分布和压测结果里判断。脱离数据大小、命令类型、网络和持久化配置,直接说某个组件"只能几千 QPS"或"肯定十万 QPS",很难指导实际容量规划。

2. 数据结构选对了,很多业务逻辑会变简单

第一次用 Redis,通常是把对象转成 JSON,然后用 String 存起来。这适合整体读取的商品详情,但需求一旦变成"只改一个字段""判断是否收藏""查看我的排名",就值得换个角度组织数据。

常见结构 适合表达的数据 业务例子
String 文本、序列化结果、计数值 商品详情缓存、访问次数
Hash 一个对象的多个字段 用户资料、商品属性
List 可从两端操作的元素序列 最近浏览记录、简单待处理列表
Set 不重复的成员集合 收藏列表、活动报名名单
ZSet(Sorted Set) 带分数、按分数排序的唯一成员 积分榜、商品热度榜

List 可以通过不同的入队、出队方向表达队列或栈;Set 不应被当成有序集合使用。它们提供的业务语义,比内部具体怎么存更值得先记住。Redis 数据类型对比

假设活动增加了积分排行榜。用 ZSet 保存用户 ID 和积分,就可以直接更新分数、查询前十名以及某个用户的排名。如果把整张榜单存成大 JSON,每次加分都先取出、排序、再写回,传输量和并发处理都会更麻烦。Sorted Set 官方说明

还有几种经常能派上用场的能力:Bitmap 可以紧凑地表示签到等二值状态;HyperLogLog 适合估算去重人数,但不能提供完整名单或精确计数;GEO 可以查询附近位置;Stream 则用来记录和消费事件序列。选型时需要确认部署版本提供了哪些能力。Bitmap、HyperLogLog、类型概览

底层结构,理解取舍就够用了

业务里使用的 String、Hash、ZSet,是对外的数据类型;SDS、listpack、哈希表、跳表,则属于内部实现。这两层不要混在一起。

SDS 是 Redis 使用的动态字符串实现。显式记录长度,使获取长度不必遍历字符串;按长度处理内容,也让它能够保存包含零字节的数据。理解这些设计动机,比背下某一版结构体的所有字段更有帮助。SDS 的设计说明

对于小规模集合,紧凑编码可以减少指针和对象开销;规模变大后,Redis 又可能切换到更适合访问的数据表示。这是在内存占用和操作成本之间做取舍。

阅读旧资料时尤其要看版本:Redis 7.0 起,Hash、ZSet 的相关紧凑编码配置已从 ziplist 转向 listpack;Redis 7.2 又增加了 Set 的 listpack 配置。因此,"Hash 一定是 ziplist 或哈希表""Set 永远只有两种编码"都不适合当成跨版本结论。Redis 内存优化与编码配置

对刚开始建立知识体系的人,先掌握"同一种类型可能采用不同内部表示"就够了。遇到真实的内存或性能问题,再沿着当前版本继续深入。

3. Redis 很快,但它也怕一个请求占着不走

Redis 的速度来自多方面:以内存为主要工作空间,提供适合操作的数据结构,并通过事件驱动和 I/O 多路复用高效处理连接。常见命令的主要执行路径串行处理,也减少了这条路径上的并发协调成本。

"Redis 是单线程"说的通常是命令执行模型,并不代表整个进程只有一个线程。后台任务会使用线程,Redis 6.0 起也支持用 I/O 线程分担网络读写。Redis 线程模型说明

这套设计也带来一个直接影响:某个命令执行得很久,其他请求就可能跟着等待。一个返回巨大集合的操作、一段长时间运行的脚本,都可能把延迟推上去。Redis 延迟诊断

还有一些慢,发生在 Redis 外面。比如商品页要读取二十个 Key,应用逐个发送命令、逐个等待结果,网络往返的时间就累积起来了。

Pipeline 可以让客户端连续发送一批命令,再读取响应,减少逐条等待带来的开销。它不保证这些命令作为一个整体原子执行,也不要求服务端必须等整批全部执行完才开始回复;这些行为不能混为一谈。批次过大还会增加缓冲压力,应根据数据量控制。Redis Pipeline

优化时,最好先分清时间花在命令执行、网络传输、序列化,还是连接池等待上。只有找到这一段,调配置才有方向。

4. 缓存要有有效期,也要有更新规则

商品详情缓存可以从这样一条命令开始:

redis 复制代码
SET shop:product:1001:detail:v1 '{"name":"马克杯","price":39}' EX 300

这里将数据写入 Redis,同时设置 300 秒有效期。时长只是演示值,实际要按数据的新鲜度要求和回源成本决定。

把写入与过期时间放在同一条命令里,能避免"写成功了,还没设置有效期程序就退出"的中间状态。另外,普通 SET 覆盖已有值时会清除旧 TTL;如果希望继续保留,需要明确使用相应选项,例如 KEEPTTL。SET 命令

Key 的名字也值得认真设计。业务、对象 ID、用途和格式版本,可以让排障更容易。如果结果还受租户、语言或用户身份影响,这些维度也应体现在 Key 中,否则容易把不同请求的结果混用。缓存 Key 设计示例

TTL 管理的是数据有效期。Redis 会在访问时检查 Key 是否过期,也会主动检查并清理过期数据,所以到期并不等于内存在同一瞬间完成回收。EXPIRE 的过期机制

内存淘汰则处理另一个问题:空间达到限制后,哪些数据要先离开。常见策略包括偏向保留近期访问数据的 allkeys-lru、偏向保留高频数据的 allkeys-lfu,以及只从有 TTL 的 Key 中选择对象的 volatile 系列。noeviction 不主动淘汰,但内存不足时相关写入会失败。内存淘汰策略

因此,一个设置了十分钟 TTL 的 Key,也可能在十分钟之前被淘汰。可以随时重建的缓存,与丢失后影响业务的状态,不宜只因为"都能放进 Redis"就使用同一套容量和淘汰规则。

价格更新之后,两个存储之间会出现时间差

运营把马克杯改成 35 元,数据库已经更新,页面却还显示 39 元。到了这里,缓存就从性能优化进入了数据一致性的范围。

常用的写入流程是:先提交数据库更新,再删除相关缓存,后续读取时重新加载。这个顺序能避开先删缓存后,其他请求趁数据库尚未更新又把旧值填回去的常见窗口。Cache-Aside 的更新流程

但它仍然不是时刻一致的保证。数据库提交和缓存删除之间存在时间差,删除也可能失败。把两个并发请求排成时间线,还能看到另一种情况:

text 复制代码
请求 A:缓存未命中,从数据库读到旧价格
请求 B:更新数据库,删除缓存
请求 A:稍后才把刚才读到的旧价格写回缓存

这意味着,需要结合业务容忍度补上 TTL、可靠重试、版本控制或针对性的并发协调。使用有延迟的数据库读副本回填缓存,也可能重新带入旧数据。缓存与最终一致性

当更新链路较多时,可以考虑通过 binlog 等变更事件统一驱动失效,但事件消费本身也需要处理延迟、重复、顺序和失败补偿。仅仅增加一个消息队列,无法自动消除所有不一致窗口。

商品介绍可以允许短暂延迟,最终成交价、扣款与库存确认应依据权威数据和事务规则。设计要细化到具体业务动作,而不是给整个系统统一贴上"强一致"或"最终一致"的标签。

5. 活动流量上来时,三个相似的名词对应三种麻烦

平时缓存未命中,查询一次数据库就结束了。活动期间,同一时刻的未命中可能来自成千上万个请求。

缓存穿透,是请求在找不存在的数据。 比如大量请求查询根本没有的商品 ID。可以先做参数校验,并把确实不存在的结果缓存成短期空值标记,避免反复查库。新商品创建时,也要考虑旧空值缓存的清理。

数据规模较大时,可以增加布隆过滤器。它能够判断一个元素"可能在集合中"或者"不在已经记录的集合中",前者允许误判。它对现实业务是否可靠,还取决于新增数据有没有及时同步进去,不能把"过滤器里没有"无条件等同于"数据库里没有"。Bloom filter 官方说明

缓存击穿,是一个热点数据失效后,大家一起回源。 首页主推的商品刚好过期,大量请求同时查询数据库,原本平滑的访问就变成了一次集中冲击。

可以合并同一个 Key 的加载请求,让少量请求负责重建,其他请求短暂等待;获得重建资格后,再检查一次缓存,避免重复加载。业务允许时,也可以保留旧值并异步刷新,但需要明确旧值最多能接受多久。缓存不存在时,还得另有冷启动方案。

缓存雪崩,则是一大片缓存同时失效,或缓存服务整体不可用。 批量预热时设置完全一样的 TTL,可能把下一轮回源也安排在同一秒。随机化有效期、分批预热和限制重建并发,都能帮助分散压力。缓存惊群与应对方式

"Redis 挂了就全部查数据库"也需要评估。数据库未必接得住原本被缓存吸收的全部请求,回源必须受到限流、超时和熔断等机制约束。缓存故障与回源压力

活动页面暂时不显示推荐榜单,与用户无法提交订单,影响并不一样。提前决定哪些功能优先保留,能让降级方案具体得多。

6. 并发来了,要看清原子性管到哪里

活动增加了领券计数。一个请求先 GET,读到 10,再加一并 SET 回去;另一个请求也恰好读到 10,最后计数就可能只变成 11。

这种场景可以直接使用 INCR。单条计数命令避免了客户端读、改、写之间的交错,但应用连续发送的多条命令不会因此自动成为一个原子操作。

机制 主要用途 需要记住的边界
Pipeline 减少网络往返 不提供整批原子性
MULTI / EXEC 集中执行一组命令 执行阶段出错,不会自动撤销之前的修改
WATCH 检测关注的数据是否变化 冲突后需要应用决定是否重试
Lua 在服务端完成带判断的短逻辑 执行期间会占用 Redis,不能容纳长耗时业务

Redis 事务不提供数据库式的自动回滚。DISCARD 可以放弃尚未执行的事务队列,无法撤销已经完成的写入。Redis 事务

Lua 可以把"检查计数、判断限制、更新状态"放在一次服务端执行里,避免其他客户端命令插入其中。这里的原子执行强调不被穿插;脚本发生运行时错误,也不能假定此前写入会自动回滚,更不会把 MySQL 更新和外部接口调用一起纳入事务。Redis Lua 脚本

秒杀系统可以利用这些能力做入口限流、库存预占或请求削峰。但库存预占成功以后,订单创建可能失败;订单请求超时以后,也可能其实已经成功。因此还需要幂等、补偿和对账。容量规划也要考虑热点商品是否集中在同一分片,不能简单拿单节点吞吐乘以节点数。

分布式锁需要处理"我以为锁还在"的时刻

常见的单实例加锁方式是:

redis 复制代码
SET lock:product:1001 "本次请求生成的唯一标识" NX PX 10000

NX 表示不存在时才设置,PX 指定租期。标识应由程序为每次加锁生成,十秒仅为示例。解锁时,要原子地完成"确认标识属于自己,再删除",避免误删别人后来取得的锁。Redis 分布式锁

假如请求 A 停顿到租期过期,请求 B 已经拿到新锁,A 恢复以后仍可能继续执行。标识校验能防止误解锁,但不能单独阻止过期持有者继续修改业务数据。

Redisson 等客户端提供可重入锁和 Watchdog 续期机制,能减少正常长任务因固定租期结束而失去锁的情况。不过,自动续期取决于获取锁的方式,显式设置固定 leaseTime 与使用 Watchdog 的语义不同。Redisson 锁与配置、Watchdog 配置说明

如果整个 JVM 发生 Stop-The-World 暂停,同一进程里多开几个续期线程也无法在暂停期间继续运行。这是进程暂停的边界,不能靠增加同进程线程数量消除。Oracle GC 调优说明

对于重复执行代价很高的操作,最终写入数据的地方仍要校验业务号、版本或条件;需要隔离过期持有者时,可以考虑由受保护资源验证递增的 fencing token。锁、幂等和数据约束,应共同服务于业务正确性。

7. 服务重启以后,持久化决定数据能回来多少

Redis 的两种经典持久化方式,可以用"拍快照"和"记变更"来理解。

RDB 保存某个时刻的数据集快照;AOF 记录写操作,恢复时据此重建状态。RDB 可能丢失最近快照之后的修改,AOF 的数据保护程度则受刷盘策略影响。

AOF 策略 主要取舍
always 更积极地同步刷盘,通常需要承担更高写入延迟
everysec 按秒安排刷盘,通常接受最近约一秒写入可能丢失的窗口
no 由操作系统决定刷盘时机,故障时丢失窗口更难控制

AOF 重写会根据当前数据生成较精简的恢复内容,避免操作历史无限积累。Redis 7.0 起采用多部件 AOF,由基础文件、增量文件和 manifest 共同管理;旧版"一个 AOF 文件加重写缓冲区"的讲解不能直接套到所有版本上。Redis 持久化

后台保存也不是零成本。fork、写时复制带来的额外内存,以及磁盘 I/O,都可能影响运行表现,需要给容量留出余量。持久化相关的延迟诊断

如果丢失的只是能从数据库重建的商品缓存,恢复策略应重点考虑回源压力;如果保存的是难以重建的业务状态,则需要更严格的持久化、备份和恢复演练。

这也是为什么"开启持久化"还不能替代完整的恢复方案。重启后能否读取文件、恢复要多久、恢复期间哪些接口可用,都值得在上线前实际演练一次。

8. 复制、哨兵和集群,分别解决不同的增长问题

主从复制让副本跟随主节点的数据变化。首次同步或无法补齐差异时,需要全量同步;短暂断连之后,如果复制历史匹配、缺失的数据仍在复制积压缓冲区中,就有机会通过部分同步追上进度。

默认复制是异步的,所以副本可能暂时落后,故障切换也可能丢失尚未复制过去的写入。增加副本并不自动得到强一致性。Redis 主从复制

在此基础上,可以按需要选择部署方案:

方案 主要作用
主从复制 数据冗余,以及允许旧读场景下的读扩展
Sentinel(哨兵) 监测主从状态、协调故障切换、帮助客户端发现新主节点
Redis Cluster 将数据分布到多个分片,扩展容量与吞吐,并提供相应的故障转移机制

Sentinel 适用于非 Cluster 的高可用部署,不负责把一份数据拆到多个主节点。切换过程中,客户端的连接恢复、超时和重试同样需要正确处理。Sentinel 官方说明

Redis Cluster 通过 16384 个哈希槽组织 Key。多 Key 命令、事务和脚本通常要求相关 Key 位于同一槽,必要时可以使用 hash tag 让它们共同定位,但也要避免由此造成负载集中。Redis Cluster 规范

新增节点与迁移数据是两个步骤。原生 Cluster 加入一个空主节点后,还需要分配或迁移槽位;"多加一台机器"不代表已有数据和压力立即自动均摊。Redis Cluster 扩容

而且,一个极热门商品对应的单个 Key 仍然属于一个分片。数据总量增长、总吞吐增长、单 Key 过热,需要分开判断,再决定采用分片、本地缓存、允许陈旧读的副本访问,还是业务层拆分。

9. 消息和延迟任务,需要把失败之后的流程补齐

活动系统经常还有"支付完成后通知""订单超时后关闭"等需求。Redis 可以参与实现,但不同机制提供的保证不同。

Pub/Sub 适合实时广播,采用至多一次的投递语义。订阅者断线期间错过的消息,不会在重连后自动补发,因此不适合单独承担必须可靠处理的订单事件。Redis Pub/Sub

Stream 会保存事件记录,并支持消费组、确认和待处理消息管理。消费者处理成功后确认,异常退出留下的消息则需要恢复处理。持久性仍然受 Redis 的持久化、复制和数据保留策略影响。Redis Streams

如果消费者已完成业务写入,却在确认前退出,重试就可能再次执行同一事件。所以,"收到一条消息"与"业务恰好生效一次"之间,还需要幂等设计。

延迟任务可以利用 ZSet 的分数排序:把计划执行时间作为 score,工作进程领取到期任务。由这个设计继续推演,很快会遇到两个问题:多个进程怎样避免同时领取,领走之后退出又怎样恢复。

前者需要原子认领,后者需要处理中状态、超时恢复、重试和幂等。如果只是查出任务,再删除并执行,中间任何一步失败都可能带来重复或丢失。完成这些机制以后,才算把一个演示思路补成了可运行的任务系统。

业务简单、团队已熟悉 Redis 时,这样的方案可以合理存在;当消息保留、回放、路由和运维需求不断增长,也应重新评估专门的消息或任务系统。

10. 上线以后,把注意力放在真正影响体验的地方

开发环境里,几十条商品数据用什么方式读取都很快。线上数据和流量增长后,一些小习惯的影响就会放大。

大 Key 是单个值太大或集合元素太多,会增加处理、传输和内存管理成本;热 Key 则是访问过度集中。一个很小的 Key 也能非常热,两者可能同时存在,处理方式却不同。

大集合应限制规模,避免一次拉取全部元素;数据模型是否需要按用户、时间或业务范围拆分,也值得重新检查。热数据增加本地缓存后,则要进一步设计更新和失效,不能只看命中时的收益。

线上遍历数据通常应采用可控制节奏的增量方式。SCAN 可能返回重复项,也不提供静态快照,调用方需要正确处理游标、重复数据和并发变化。SCAN 官方说明

监控也应覆盖完整请求链路。Redis 命令很快,接口仍然可能在等待连接池、跨网络传输或反序列化。SLOWLOG 记录命令执行耗时,不包括与客户端通信的全部时间,所以"慢日志没有异常"不能直接等同于"Redis 调用链路没问题"。SLOWLOG GET

除了 CPU 和内存,还可以持续关注:接口 P95/P99 延迟、客户端超时、命中率变化、淘汰与过期数量、回源并发、复制落后程度,以及持久化状态。比起盯着单个漂亮数字,把这些指标串在一起更容易解释故障。

运行环境也需要基本的访问保护:限制网络来源,按业务分配 ACL 权限,并根据传输环境配置 TLS。缓存里的会话和业务状态,同样需要得到妥善保护。Redis 安全说明

再回到那场活动。上线前的讨论,会逐渐从"Redis 配好了没有",变成"热点失效时谁来重建""数据库能接多少回源""主节点切换后怎么恢复""哪部分功能可以暂时降级"。

相关推荐
麦壳饼8 分钟前
SonnetDB:PID 调参实战:阶跃响应分析与参数解读
数据库·sonnetdb
我才是银古21 分钟前
把 666 个会话摊开看:一份 DSH 使用经验与教训的复盘
ai·deepseek·dsh
码智社27 分钟前
Redis 哨兵模式详解:从原理到生产实践
java·redis
空堂与归37 分钟前
国产开源登顶全球第一,1.3B OXR 登顶三榜:RSI 闭环重塑文档解析
人工智能·ai·agent
猿究院--Cu-Sn合金38 分钟前
Spring Boot + DeepSeek 求职 Agent 实践:工具注册、多轮调用与简历 JD 对比
数据库·人工智能·spring boot·后端·deepseek
IvorySQL41 分钟前
PostgreSQL 日报|Anonymizer 3.3 修复高危漏洞(10 月 29 日)
数据库·postgresql
geovindu1 小时前
SQL Data Sampling and Approximation Techniques using postgreSQL 18
大数据·数据库·数据仓库·sql·postgresql·数据库开发·数据库架构
zyseo81 小时前
谷歌SEO 关键词研究完整方法论:从种子词到优先级矩阵
数据库·人工智能·矩阵
PaperData1 小时前
2000-2025年上市公司绿色金融改革创新试验区政策试点DID
数据库
Championship.23.241 小时前
Linux 内存管理深度解析与实战指南(基于 Linux 5.10+)
数据库