1. Redis 简介
1.1 Redis 是什么
Redis 是一个开源的,基于内存的Key - Value数据结构存储系统,它不仅支持数据的持久化 (可以将内存中的数据保存到磁盘,重启后再次加载),还可以作为数据库、缓存和消息中间件使用。
1.2 Redis 特点
- 高性能:数据存储在内存中,具备极快的读写速度(读 ~11w/s,写 ~8w/s, 读写混合大概在~5 万/s)。
- 核心执行模型:
- 命令执行:采用单线程处理命令,避免了多线程带来的锁竞争和上下文切换开销,保证了操作的原子性。
- 网络 I/O:使用 I/O 多路复用技术处理网络连接。在6.0+ 版本中,引入了多线程来处理网络数据的读取和回写,进一步提升了吞吐量(epoll 监听多个 socket 实现非阻塞式 I/O)。
- 丰富的数据结构:支持 String, Hash, List, Set, Sorted Set, Bitmaps, HyperLogLog, Stream 等。
- 持久化: 提供 RDB、AOF 及混合持久化机制。
- 高可用与分片: 拥有成熟的 Sentinel(哨兵)和 Cluster(集群)部署方案。
1.3 Redis 应用场景
- 缓存: 利用 Redis 的高速读写能力,缓存热点数据,减轻数据库压力,显著提升系统响应速度。
- 消息队列: 使用 List 的 LPUSH/RPOP 实现简单队列,或使用 Pub/Sub、Stream 实现更复杂的消息系统。
- 排行榜: Sorted Set 天然支持排序,非常适合实现游戏排行榜、热搜榜等功能。
- 计数器: String 类型的 INCR/DECR 命令提供原子性的计数功能,可用于文章点赞数、访问量统计等。
- 分布式锁: 利用 SETNX(SET if Not eXists) 或 SET NX EX 命令实现分布式锁,确保分布式环境下的资源互斥访问。
- 会话管理: 将用户 Session 存储在 Redis 中,实现分布式系统的会话共享,支持服务器的水平扩展。
- 实时分析: 使用 HyperLogLog 进行 UV 统计,使用 Bitmap 进行用户签到统计,使用 Sorted Set 进行实时排行分析。
2. Redis 数据结构

6.0+ 版本
- String:字符串在 Redis 内部使用 SDS(Simple Dynamic String)结构进行存储,SDS 结构可以动态地扩展字符串的长度,避免了字符串的频繁扩容和内存拷贝,提高了字符串操作的效率,同时获取字符串时间复杂度为 O(1)。
- 双向链表(linked list)
- 当列表的元素个数≤512(默认值,可配置)且每个元素的长度≤64 字节时,使用双向链表。
- 双向链表是一种连续内存块,采用"元素 1→元素 2→元素 3..."的顺序存储,无指针开销,内存利用率极高。
- 适用于小数据量场景,但插入/删除操作需要移动内存,时间复杂度为 O(n)
- 压缩列表(zip list):
- 当 Hash 中的 field 数量≤512(默认值,可配置)且每个 field/value 的长度≤64 字节时,使用压缩列表。
- 压缩列表是连续内存块,采用"field1→value1→field2→value2..."的顺序交替存储,无指针开销,内存利用率极高。
- 适用于小数据量场景,但插入/删除操作需要移动内存,时间复杂度为 O(n)
- 哈希表(hash table)
- 当数据量超过上述阈值时,自动转换为哈希表。
- 采用经典的"数组 + 链表"结构,通过哈希函数实现 O(1) 级别的查找、插入、删除操作。
- 跳表(skip list)
- 当有序集合的元素个数≥128(默认值,可配置)时,使用跳表。
- 跳表是一种多层链表结构,通过跳跃节点实现快速查找,时间复杂度为 O(log n)。
2.1 字符串(String)
简介: 字符串是 Redis 中最基本的数据结构,可以存储任意类型的数据,包括字符串、数字、二进制数据等。字符串的最大长度为 512MB。
常用指令:
SET key value:设置键值对GET key:获取键对应的值DEL key:删除键值对EXPIRE key seconds:设置键的过期时间(单位:秒)TTL key:获取键的剩余过期时间(单位:秒)
应用场景:
- 缓存:字符串是最常用的数据结构,可以用于缓存热点数据,提高系统的性能。
- 计数器:字符串可以用于实现计数器功能,例如,可以使用字符串存储用户的访问次数、点赞次数等。
- 分布式锁:字符串的原子性操作可以用于实现分布式锁。例如,可以使用字符串的
SETNX命令实现分布式锁。 - 会话管理:字符串可以用于存储会话信息,实现分布式系统的会话共享。
- 实时分析:字符串可以用于实时分析,例如,可以实现实时排行榜、实时统计等功能。
2.2 哈希(Hash)
简介: 哈希是一种键值对的集合,可以存储多个键值对。哈希的键和值只能是字符串类型。
常用指令:
HSET key field value:设置哈希表中指定字段的值HGET key field:获取哈希表中指定字段的值HDEL key field:删除哈希表中指定字段HGETALL key:获取哈希表中所有字段的值HINCRBY key field increment:对哈希表中指定字段的值进行自增操作
应用场景:
- 如何复杂对象:哈希可以用于存储复杂对象,例如,可以使用哈希存储用户信息,包括用户名、年龄、性别等字段。
Note: Redis Hash 的设计目标是高性能的扁平化键值存储,而非复杂的嵌套对象存储。因此,Redis Hash 并不适合存储嵌套对象,例如,嵌套的哈希或列表等,如果需要存储嵌套对象,建议使用 JSON 序列化后存储为字符串。
2.3 列表(List)
简介: 列表是一种有序的、可重复的元素集合,可以存储多个元素。列表的元素只能是字符串类型。
QuickList

