每天认识一个组件:Redis

0 阅读14分钟

一、引言

关系型数据库擅长持久化、事务、复杂查询和数据约束,但在下面这些场景中,直接访问数据库往往代价较高:

  • 大量请求反复读取相同数据;
  • 每次请求只执行一次简单计数或状态判断;
  • 排行榜需要频繁更新并按分数排序;
  • 会话、验证码等临时数据需要自动过期;
  • 多台应用服务器需要共享短期状态;
  • 实时事件需要在生产者和消费者之间快速流动。

这些操作的数据量未必大,却可能具有很高的访问频率。如果每个请求都落到磁盘数据库,不仅增加响应时间,也会消耗数据库连接、CPU 和 I/O。

Redis 是一个以内存为主要数据存储介质的数据结构服务器,它将主要数据表示保存在内存中,通过网络提供结构化数据操作;磁盘不再承担每次随机读取的关键路径,而主要用于持久化、恢复和备份。这个取舍带来了低延迟,也带来了数据集受内存容量限制、内存成本较高以及持久性需要单独设计等问题。

二、Redis 的整体架构

一个常见的 Redis 单实例访问路径如下:

+-------------------+
| Application       |
| Java / Go / Python|
+---------+---------+
          |
          | Redis Protocol over TCP
          v
+------------------------------------------------+
| Redis Server                                   |
|                                                |
|  +---------------+     +--------------------+  |
|  | Network Event | --> | Command Execution  |  |
|  | Loop / I/O    |     | & Data Structures  |  |
|  +---------------+     +---------+----------+  |
|                                  |             |
|                    +-------------+-----------+ |
|                    |                         | |
|                    v                         v |
|              +-----------+             +-------+
|              | Keyspace  |             | Expire|
|              | in Memory |             | Evict |
|              +-----+-----+             +-------+
|                    |
|          +---------+----------+
|          |                    |
|          v                    v
|       RDB Snapshot         AOF Log
+----------+--------------------+-----------------+
           |                    |
           v                    v
        dump.rdb       appendonly files

2.1.内存是主要工作区

Redis 的数据结构主要驻留在内存中。命令到达后,Redis可以直接访问内存对象,无须先从磁盘读取数据页。相比以磁盘为主要介质的数据库,这种设计减少了随机 I/O 和复杂磁盘数据结构带来的开销。

但“基于内存”不等于“数据绝不落盘”。Redis 可以通过 RDB、AOF,或者同时使用两者,将内存状态保存到持久化存储。

2.2.事件驱动和非阻塞 I/O

Redis 接受客户端连接后,会将套接字设置为非阻塞状态,并为其创建可读文件事件。一个 Redis 进程可以借助 I/O 多路复用管理大量客户端连接,而不需要为每个连接创建一个业务线程。

一次请求大致经历以下过程:

客户端发送命令
      |
      v
网络层读取请求
      |
      v
解析 Redis 协议
      |
      v
执行数据结构操作
      |
      v
生成响应并写回客户端

2.3.“Redis 是单线程的”为什么不够准确

很多介绍会把 Redis 的性能归结为“单线程”,这种说法过度简化了真实实现。

更稳妥的理解是:

  • Redis 的核心命令执行长期采用串行化模型,因此避免了大量数据结构锁竞争;
  • 网络连接采用事件驱动和非阻塞 I/O;
  • 持久化、异步删除以及部分 I/O 工作可以使用后台进程或线程;
  • 具体线程模型会随 Redis 版本和配置变化。

2.4.为什么 Redis 很快

Redis 的低延迟来自多个因素,而不是某个单一技巧:

  • 主要数据驻留内存;
  • 数据结构针对具体操作设计;
  • 核心命令执行减少锁竞争;
  • 使用非阻塞 I/O 和事件循环管理连接;
  • Redis 协议较为紧凑;
  • Pipeline 可以减少网络往返和系统调用。

三、Redis 的数据结构

Redis 的核心优势是“服务端数据结构”,而不只是“内存中的键值对”。

数据类型典型命令适用场景主要注意点
StringSET、GET、INCR缓存、计数器、令牌大字符串可能形成大 Key
HashHSET、HGET对象属性、购物车字段过多时关注内存和遍历成本
ListLPUSH、RPOP、BLPOP简单队列、时间线不适合频繁随机访问
SetSADD、SISMEMBER去重、标签、共同关系大集合运算可能耗时
Sorted SetZADD、ZRANGE排行榜、延时任务分数是排序依据,不等于业务唯一键
StreamXADD、XREADGROUP事件流、消费组需要设计确认、重试和裁剪
BitmapSETBIT、BITCOUNT签到、布尔状态统计依赖稳定的整数偏移映射
HyperLogLogPFADD、PFCOUNTUV 近似统计结果是估算值,不是精确值
GeospatialGEOADD、GEOSEARCH附近位置检索不替代完整 GIS 系统

