Redis — 用问答的方式拆解分布式缓存的核心(上)
常规问题
什么是 Redis,为什么要使用它?
Redis 是一个开源的、基于内存运行的键值型(Key-Value)NoSQL 数据库。它以极高的读写速度著称,常被用作缓存、数据库或消息中间件。使用 Redis 的原因有以下几点:
- 高性能:拦截大量请求,保护后端数据库不被大流量冲垮;
- 多数据结构:支持 String、Hash、List、Set、ZSet 等,能直接在内存中处理复杂的业务逻辑;
- 原子性:所有操作均为原子性,天然适合处理计数器、分布式锁等并发场景。
Redis 一般有哪些使用场景?
- 缓存:存储热点数据(如商品、用户信息等),大幅降低数据库压力,提升响应速度。
- 分布式锁:利用
SETNX等原子操作,解决分布式系统下的资源竞争问题。 - 计数器 / 限流:实现点赞数、播放量统计,或通过计数器限制 API 的访问频率。
- 排行榜:利用 ZSet 自动排序功能,实现实时积分或热度榜单。
- 会话管理:在分布式集群中统一存储用户的登录状态,实现多机共享。
- 消息队列:利用 List 或 Stream 结构,实现简单的异步任务处理和解耦。
Redis 为什么快?
- 基于内存操作:数据直接存储在内存中,省去了磁盘 I/O 的寻道与读写开销(内存访问速度比磁盘快数万倍)。
- 单线程模型:核心网络处理采用单线程,避免了多线程环境下的上下文切换和锁竞争开销,保证了操作的原子性。
- I/O 多路复用:使用 epoll 非阻塞 I/O 模型,单个线程即可高效处理数万个并发连接。
- 高效的数据结构:Redis 内部对各种结构(如 SDS 字符串、跳表 SkipList、压缩列表 ZipList)进行了极致的内存优化和算法优化。
数据类型和数据结构
Redis 有哪些数据类型?
五种基础数据类型
| 类型 | 说明 | 应用场景 |
|---|---|---|
| String | 最基础类型,二进制安全,最大 512MB | 缓存、计数器、分布式锁、验证码 |
| Hash | 键值对集合(如 user:101 {name: "Tom", age: 18}) | 存储对象、购物车 |
| List | 简单的字符串列表,按插入顺序排序 | 消息队列、最新动态、时间轴 |
| Set | 无序且不重复的字符串集合 | 标签、共同好友、抽奖去重 |
| ZSet | 有序集合,每个元素关联一个 double 类型的分数,按分数排序 | 排行榜、热搜、延时队列 |
三种高级数据类型
| 类型 | 说明 | 应用场景 |
|---|---|---|
| Bitmap | 基于 String,通过位操作记录 0/1 状态,极省空间 | 用户签到、活跃状态统计 |
| HyperLogLog | 概率型数据结构,统计基数,大数据量下仅占约 12KB 内存,误差约 0.81% | 亿级 UV 统计 |
| GeoSpatial (GEO) | 存储经纬度信息,用于计算附近的人或两点之间的距离 | 附近的人、打车距离计算 |
新一代数据类型
- Stream:Redis 5.0 新增,主要用于实现持久化的消息队列(类似 Kafka),解决了 List 做队列时消息丢失的问题。
谈谈 Redis 的对象机制(redisObject)
typedef struct redisObject {
unsigned type:4; // 1. 类型(对外,即通常说的 5 大数据类型)
unsigned encoding:4; // 2. 编码(对内,内部编码)
unsigned lru:24; // 3. 记录 LRU/LFU 信息(用于淘汰)
int refcount; // 4. 引用计数(用于内存回收)
void *ptr; // 5. 指针(指向底层实际的数据结构)
} robj;
设计这套对象机制的原因有以下三点:
- 解耦:命令(如
LLEN)只需要针对 List 类型,不需要关心底层是 ZipList 还是 LinkedList。 - 极致的内存优化:小数据量用紧凑存储(时间换空间),大数据量用高效索引(空间换时间)。
- 智能维护:自带引用计数和访问记录,自动处理内存回收和缓存淘汰。
Redis 数据类型有哪些底层数据结构?
| 常用类型 | 底层数据结构 |
|---|---|
| String | SDS(简单动态字符串) |
| List | quicklist(双向链表 + ziplist/listpack 的结合体) |
| Hash | ziplist(压缩列表)或 hashtable(哈希表) |
| Set | intset(整数集合)或 hashtable |
| ZSet | ziplist 或 skiplist + hashtable |
为什么要设计 SDS?
Redis 没有直接使用 C 语言的字符串(char*),而是自己封装了 SDS。C 语言原生的字符串(以 \0 结尾)无法满足 Redis 对高性能和安全性的要求。SDS 的设计优势如下:
- 常数复杂度获取长度:内部记录了
len属性,读取长度的时间复杂度为 O(1)。 - 杜绝缓冲区溢出:修改前会先检查空间是否足够,不足则自动扩容。
- 减少内存重分配:采用空间预分配和惰性空间释放策略。
- 二进制安全:不以
\0判断结束,可以存储图片、音频等二进制数据。
一个字符串类型的值能存储的最大容量是多少?
512 MB
为什么会设计 Stream?
在 Stream 出现之前,Redis 的消息发布订阅有明显痛点:
- List 类型:虽能持久化,但不支持多消费组,确认机制(ACK)实现复杂。
- Pub/Sub:无法持久化,消息“发完即丢“,消费者离线会导致消息丢失。
Stream 的设计目标:提供一个支持持久化、支持多消费组、支持消息确认机制的高可用消息队列模型。
Stream 用在什么场景?
- 异步任务处理:需要保证消息不丢失的任务流。
- 多端消费:同一个数据流需要被不同的业务系统(如结算系统、通知系统)同时消费。
- 高性能日志采集:利用其追加写入的特性,记录海量流水数据。
消息 ID 的设计是否考虑了时间回拨的问题?
考虑了。 Stream 的 ID 默认格式是 <millisecondsTime>-<sequenceNumber>。
- 防御机制:Redis 会记录服务器当前最大的 ID 时间戳。
- 处理逻辑:如果系统时间发生回拨,导致产生的时间戳小于上一个 ID,Redis 会强制使用上一次的时间戳,并递增其序列号,从而保证 ID 的单调递增性。
持久化和内存
Redis 的持久化机制是什么?各自的优缺点?一般怎么用?
| 机制 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| RDB(快照) | 定期将内存数据生成二进制文件保存 | 恢复快、文件体积小、性能开销低 | 数据丢失多(最后一次快照后会丢)、生成快照耗时 |
| AOF(日志) | 记录每一个写命令,以追加方式保存 | 数据更安全(秒级丢失)、日志可读性强 | 文件大、恢复慢、高并发下有 I/O 瓶颈 |
一般用法:混合持久化(RDB + AOF)。用 RDB 做全量备份,用 AOF 做增量记录,兼顾安全与速度。
Redis 过期键的删除策略有哪些?
Redis 采用 “惰性删除 + 定期删除” 配合使用:
- 惰性删除:访问 key 时才检查是否过期,过期则删除。(省 CPU,费内存)
- 定期删除:每隔一段时间随机抽取一部分 key 检查并删除。(折中方案)
Redis 内存淘汰算法有哪些?
当内存达到 maxmemory 限制时,触发以下算法:
| 算法 | 说明 |
|---|---|
| LRU(Least Recently Used) | 淘汰最久没被访问的数据 |
| LFU(Least Frequently Used) | 淘汰访问频率最低的数据 |
| Random | 随机淘汰 |
| TTL | 优先淘汰快过期的数据 |
| Noeviction | 不淘汰,写操作直接报错(默认配置) |
Redis 的内存用完了会发生什么?
- 如果设置了淘汰策略(如
allkeys-lru),Redis 会根据算法自动删除旧数据腾出空间。 - 如果没有设置策略或策略为
noeviction,Redis 将拒绝所有写请求(报错 OOM),但读请求正常。
Redis 如何做内存优化?
- 控制 Key 长度:缩短键名。
- 避免存储大 Key:拆分大的 Hash 或 List。
- 使用高效编码:尽量利用 ZipList(压缩列表)存储小规模数据。
- 设置过期时间:确保冷数据能自动释放。
- 开启内存碎片整理:配置
activedefrag yes。
Redis key 的过期时间和永久有效分别怎么设置?
- 设置过期:
EXPIRE key seconds或PEXPIRE key milliseconds - 永久有效:默认创建即永久。若需取消过期时间,使用
PERSIST key
Redis 中的管道有什么用?
- 作用:将多个命令打包一次性发送给服务器,减少网络 RTT(往返时延)。
- 效果:极大提升批量操作的性能。从“发一个收一个“变成“发一堆收一堆“。