QuickList
在 Redis 3.2+ 版本统一使用 QuickList.
QuickList = ZipList + linked list,它结合了 ZipList 和 linked list 的优点,既节省内存,又提高了性能。
在 Redis 7.0+ 版本,QuickList = ListPack + linked list,ListPack 是 ZipList 的升级版,它进一步优化了内存使用和性能。
常用指令:
LPUSH key value1 [value2]:将一个或多个值插入到列表头部RPUSH key value1 [value2]:将一个或多个值插入到列表尾部LPOP key:移除并返回列表的第一个元素RPOP key:移除并返回列表的最后一个元素LRANGE key start stop:获取列表指定范围内的元素LINDEX key index:获取列表指定索引的元素LLEN key:获取列表的长度
应用场景:
- 实现队列:列表可以用于实现队列,例如,可以使用列表存储待处理的任务,然后使用
LPUSH和RPOP指令将任务插入队列和从队列中取出任务。
2.4 集合 (Set)
简介: Set 是 String 类型的无序集合,集合成员是唯一的,不允许重复。Set 提供了交集、并集、差集等数学集合操作。
常用指令:
应用场景:
- 标签系统: 文章标签、用户兴趣标签,使用 SINTER 找共同兴趣
- 共同好友: SINTER 计算两个用户的共同好友
- 抽奖系统: SPOP 随机抽取中奖用户
- 去重: 利用 Set 的唯一性实现数据去重
- 点赞/收藏: 用户点赞集合,SISMEMBER 快速判断是否已点赞
2.5 有序集合 (Sorted Set)
简介: Sorted Set 是 Set 的升级版,每个成员关联一个分数 (score),集合按分数从小到大排序。成员唯一,但分数可重复。
当有序集合的元素小于 128 个,且每个元素的长度小于 64 字节时,使用 ziplist 编码,否则使用 skiplist 和 hash 编码。
SkipList