3.1.用数据结构表达业务,而不是把 Redis 当内存 Map

以下两个方案都能实现排行榜:

方案 A:
user_id -> JSON(score)
应用层读取所有用户、反序列化、排序

方案 B:
leaderboard -> Sorted Set
member = user_id
score  = business_score
直接执行 ZADD / ZRANGE

方案 B 把排序交给 Redis 的有序集合,这样既减少应用代码,也避免每次查询时搬运全部数据。

这也是 Redis 与普通本地缓存的重要区别:本地缓存解决的是“本进程如何快速取值”,Redis 还解决“多个进程如何共享并原子修改结构化状态”。

3.2.原子操作的边界

Redis 的单条命令通常以不可被其他命令插入的方式执行。例如,多个客户端同时调用INCR counter ,不会因为应用层的“读后写”竞争而丢失更新。

多条命令需要组合时,可以考虑:

  • MULTI 、EXEC 事务;
  • WATCH 提供乐观锁式条件检查;
  • Lua 脚本或 Redis Functions;
  • 将复杂操作改造为一条原生命令。

Redis 事务保证事务中的命令按顺序执行,其他客户端的命令不会插入事务执行过程。但它不等同于关系型数据库事务:Redis 不提供传统意义上的执行期错误自动回滚。部分命令即使失败,其他已排队命令仍可能执行。

四、过期与内存淘汰不是一回事

  • 过期:过期时间表达的是业务生命周期。
SET login:code:13800138000 731826 EX 300

这表示验证码五分钟后失效。过期属于 Key 自身的业务属性。

  • 淘汰:淘汰解决的是内存达到maxmemory 后,Redis 应该如何处理新写入。
内存未达到 maxmemory
        |
        v
     正常写入
        |
        v
内存超过 maxmemory
        |
        +--> noeviction:拒绝可能增加内存的写操作
        |
        +--> allkeys-*:从所有 Key 中选择淘汰对象
        |
        +--> volatile-*:从设置了过期时间的 Key 中选择

Redis 支持 LRU、LFU、随机和 TTL 等策略。不过Redis 实现的是近似 LRU/LFU,而不是维护代价更高的绝对精确顺序。

如果 Redis 只承担可重建缓存,可以考虑allkeys-lru 或根据访问特征选择 LFU。如果 Redis 存储不可随意丢弃的数据,就不应该把淘汰策略当作容量治理方案。

五、持久化:RDB 与 AOF 如何选择

Redis 提供四种基本选择:

  • 只使用 RDB;
  • 只使用 AOF;
  • 同时使用 RDB 和 AOF;
  • 完全关闭持久化。

5.1.RDB:保存某个时间点的快照

RDB 会生成数据集在某个时间点的快照。

内存数据
   |
   | 定时或手动触发快照
   v
 dump.rdb
   |
   | 重启时加载
   v
恢复内存状态

优点:

  • 文件紧凑,便于备份和传输;
  • 对主进程正常处理请求的影响相对有限;
  • 大数据集通常可以获得较快的重启加载速度。 

代价:

  • 两次快照之间发生故障,期间的数据可能丢失;
  • 创建快照需要fork() ,大实例在内存紧张或写入密集时可能出现额外内存和延迟压力。

5.2.AOF:记录写命令

AOF 将修改数据集的命令追加到日志中,重启时重放这些命令恢复数据。

SET / HSET / INCR ...
          |
          v
      AOF Buffer
          |
       fsync 策略
          |
          v
   Append Only File

常见的同步策略包括:

  • appendfsync always :更强持久性,写入开销更高;
  • appendfsync everysec :通常在性能和持久性之间折中;
  • appendfsync no :由操作系统决定何时刷盘,数据丢失窗口更难控制。

默认的everysec 策略在灾难性故障下可能丢失大约一秒的数据。这里的“一秒”是设计目标,不是所有硬件、文件系统和故障模型下的绝对上限。

5.3.组合策略

如果 Redis 中保存重要数据,可以同时使用 RDB 和 AOF:

              +--> RDB:备份、较快恢复基线
内存数据 -----|
              +--> AOF:缩小最近写入的丢失窗口

但即使同时启用,也不能把 Redis 持久化等同于完整灾备。至少还要考虑:

  • 持久化文件是否存放在可靠磁盘;
  • 是否有异机或异地备份;
  • 是否定期演练恢复;
  • AOF 重写和 RDB 快照期间是否有足够内存;
  • 宿主机、可用区或集群整体故障时如何恢复。

六、复制、Sentinel 与 Cluster 的区别

