每天认识一个组件:Redis

一、引言

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

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

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

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

二、Redis 的整体架构

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

lua 复制代码
+-------------------+
| 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 多路复用管理大量客户端连接,而不需要为每个连接创建一个业务线程。

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

yaml 复制代码
客户端发送命令
      |
      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 的核心优势是"服务端数据结构",而不只是"内存中的键值对"。

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

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

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

ini 复制代码
方案 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 不提供传统意义上的执行期错误自动回滚。部分命令即使失败,其他已排队命令仍可能执行。

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

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

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

  • 淘汰:淘汰解决的是内存达到maxmemory 后,Redis 应该如何处理新写入。
lua 复制代码
内存未达到 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 会生成数据集在某个时间点的快照。

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

优点:

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

代价:

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

5.2.AOF:记录写命令

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

sql 复制代码
SET / HSET / INCR ...
          |
          v
      AOF Buffer
          |
       fsync 策略
          |
          v
   Append Only File

常见的同步策略包括:

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

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

5.3.组合策略

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

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

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

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

六、复制、Sentinel 与 Cluster 的区别

6.1.主从复制

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

lua 复制代码
                   write
Application -----------------> Primary
                                 |
                       async replication
                       +---------+---------+
                       |                   |
                       v                   v
                   Replica A           Replica B
                       ^                   ^
                       |                   |
                    read 可选           read 可选

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

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

6.2.Sentinel

Sentinel 面向非 Cluster 部署,提供:

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

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

6.3.Redis Cluster

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

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

css 复制代码
             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 控制:

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

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

6.4.三种模式怎么选

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

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

七、Redis 适合哪些场景

7.1.热点缓存

lua 复制代码
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:

rust 复制代码
Producer --> Stream --> Consumer Group
                         |      |      |
                         v      v      v
                        C1     C2     C3
                         ______|______/
                          ACK / Pending

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

7.6.Redis 不太适合的场景

以下场景需要谨慎:

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

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

八、部署使用

简单部署使用示意如下:

ruby 复制代码
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选型决策:

markdown 复制代码
                     数据是否可重建?
                         /       \
                       是         否
                      /            \
             优先按缓存设计      是否允许短暂数据丢失?
              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.延迟突然升高

lua 复制代码
延迟升高
   |
   +--> 慢命令?        SLOWLOG
   |
   +--> 大 Key?        MEMORY USAGE / 数据抽样
   |
   +--> fork 抖动?     RDB / AOF rewrite
   |
   +--> 内存换页?      OS metrics / swap
   |
   +--> 网络问题?      RTT / 丢包 / 连接池
   |
   +--> 客户端堆积?    CLIENT LIST / buffer
   |
   +--> CPU 饱和?      host metrics
相关推荐
虹科网络安全10 小时前
Search on Flex:用分层存储承载更大规模的 Redis 搜索索引
经验分享·redis·艾体宝·艾体宝it
青瓦梦滋11 小时前
Redis主从复制
服务器·数据库·redis·缓存·bootstrap
Joy T13 小时前
Redis Lua 原子限流:Lua 脚本、竞态条件与并发控制
数据库·redis·lua·限流·竞态条件·redis scripting
IT规划师14 小时前
Redis 数据类型全解 - Stream 深度:Pending 回收、重试死信与消息队列选型(十一)
redis·.net
虹科网络安全1 天前
RDI 多数据源管道:把分散的数据汇入统一 Redis 数据层
经验分享·redis·艾体宝·艾体宝it·艾体宝redis
我不会起名字3221 天前
Redis 从 0 到 1(三十七·收官):36 篇浓缩成 1 张知识地图 + 50 项生产 Checklist
redis·go·知识体系·checklist·收官
2601_967987371 天前
安装 Redis 或 Redis Stack
linux·windows·redis·macos·安装