SkipList 示意图
跳表查询过程是从最高级索引开始,逐级向下查找,直到找到目标元素或到达最底层索引。跳表插入和删除操作也是从最高级索引开始,逐级向下进行,直到找到目标元素或到达最底层索引,然后进行相应的操作。
查找某个元素类似于二分查找,时间复杂度为 O(log n)。插入和删除操作需要更新索引,时间复杂度为 O(log n)。跳表的空间复杂度为 O(n),其中 n 是链表的长度。
为什么不用平衡树?
内存使用更灵活 平衡树:每个节点固定包含 2 个指针(左右子树) 跳表:每个节点的指针数量是动态的,平均为 1/(1-p) 在 Redis 中,p=1/4,所以平均每个节点包含 1.33 个指针 结论:跳表比平衡树节省内存
范围查询更简单 平衡树的问题:找到范围的起始值后,需要进行复杂的中序遍历来找到范围内的所有节点 跳表的优势:找到起始值后,只需要在第 1 层(最底层)链表上连续遍历即可 比喻:就像在图书馆找书,跳表相当于找到书架后直接从左到右拿书,而平衡树需要在整个图书馆里按特定顺序跳着找书
实现和维护更简单 平衡树的复杂性:插入和删除可能触发复杂的树结构调整(旋转操作),代码复杂,调试困难 跳表的简单性:插入和删除只需要修改相邻节点的指针,就像在链表中插入节点一样简单 实际好处:代码易读、易维护、易扩展(比如添加 ZRANK 功能只需要少量代码修改)
常用指令:
应用场景:
- 排行榜: 游戏积分榜、热搜榜、销量排行
bash ZADD game:rank 9500 player1 8800 player2 ZREVRANGE game:rank 0 9 WITHSCORES # 获取前 10 名 - 延迟队列: score 存储执行时间戳,定时取出到期任务
- 范围查询: 按价格、时间等范围筛选商品
- 优先级队列: score 表示优先级
- 带权重的推荐: score 表示推荐权重
3. 数据结构选择建议
4. Redis 持久化机制
Redis 提供 RDB、AOF 及混合持久化机制,确保数据安全。
4.1 持久化方式对比
| 特性 | RDB (快照) | AOF (日志) | 混合持久化 (推荐) |
|---|---|---|---|
| 原理 | 特定间隔保存内存快照 | 记录每条写命令 | AOF 文件头部是 RDB 快照,尾部是 AOF 增量日志 |
| 优点 | 恢复极快,文件紧凑 | 数据丢失少(高安全性) | 兼顾恢复速度与数据安全性 |
RDB
- 触发机制:
- save: 阻塞 Redis 服务器,直到 RDB 文件生成完成。
- bgsave: fork 子进程生成 RDB 文件,不阻塞服务器。
- 自动触发:配置文件中
save m n,m 秒内发生 n 次修改时触发。
- 优点:
- 文件紧凑,恢复速度快。
- 缺点:
- 数据丢失风险大。
- fork 子进程,内存消耗大。
AOF
- 触发机制:
- 每次写入命令时,记录到 AOF 文件,如果是重复的 key 操作,则只记录最后一次操作,节省空间。
- 每秒写入 (everysec): 每秒将缓冲区命令写入 AOF 文件,可能丢失 1 秒数据。
- 始终写入 (always): 每次写入命令时,立即将缓冲区命令写入 AOF 文件,数据最安全,但 I/O 负载大。
- 优点:
- 数据丢失少(高安全性)。
- 缺点:
- 文件大,恢复速度慢。
- 写入 I/O 负载大。
混合持久化
- 触发机制:
- 开启混合持久化后,AOF 文件头部是 RDB 快照,尾部是 AOF 增量日志。
- 优点:
- 兼顾恢复速度与数据安全性: 恢复数据优先使用 RDB 文件,再对比 AOF 文件,把缺失的数据补上。
5. Redis 常见生产问题与解决方案
5.1 缓存穿透 (Cache Penetration)
- 问题:大量恶意请求查询数据库和缓存中都不存在的数据,造成巨大的 I/O 压力,可能导致数据库宕机。
- 解决方案:
- 布隆过滤器:请求前快速判断 Key 是否可能存在。
- 缓存空对象:将查询结果(空)也缓存起来,设置较短过期时间。
5.2 缓存击穿 (Cache Breakdown)
- 问题:某个热点 Key 过期瞬间,大量请求并发,击穿缓存。
- 解决方案:
- 互斥锁 (Mutex):只允许一个线程去重建缓存(用户体验偏差,如果重建比较慢,后续所有用户请求都将阻塞)。
- 逻辑过期:数据永不过期,但在 Value 中记录过期时间,过期后异步更新,重建过程中依旧返回旧数据。
5.3 缓存雪崩 (Cache Avalanche)
- 问题:大量 Key 同时过期,或 Redis 服务宕机,导致数据库压力激增。
- 解决方案:
- 随机 TTL:在设置过期时间时增加随机值,打散失效时间。
- 高可用架构:部署 Sentinel 或 Cluster。
5.4 数据库与缓存数据一致性
- 策略:Cache Aside Pattern (旁路缓存)。
- 核心步骤:先更新数据库 再删除缓存。
- 高级方案:延时双删。更新 DB 删缓存 暂停 再删缓存,防止主从同步延迟导致的脏数据。
6. Redis 高可用架构
6.1 主从复制
- 作用:实现读写分离,提高并发量。
- 原理:主节点写入数据,从节点同步数据。
- 配置:
- 配置主节点。
- 配置从节点,并指定主节点地址。
- 缺点:主节点宕机后,无法自动切换。
6.2 哨兵 (Sentinel)
- 作用:监控主从节点,自动故障转移。
- 原理:多个哨兵监控主从节点,当主节点宕机时,哨兵投票选举新的主节点。
- 配置:
- 配置哨兵节点。
- 配置主节点地址。
- 缺点:哨兵节点数量过少时,可能无法选举出新的主节点。
6.3 集群 (Cluster)
- 作用:实现分布式存储,提高并发量。
- 原理:将数据分片存储在多个节点上,每个节点存储一部分数据。
- 配置:
- 配置集群节点。
- 配置分片规则。
- 缺点:数据迁移复杂,维护成本高。
7. Redis 过期策略与内存淘汰策略
7.1 过期策略
Redis 的 Key 过期后并非立即删除,而是采用惰性删除和定期删除两种策略,此目的是为了减少 CPU 和内存的消耗,如果采用立即删除策略,则必然要创建大量定时器或定期扫描所有 key,当数据量较大时,会严重影响 Redis 的性能。
| 策略 | 触发时机 | 工作原理 | 优点 |
|---|---|---|---|
| 惰性删除 | 当访问某个 Key 时 | 查询 key 时,如果 key 已过期,则删除并返回空值 | 减少 CPU 和内存的消耗,但可能导致内存泄漏 |
| 定期删除 | 周期性执行(100ms) | 随机抽取少量(如 20 个)设置了过期时间的 key,如果过期 key 比例>25% 则持续重复此步骤 | 减少内存泄漏,但可能导致部分 key 无法及时删除 |
如果某个 key 过期了,一直没被访问(没办法触发惰性删除),又一直没在定期删除中被删除,那么这个 key 就会一直占用内存,导致内存泄漏。因此,Redis 提供了内存淘汰策略,当内存不足时,会根据一定的规则删除一些 key,以释放内存。
7.2 内存淘汰策略
触发条件: 当 Redis 内存使用达到 maxmemory 配置的值时,会触发内存淘汰策略。
| 策略 | 作用 | 适用场景 |
|---|---|---|
| noeviction | 不淘汰任何 key,达到 maxmemory 时直接报错 | 数据绝对不能丢失的常见 |
| allkeys-lru | 淘汰最近最少使用的 key | 纯缓存场景,所有 key 都可以淘汰 |
| allkeys-lfu | 淘汰访问量最低的 key | 需要精确淘汰低频数据 |
| allkeys-random | 随机淘汰 key | 不关心淘汰哪个 key |
| volatile-lru | 淘汰最近最少使用的设置了过期时间的 key | 部分 key 需要持久化 |
| volatile-lfu | 淘汰访问量最低的设置了过期时间的 key | 部分 key 需要持久化,需要精确淘汰低频数据 |
| volatile-random | 随机淘汰设置了过期时间的 key | 随机清理过期数据 |
| volatile-ttl | 淘汰即将过期的 key | 优先清理快过期的数据 |
如何选择?
选择 LRU
- 访问模式规律
- 对性能要求高
选择 LFU
- 热点数据明显
- 有突发访问
混合使用
- 缓存用 LFU
- 会话用 LRU