6.1.主从复制

Redis 复制采用主节点和副本节点模型。主节点将数据变更传播给副本;连接中断后,副本会尝试部分同步,无法部分同步时再进行全量同步。

                   write
Application -----------------> Primary
                                 |
                       async replication
                       +---------+---------+
                       |                   |
                       v                   v
                   Replica A           Replica B
                       ^                   ^
                       |                   |
                    read 可选           read 可选

Redis 默认使用异步复制,因此“主节点已经向客户端确认写入”不等于“副本一定已经持久保存该写入”。如果主节点在复制完成前故障,故障切换可能丢失最近写入。

复制本身主要解决数据冗余和读扩展,不会单独完成可靠的自动故障转移。

6.2.Sentinel

Sentinel 面向非 Cluster 部署,提供:

  • 节点监控;
  • 故障通知;
  • 自动故障转移;
  • 向客户端提供当前主节点地址。
             +-------------+
             | Sentinel 1  |
             +------+------+
                    |
        +-----------+-----------+
        |                       |
+-------v-------+       +-------v-------+
|   Primary     | ----> |   Replica     |
+---------------+       +---------------+
        ^
        |
+-------+-------+
| Application   |
| Sentinel-aware|
+---------------+

生产环境通常运行多个 Sentinel 进程,由多个 Sentinel 共同判断故障。只运行一个 Sentinel 会使 Sentinel 自己成为单点。

6.3.Redis Cluster

Cluster 同时解决数据分片和一定程度的高可用。

Redis Cluster 不采用经典的一致性哈希,而是将键空间划分为 16384 个 Hash Slot 。每个主节点负责其中一部分槽位。

             CRC16(key) mod 16384
                       |
                       v
+--------------------------------------------------+
|                 16384 Hash Slots                 |
+----------------+----------------+----------------+
|   Node A       |    Node B      |    Node C      |
| slots 0..5500  | 5501..11000    | 11001..16383   |
+-------+--------+--------+-------+--------+--------+
        |                  |                 |
        v                  v                 v
    Replica A          Replica B         Replica C

Cluster 客户端需要识别重定向并维护槽位到节点的映射。多 Key 命令通常要求相关 Key 位于同一槽位,可以通过 Hash Tag 控制:

user:{1001}:profile
user:{1001}:orders
      ^----^
       相同 Hash Tag,因此进入同一槽位

Cluster 并不提供强一致性。它使用异步复制,在特定故障窗口或网络分区条件下,已经确认的写入仍可能丢失;当多数主节点不可用时,集群也可能停止提供服务。

6.4.三种模式怎么选

模式数据分片自动故障转移典型用途
单实例否否本地开发、临时缓存
主从复制否单独使用时否读扩展、数据副本
Sentinel否是数据量可由单主节点承载的高可用
Cluster是是数据量或吞吐需要跨节点扩展

如果单节点容量和吞吐已经满足需求,Sentinel 往往比 Cluster 简单。不要因为“生产环境”四个字就直接上 Cluster。Cluster 会引入客户端兼容、多 Key 操作、槽位迁移和故障诊断等额外复杂度。

七、Redis 适合哪些场景

7.1.热点缓存

Client
  |
  v
Application ---- GET ----> Redis
  |                         |
  | cache miss              | cache hit
  v                         v
Database <---- query      Return
  |
  +---- SET EX ----> Redis

适合缓存商品详情、配置、权限信息和计算结果。缓存中的数据应当能够从权威数据源重建。

主要收益不是单纯提高单次读取速度,而是削减后端数据库的重复负载。

7.2.计数器与限流

INCR 、DECR 和过期时间适合实现:

  • 接口访问次数;
  • 登录失败次数;
  • 短周期固定窗口限流;
  • 库存预扣中的简单计数。

复杂限流通常需要 Lua 脚本或 Functions 把“读取、判断、更新”组合成一次原子操作。

7.3.会话与短期状态

Redis 适合存储:

  • 登录 Session;
  • 验证码;
  • OAuth 临时状态;
  • 幂等请求标记;
  • 短期锁状态。

这些数据天然需要共享、低延迟和过期机制。

7.4.排行榜

Sorted Set 可以同时保存成员和分数,并按分数范围、排名范围查询,适合积分榜、热度榜和实时竞赛排名。

7.5.实时事件流

Pub/Sub 适合在线通知等允许丢失的瞬时消息。Redis Pub/Sub 使用 at-most-once 语义:消息一旦发送,如果订阅者断线或处理失败,Redis 不会再次投递。

如果需要消息留存、消费组、确认和待处理消息追踪,应优先评估 Redis Streams:

Producer --> Stream --> Consumer Group
                         |      |      |
                         v      v      v
                        C1     C2     C3
                         ______|______/
                          ACK / Pending

