一、引言
关系型数据库擅长持久化、事务、复杂查询和数据约束,但在下面这些场景中,直接访问数据库往往代价较高:
- 大量请求反复读取相同数据;
- 每次请求只执行一次简单计数或状态判断;
- 排行榜需要频繁更新并按分数排序;
- 会话、验证码等临时数据需要自动过期;
- 多台应用服务器需要共享短期状态;
- 实时事件需要在生产者和消费者之间快速流动。
这些操作的数据量未必大,却可能具有很高的访问频率。如果每个请求都落到磁盘数据库,不仅增加响应时间,也会消耗数据库连接、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 的核心优势是“服务端数据结构”,而不只是“内存中的键值对”。
| 数据类型 | 典型命令 | 适用场景 | 主要注意点 |
|---|---|---|---|
| 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
以下两个方案都能实现排行榜:
方案 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