即使使用 Streams,应用仍需自行设计幂等、重试、死信处理和流裁剪策略。它不会自动替你实现完整消息治理。

7.6.Redis 不太适合的场景

以下场景需要谨慎:

  • 数据规模远大于可承担的内存容量;
  • 需要复杂关联查询和临时分析;
  • 需要严格的跨记录 ACID 事务;
  • 无法接受异步复制导致的写入丢失窗口;
  • 希望用 Pub/Sub 承担必须可靠送达的业务消息;
  • 业务团队没有能力维护内存容量、热 Key 和故障切换。

Redis 可以存储关键数据,但必须明确持久化、复制和灾备边界。它不能仅凭“开启 AOF”就自动获得传统数据库的全部保障。

八、部署使用

简单部署使用示意如下:

docker run -d \
  --name redis-demo \
  -p 127.0.0.1:6379:6379 \
  redis:<version>

//连接redis
docker exec -it redis-demo redis-cli

127.0.0.1:6379> PING
PONG

127.0.0.1:6379> SET article:1:title "Redis 入门"
OK

127.0.0.1:6379> GET article:1:title
"Redis 入门"

127.0.0.1:6379> INCR article:1:views
(integer) 1

127.0.0.1:6379> EXPIRE article:1:title 3600
(integer) 1

Redis选型决策:

                     数据是否可重建?
                         /       \
                       是         否
                      /            \
             优先按缓存设计      是否允许短暂数据丢失?
              TTL + 淘汰            /       \
                                  是         否
                                 /            \
                         持久化 + 复制      重新评估技术选型
                         + 灾备演练        或增加权威存储
                                |
                       数据是否超过单机容量?
                           /          \
                         否            是
                        /               \
                 Sentinel 可选       Cluster / 分片
  • 缓存数据 :关注命中率、TTL、淘汰策略和数据库保护;
  • 临时状态 :关注过期语义、幂等和故障后的业务行为;
  • 消息数据 :关注投递语义、确认、重试和消息保留;
  • 权威数据 :关注持久化窗口、复制一致性、备份和恢复;
  • 分布式协调 :关注故障模型,而不只是正常路径下的原子性。

九、常见问题

9.1.缓存穿透

现象: 请求查询一个数据库中也不存在的 Key,每次都绕过缓存访问数据库。

常用措施:

  • 参数合法性校验;
  • 短时间缓存空结果;
  • 使用 Bloom Filter 过滤明确不存在的数据;
  • 对异常来源限流。

9.2.缓存击穿

现象: 一个高频热点 Key 失效,大量请求同时访问数据库。

常用措施:

  • 互斥重建,只允许一个请求加载数据;
  • 逻辑过期,后台刷新;
  • 热点数据采用更合理的过期策略;
  • 数据库侧保留限流和降级。

9.3.缓存雪崩

现象: 大量 Key 在相近时间失效,或整个 Redis 服务不可用,流量集中回到数据库。

常用措施:

  • 给过期时间加入合理随机扰动;
  • 分批预热和刷新;
  • 部署高可用 Redis;
  • 应用侧限流、熔断和降级;
  • 为数据库预留受控兜底能力。

9.4.大 Key

大 Key 可能是:

  • 一个超大字符串;
  • 包含几十万成员的集合;
  • 字段数量过多的 Hash;
  • 长时间没有裁剪的 Stream。

它可能造成网络传输变慢、命令执行时间增长、删除延迟、复制压力和故障切换变慢。

处理思路:

  • 在数据模型层拆分;
  • 避免一次返回全部成员;
  • 使用UNLINK 等异步释放方式处理适用对象;
  • 对 Stream 设置裁剪策略;
  • 结合MEMORY USAGE 、SCAN 和诊断工具排查。

9.5.热 Key

热 Key 指访问集中在少量 Key 上。即使内存足够,单个 Key 仍可能使某个 Redis 节点、网络链路或 CPU 核心成为瓶颈。

常见措施包括:

  • 在应用本地增加一级缓存;
  • 将可拆分的热点数据分片;
  • 合并读取或使用 Pipeline;
  • 对热点接口限流;
  • 检查是否存在不必要的重复读取。

9.6.延迟突然升高

延迟升高
   |
   +--> 慢命令?        SLOWLOG
   |
   +--> 大 Key?        MEMORY USAGE / 数据抽样
   |
   +--> fork 抖动?     RDB / AOF rewrite
   |
   +--> 内存换页?      OS metrics / swap
   |
   +--> 网络问题?      RTT / 丢包 / 连接池
   |
   +--> 客户端堆积?    CLIENT LIST / buffer
   |
   +--> CPU 饱和?      host metrics