Redis面试总结
讲一下redis的数据类型
数据类型 | 简介 | 典型应用场景 |
---|---|---|
String(字符串) | 最基础的类型,值可以是字符串、整数或浮点数,最大长度 512MB。 | 缓存、计数器(如文章阅读量)、分布式锁。 |
Hash(哈希) | 键值对的集合,适合存储对象(如用户信息:name、age、email)。 | 存储用户信息、商品属性等结构化数据。 |
List(列表) | 有序的字符串列表,允许重复元素,支持两端插入 / 删除。 | 消息队列、最新消息排行、历史记录。 |
Set(集合) | 无序的字符串集合,不允许重复元素,支持交集、并集、差集等操作。 | 好友关系、标签去重、共同好友计算。 |
Sorted Set(有序集合) | 类似 Set,但每个元素关联一个分数(score),按分数排序,不允许重复元素。 | 排行榜(如游戏积分排名)、范围查询。 |
Bitmap(位图) | 基于 String 实现的二进制位操作,适合存储布尔值序列(如用户签到状态)。 | 签到统计、活跃用户判断、权限标记。 |
HyperLogLog | 用于基数统计(不重复元素个数),占用空间极小(约 12KB)。 | UV(独立访客)统计、数据去重估算。 |
Geo | 基于 Sorted Set 实现的地理位置存储,支持距离计算、范围查询。 | 附近的人、地理位置推荐。 |
Sorted Set底层是怎么实现的?
Redis 有序集合(Sorted Set/ZSet)底层核心采用跳跃表(SkipList)+ 字典(Dictionary) 的组合结构实现,兼顾高效排序与快速查询:跳跃表通过多层有序链表结构支持按分数的范围查询、排名计算及插入删除操作(时间复杂度 O (logN)),字典则通过哈希表实现成员到分数的 O (1) 快速映射。此外,当元素数量少且简单时(默认元素数少于 128 且每个元素小于 64 字节),Redis 会用压缩列表(ZipList)替代上述结构以节省内存,连续内存存储,无指针开销,节省内存。
跳表是怎么设置层高的?
- 初始化:新节点的初始层高设为 1。
- 随机判断:使用一个随机数生成器来模拟抛硬币的过程。通常可以生成一个在 0 到 1 之间的随机小数,如果这个随机小数小于 0.5(即 50% 的概率),则将节点的层高加 1。
- 重复判断:重复步骤 2,不断地判断是否要增加层高,直到随机小数大于等于 0.5 或者达到了预设的最大层高。
Redis为什么使用跳表而不是用B+树?
-
内存结构与访问模式差异:
B+ 树的特性
磁盘友好:B+ 树的设计目标是优化磁盘I/O,通过减少树的高度来降低磁盘寻道次数(例如,一个3层的B+树可以管理数百万数据)。
节点填充率高:每个节点存储多个键值(Page/Block),适合批量读写。
范围查询高效:叶子节点形成有序链表,范围查询(如 ZRANGE)性能极佳。
跳表的特性
内存友好:跳表基于链表,通过多级索引加速查询,内存访问模式更符合CPU缓存局部性(指针跳跃更少)。
简单灵活:插入/删除时仅需调整局部指针,无需复杂的节点分裂与合并。
概率平衡:通过随机层高实现近似平衡,避免了严格的平衡约束(如红黑树的旋转)。
Redis 是内存数据库,数据完全存储在内存中,不需要优化磁盘I/O,因此 B+ 树的磁盘友好特性对 Redis 意义不大。而跳表的内存访问模式更优,更适合高频的内存操作。
-
实现复杂度的对比:
B+ 树的实现复杂度:
节点分裂与合并:插入/删除时可能触发节点分裂或合并,需要复杂的再平衡逻辑。
锁竞争:在并发环境下,B+ 树的锁粒度较粗(如页锁),容易成为性能瓶颈。
代码复杂度:B+ 树的实现需要处理大量边界条件(如最小填充因子、兄弟节点借用等)。
跳表的实现复杂度:
无再平衡操作:插入时只需随机生成层高,删除时直接移除节点并调整指针。
细粒度锁或无锁:跳表可以通过分段锁或无锁结构(如 CAS)实现高效并发。
代码简洁:Redis 的跳表核心代码仅需约 200 行(B+ 树实现通常需要数千行)。
-
性能对比:
查询性能
单点查询:跳表和 B+ 树的时间复杂度均为 O(log N),但跳表的实际常数更小(内存中指针跳转比磁盘块访问快得多)。
范围查询:B+ 树的叶子链表在范围查询时占优,但跳表通过双向链表也能高效支持 ZRANGE 操作。
写入性能
B+ 树:插入可能触发节点分裂,涉及父节点递归更新,成本较高。
跳表:插入仅需修改相邻节点的指针,写入性能更优(Redis 的 ZADD 操作时间复杂度为 O(log N))。
-
内存占用:
B+ 树:每个节点需要存储多个键值和子节点指针,存在内部碎片(节点未填满时)。
跳表:每个节点只需存储键值、层高和多个前向指针,内存占用更紧凑。
压缩列表的结构
一、压缩列表的整体结构
压缩列表是由一系列特殊编码的连续内存块组成的线性结构,整体结构如下(从左到右依次排列):
字段名 | 长度(字节) | 作用描述 |
---|---|---|
zlbytes | 4 | 记录整个压缩列表的总字节数(包括自身),用于快速计算压缩列表的内存大小或重分配内存。 |
zltail | 4 | 记录压缩列表尾节点(最后一个 entry)距离压缩列表起始地址的偏移量,用于快速定位尾节点(无需遍历)。 |
zllen | 2 | 记录压缩列表中entry 的数量。若数量超过 65535 (2^16-1),该值会固定为 65535 ,此时需通过遍历获取实际数量。 |
entry | 不定 | 压缩列表的实际存储元素,每个 entry 存储一个数据(字符串或整数),多个 entry 连续排列。 |
zlend | 1 | 压缩列表的结束标记,固定值为 0xFF (十进制 255),用于标识压缩列表的末尾。 |
二、Entry 的内部结构
每个 entry
是压缩列表的核心,用于存储具体数据。为了进一步节省内存,entry 的结构设计非常灵活,由以下 3 个部分组成(从左到右):
- prevlen:前一个 entry 的长度
-
作用:记录前一个 entry 的总字节数,用于支持压缩列表的反向遍历(从尾节点向前遍历)。
-
长度:根据前一个 entry 的实际长度动态调整,有两种可能:
- 若前一个 entry 的长度 ≤ 253 字节:
prevlen
用 1 字节 存储(直接记录长度值)。 - 若前一个 entry 的长度 > 253 字节:
prevlen
用 5 字节 存储(第一个字节固定为0xFE
,后 4 字节记录实际长度)。
设计逻辑:大多数场景下 entry 长度较小,用 1 字节即可覆盖,仅极端情况才用 5 字节,平衡空间和灵活性。
- 若前一个 entry 的长度 ≤ 253 字节:
- encoding:数据编码方式
-
作用:标识当前 entry 的
data
部分存储的是字符串还是整数,以及数据的长度(或类型),用于正确解析data
。 -
编码规则:根据数据类型和长度,encoding 的前缀(高 2 位或高 4 位)决定编码方式,具体分为两类:
(1)字符串编码(数据为字符串)
编码前缀决定字符串的长度范围,具体如下:
00xxxxxx
(前缀 00):字符串长度 ≤ 63 字节(6 位可表示 0-63)。编码占 1 字节,后 6 位直接记录字符串长度。01xxxxxx xxxxxxxx
(前缀 01):字符串长度 ≤ 16383 字节(14 位可表示 0-16383)。编码占 2 字节,后 14 位记录长度。10xxxxxx xxxxxxxx xxxxxxxx xxxxxxxx
(前缀 10):字符串长度 > 16383 字节。编码占 5 字节,第一个字节为10xxxxxx
(固定前缀),后 4 字节记录实际长度(32 位无符号整数)。
(2)整数编码(数据为整数)
编码前缀直接标识整数类型,
data
部分存储整数的二进制值,无需额外长度字段:11000000
(0xC0):表示data
是 16 位整数(2 字节)。11010000
(0xD0):表示data
是 32 位整数(4 字节)。11100000
(0xE0):表示data
是 64 位整数(8 字节)。11110000
(0xF0):表示data
是 8 位整数(1 字节,有符号)。11111110
(0xFE):表示data
是 4 位整数(0-12,无符号,Redis 预定义的小整数优化)。1111xxxx
(xxxx
为 0001-1101):表示data
是 0-12 的小整数(xxxx
减 1 即为值,如11110001
表示 0,11110010
表示 1,以此类推)。
- data:实际数据
- 作用:存储 entry 的具体值,内容由encoding字段决定:
- 若为字符串编码:
data
是字符串的二进制数据,长度由encoding
解析。 - 若为整数编码:
data
是整数的二进制表示(如 16 位、32 位整数的字节序列)。
- 若为字符串编码:
压缩列表中的连锁更新问题
连锁更新(Cascade Update)是压缩列表(ziplist)在特定条件下发生的性能退化现象。当连续多个节点的长度接近 254 字节时,修改其中一个节点可能引发后续节点的级联更新,导致多次内存重分配。
具体例子:
一、触发条件
连锁更新的发生需要同时满足以下条件:
- 连续节点长度临界:存在多个连续节点,其长度刚好在 254 字节左右
- 节点长度变化:某个操作导致这些节点的长度发生变化(如插入、删除、修改)
- prevlen 字段扩展:长度变化使得后续节点的
prevlen
字段需要从 1 字节扩展为 5 字节
示例场景
假设有四个连续节点 A、B、C、D,长度分别为 253、252、253、252 字节:
- 每个节点的
prevlen
字段均为 1 字节(因为前一节点长度 < 254 字节) - 若修改节点 A 使其长度变为 254 字节,将触发连锁反应
二、连锁更新过程
以下图为例,展示修改节点 A 后的连锁更新过程:
初始状态:
[节点A=253B] [节点B=252B] [节点C=253B] [节点D=252B]
prevlen=1B prevlen=1B prevlen=1B prevlen=1B
步骤1:修改节点A,长度变为254B
[节点A=254B] [节点B=252B] [节点C=253B] [节点D=252B]
prevlen=1B prevlen=? prevlen=1B prevlen=1B
步骤2:节点B的prevlen需从1B扩展为5B
[节点A=254B] [节点B=256B] [节点C=253B] [节点D=252B]
prevlen=1B prevlen=5B prevlen=? prevlen=1B
步骤3:节点C的prevlen需从1B扩展为5B
[节点A=254B] [节点B=256B] [节点C=257B] [节点D=252B]
prevlen=1B prevlen=5B prevlen=5B prevlen=?
步骤4:节点D的prevlen需从1B扩展为5B
[节点A=254B] [节点B=256B] [节点C=257B] [节点D=256B]
prevlen=1B prevlen=5B prevlen=5B prevlen=5B
listpack的结构
Redis 的 Listpack 整体结构及节点结构如下:
一、Listpack 整体结构
[总长度(total_bytes)] [元素数量(num_elements)] [节点1] [节点2] ... [节点N] [结束标记(end)]
- 总长度 (total_bytes):4 字节无符号整数,记录整个 Listpack 的总字节数。
- 元素数量 (num_elements):4 字节无符号整数,记录包含的节点总数。
- 节点列表:连续存储的 N 个节点(具体结构见下文)。
- 结束标记 (end):1 字节固定值
0xFF
,标识 Listpack 末尾。
二、单个节点结构
[element_length] [encoding-data]
-
element_length:可变长度(1~5 字节),采用 VarInt 编码,记录当前节点的总字节数(包括自身和 encoding-data)。
-
encoding-data
:包含两部分:
- encoding:1~2 字节,标识数据类型(如字符串 / 整数)和数据长度。
- data:实际存储的数据(字符串字节或整数二进制值),长度由 encoding 定义。
哈希表扩容时,有读请求会怎么查?如果此时添加一个新的键值对呢?
先从哈希表1查,若没有再在哈希表2查。如果此时添加新的键值对,则会在哈希表2里插入,因为在渐进式hash中,要确保哈希表1的键值对是减少的,这样哈希表最终才会变成空表。
哈希表扩容过程
- 给「哈希表 2」 分配空间,一般会比「哈希表 1」 大 2 倍;
- 将「哈希表 1 」的数据迁移到「哈希表 2」 中;
- 迁移完成后,「哈希表 1 」的空间会被释放,并把「哈希表 2」 设置为「哈希表 1」,然后在「哈希表 2」 新创建一个空白的哈希表,为下次 rehash 做准备。
渐进式哈希过程
- 准备新表:当哈希表的负载因子过高(大于等于 1 且无 RDB 快照或 AOF 重写,或大于等于 5)需扩容,或负载因子过低(小于 0.1)需缩容时,先申请一个新的哈希表。若为扩容,新表通常是旧表大小的两倍;缩容时,新表大小为第一个大于等于当前 key 数量的 2 的 n 次方,且最小为 4。
- 初始化标记:使用一个标记
rehashidx
记录迁移进度,初始设为 0,表示从旧表的第 0 个位置开始迁移。 - 操作顺带迁移:每次对哈希表进行查、改、删数据操作时,除了执行相应操作外,还会顺手将旧表中
rehashidx
位置的数据迁移到新表。迁移完成后,rehashidx
自增 1,指向下一个待迁移的位置。新增数据则直接放入新表,旧表不再存放新数据。 - 定时任务辅助:如果长时间没有对哈希表进行操作,系统会开启定时任务,每次最多花费 1 毫秒来迁移数据,避免迁移任务长时间未完成。
- 完成迁移与清理:当旧表中的数据全部迁移到新表后,释放旧表的空间,将新表改名为 “旧表”(即更新相关引用,使原来指向旧表的指针指向新表),并将
rehashidx
重置为 - 1,等待下次扩容或缩容时复用。
String 是使用什么存储的?为什么不用 c 语言中的字符串?
Redis使用SDS(简单动态字符串)存储String。
一、为什么不用 C 语言的字符串?
C 语言使用 以空字符 '\0'
结尾的字符数组 表示字符串,这种方式有以下 局限性:
- 获取字符串长度的时间复杂度高
C 字符串需要遍历整个数组直到遇到'\0'
才能确定长度,时间复杂度为 O(n)。而 Redis 作为高性能数据库,需要快速获取字符串长度(如执行STRLEN
命令)。 - 缓冲区溢出风险
C 字符串在进行strcat
、sprintf
等操作时,如果没有预先分配足够空间,容易导致缓冲区溢出。Redis 需要安全地处理字符串操作,避免此类风险。 - 二进制不安全
C 字符串只能存储文本数据,因为'\0'
被用作字符串结束符,无法存储包含'\0'
的二进制数据(如图片、序列化对象等)。而 Redis 需要支持二进制安全的存储。 - 内存分配效率低
C 字符串每次修改(如追加、截断)都需要重新分配内存并复制数据,频繁操作会导致大量内存分配和释放,影响性能。
SDS 的优势:
- O (1) 时间复杂度获取长度
通过len
字段直接记录字符串长度,无需遍历,执行STRLEN
命令的时间复杂度为 O(1)。 - 防止缓冲区溢出
SDS 在进行字符串修改时,会先检查free
空间是否足够。如果不足,会自动扩容缓冲区,避免溢出。 - 二进制安全
SDS 使用len
字段而非'\0'
来判断字符串结束,因此可以存储任意二进制数据,包括包含'\0'
的数据。 - 内存预分配和惰性释放
- 预分配:当字符串增长时,SDS 会预分配额外空间(例如加倍),减少后续扩容次数。
- 惰性释放:当字符串缩短时,SDS 不会立即释放多余空间,而是通过
free
字段标记,避免频繁内存操作。
- 兼容 C 字符串函数
SDS 的buf
数组仍然以'\0'
结尾,因此可以直接使用部分 C 字符串函数(如printf
)。
Redis为什么快?
-
全内存操作与高效的数据结构:Redis 将数据存储在内存中,读写操作仅涉及内存访问,避免了磁盘 I/O 的巨大开销(磁盘随机读写延迟通常在毫秒级,而内存读写延迟在纳秒级)。这使得 Redis 的单线程处理能力就能达到10 万级 QPS(每秒查询次数)。
-
单线程架构与I/O多路复用:Redis 的核心处理逻辑是单线程的(6.0 版本前),避免了多线程上下文切换和锁竞争的开销。单线程设计简化了代码实现,并利用了内存操作的原子性(如 INCR 命令)。Redis 使用epoll/kqueue等 I/O 多路复用机制处理大量并发连接,单个线程可以高效地处理数千个客户端连接。
Redis哪些地方使用了多线程?
Redis 在 2.6 版本,会启动 2 个后台线程,分别处理关闭文件、AOF 刷盘这两个任务;
Redis 在 4.0 版本之后,新增了一个新的后台线程,用来异步释放 Redis 内存,也就是 lazyfree 线程。例如执行 unlink key / flushdb async / flushall async 等命令,会把这些删除操作交给后台线程来执行,好处是不会导致 Redis 主线程卡顿。因此,当我们要删除一个大 key 的时候,不要使用 del 命令删除,因为 del 是在主线程处理的,这样会导致 Redis 主线程卡顿,因此我们应该使用 unlink 命令来异步删除大key。
在 Redis 6.0 版本之后,也采用了多个 I/O 线程来处理网络请求,这是因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上。所以为了提高网络 I/O 的并行度,Redis 6.0 对于网络 I/O 采用多线程来处理。但是对于命令的执行,Redis 仍然使用单线程来处理,所以大家不要误解Redis 有多线程同时执行命令。
Redis I/O多线程配置参数
io-threads N
(Redis 6.0+):配置网络 I/O 线程数量(默认 1,即禁用多线程 I/O;建议设为 CPU 核心数的 1/2 或 1/4),仅负责网络数据的读取(read
)和响应发送(write
),不参与命令解析和执行。io-threads-do-reads yes
:开启 I/O 线程的读操作并行处理(默认关闭)。
Redis怎么实现的I/O多路复用
-
IO多路复用解决的问题:
- 阻塞式 IO:若单线程逐个处理连接,当某个连接无数据可读时,线程会阻塞等待,导致其他就绪连接无法被及时处理。
- 轮询效率低:若用非阻塞 IO 轮询所有连接,会浪费大量 CPU 资源在无数据的连接上。
-
实现流程:
一、初始化:准备多路复用环境
- 创建多路复用器实例
应用程序首先调用操作系统提供的多路复用器初始化接口,创建一个多路复用器实例(如 Linux 的epoll_create
、BSD 的kqueue
、传统的select
无需显式创建)。- 例如,Redis 在 Linux 下会调用
epoll_create
创建一个epfd
(epoll 实例句柄),后续所有 FD 的监控都通过这个句柄操作。
- 例如,Redis 在 Linux 下会调用
- 注册待监控的 FD 及事件类型
应用程序将需要监控的 FD(如监听套接字、客户端连接套接字)注册到多路复用器中,并指定要监控的事件类型:- 读事件(Readable):当 FD 有数据可读时触发(如客户端发送请求、新连接到来)。
- 写事件(Writable):当 FD 可写入数据时触发(如缓冲区空闲,可发送响应)。
- 异常事件:如连接断开、错误发生等。
例如,Redis 启动时会将监听套接字(如 6379 端口)注册为 “读事件”,回调函数为acceptTcpHandler
(用于接受新连接)。
二、事件循环:等待并处理就绪事件
多路复用的核心是一个无限循环(Event Loop),应用程序通过这个循环不断等待和处理就绪事件,流程如下:
- 等待就绪事件(阻塞阶段)
应用程序调用多路复用器的 “等待接口”,阻塞等待多路复用器返回就绪的 FD 列表。此时线程进入休眠状态,不占用 CPU 资源,直到以下情况发生:
-
至少有一个 FD 的注册事件就绪(如可读 / 可写)。
-
等待超时(由应用程序设置,避免无限阻塞)。
-
被信号中断。
不同多路复用器的等待接口不同:
epoll
:epoll_wait(epfd, events, maxevents, timeout)
kqueue
:kevent(kq, changelist, nchanges, eventlist, nevents, timeout)
select
:select(nfds, readfds, writefds, exceptfds, timeout)
例如,Redis 的事件循环会先计算最近的定时任务时间(如过期键清理),将其作为
epoll_wait
的超时时间,避免因无限等待阻塞定时任务。
- 接收就绪事件列表
多路复用器返回所有就绪的 FD 及其对应的事件类型。不同模型的返回方式不同:
-
epoll/kqueue(事件驱动):直接返回就绪的 FD 列表,无需应用程序遍历所有 FD,效率高。
-
select/poll(轮询):返回所有 FD 的状态掩码,应用程序需遍历所有注册的 FD 检查是否就绪,效率低。
例如,
epoll_wait
会将就绪事件存储在events
数组中,每个元素包含 FD 和事件类型(如EPOLLIN
表示读就绪)。
- 处理就绪事件(非阻塞阶段)
应用程序遍历多路复用器返回的就绪 FD 列表,根据事件类型调用对应的回调函数处理:
-
读事件处理:若 FD 是监听套接字,调用
accept
接受新连接;若 FD 是客户端连接,调用read
读取请求数据并解析。 -
写事件处理:调用
write
向 FD 发送响应数据(如 Redis 向客户端返回命令执行结果)。 -
异常处理:如关闭连接、记录错误日志等。
关键特性:所有 I/O 操作均采用非阻塞方式(通过
fcntl
将 FD 设置为O_NONBLOCK
),确保单个事件处理不会长时间阻塞线程。例如,Redis 读取客户端数据时,若一次未读完,会在下次读事件就绪时继续读取,避免阻塞其他连接。
- 重复循环
处理完当前就绪事件后,事件循环回到 “等待就绪事件” 阶段,继续监控 FD 状态,周而复始。
- 创建多路复用器实例
为什么 epoll 是 Redis 在 Linux 上的最优选择?
在 Linux 系统中,Redis 优先使用 epoll 而非 select/poll,原因是 epoll 解决了传统模型的核心缺陷:
- 无连接数限制:select/poll 受限于固定数组大小或用户态数组拷贝,而 epoll 支持的 FD 数量仅受系统最大文件描述符限制(可配置)。
- 事件驱动而非轮询:select/poll 需要遍历所有 FD 检查状态,时间复杂度为 O (n);epoll 只需处理就绪的 FD(通过内核维护的就绪列表),时间复杂度为 O (1)。
- 减少用户态与内核态拷贝:epoll 通过内存映射(mmap)避免了 FD 列表在用户态和内核态之间的拷贝,效率更高。
Redis的网络模型是怎么样的?
- 单线程处理命令
Redis 的命令执行逻辑(如读取命令、解析参数、执行操作、返回结果)由单个主线程完成,避免了多线程锁竞争和上下文切换的开销,确保命令执行的原子性。 - I/O 多路复用处理网络连接
利用操作系统提供的 I/O 多路复用技术(如 Linux 的epoll
、macOS 的kqueue
),单线程可以同时监控数千甚至数万个客户端连接的 I/O 状态(可读 / 可写),高效处理并发请求。 - 事件驱动模型
将网络操作抽象为文件事件(如连接建立、数据可读、数据可写)和时间事件(如定时任务),通过事件循环(Event Loop)不断处理就绪事件,实现非阻塞的高效运行。
如何实现Redis原子性?
一、单线程执行
- 核心逻辑:Redis 的命令执行由单线程处理,同一时间仅执行一个命令,天然避免多线程竞争。
- 作用:确保基本命令(如
SET
、INCR
)的原子性,无需额外同步机制。
二、原子命令
- 内置操作:Redis 提供大量原子命令(如
INCR
、HSET
、RPOP
),这些命令在单线程中不可中断。 - 原理:命令执行过程中不会插入其他操作,保证操作的完整性。
三、事务机制
- MULTI/EXEC:将多个命令打包执行,执行期间不会插入其他客户端命令。
- WATCH:通过乐观锁机制,监视键的变化,若键在事务前被修改,事务自动失败。
- 局限性:不支持事务回滚,若部分命令失败,已执行的命令不会撤销。
四、分布式锁
- SETNX + 过期时间:通过原子性的
SETNX
命令获取锁,并设置过期时间防止死锁。 - RedLock 算法:在集群环境中,向多个节点申请锁,超过半数成功则认为加锁成功。
- 作用:保障跨节点操作的原子性,避免分布式环境中的竞态条件。
五、Lua 脚本
- 原子执行:Redis 使用单线程执行 Lua 脚本,脚本内部操作不可中断。
- 应用场景:实现复杂的原子逻辑(如先检查后操作),避免多个命令间的竞争。
Redis有哪2种持久化方式?分别的优缺点是什么?
一、RDB(Redis Database Snapshot)
机制
- 定时快照:将某一时刻的内存数据以二进制格式写入磁盘(默认文件名
dump.rdb
)。 - 触发方式:
- 手动触发:
SAVE
(阻塞主线程)或BGSAVE
(后台子进程执行)。 - 自动触发:通过配置(如
save 900 1
表示 900 秒内至少 1 个 key 变化时触发)。 - 主从复制时,主节点自动生成 RDB 并同步给从节点。
- 手动触发:
优点
- 高性能:仅需周期性生成文件,写入操作是一次性的,对 Redis 性能影响小(尤其
BGSAVE
)。 - 恢复速度快:RDB 文件是内存数据的二进制快照,重启时直接加载,适合大规模数据恢复。
- 文件紧凑:RDB 文件经过压缩,占用空间小,便于传输和备份。
缺点
- 数据安全性低:若 Redis 崩溃,可能丢失最后一次快照后的数据(取决于快照频率)。
- fork 开销大:生成 RDB 需通过
fork()
创建子进程,内存较大时可能导致短暂阻塞(尤其大内存实例)。 - 兼容性问题:不同 Redis 版本的 RDB 文件可能不兼容,升级时需注意。
二、AOF(Append Only File)
机制
-
日志追加:将 Redis 执行的写命令(如
SET
、INCR
)以文本形式追加到文件(默认appendonly.aof
)。 -
同步策略
(通过appendfsync配置):
always
:每次写操作同步到磁盘(最安全,性能最差)。everysec
:每秒同步一次(默认,平衡安全与性能)。no
:由操作系统决定何时同步(最快,可能丢失较多数据)。
-
AOF 重写:定期合并冗余命令(如多次
SET
合并为一次),压缩文件体积。
优点
- 数据安全性高:
everysec
策略下,最多丢失 1 秒数据;always
策略几乎不丢数据。 - 实时持久化:写操作立即记录,适合对数据完整性要求高的场景(如支付系统)。
- 日志易读:AOF 文件是文本格式,可直接查看和修改,便于调试。
- 兼容性强:即使 Redis 崩溃,AOF 文件可通过
redis-check-aof
工具修复。
缺点
- 文件体积大:AOF 记录所有写命令,长期运行后文件可能远大于 RDB。
- 性能开销:频繁磁盘写入(尤其
always
策略)可能降低 Redis 响应速度。 - 恢复速度慢:重启时需重新执行 AOF 中的所有命令,数据量大时耗时较长。
过期删除策略和内存淘汰策略有什么区别?
对比维度 | 过期删除策略 | 内存淘汰策略 |
---|---|---|
触发条件 | 当键的过期时间(TTL)耗尽时触发(被动或主动检查)。 | 当内存使用达到预设上限(如maxmemory ),且有新数据需要写入时触发。 |
处理对象 | 仅针对设置了过期时间(EXPIRE )的键。 | 可针对所有键(包括未设置过期时间的键),具体范围由策略类型决定(如仅淘汰过期键或所有键)。 |
核心目标 | 清理 “逻辑过期” 的数据,保证数据有效性(如避免使用过期的缓存)。 | 释放内存空间,避免系统因内存不足崩溃,确保新数据可正常写入。 |
实现方式 | 常见方式: - 惰性删除(访问键时才检查是否过期,过期则删除); - 定期删除(定时扫描部分过期键并删除)。 | 常见方式: 根据预设策略(如 LRU、LFU、随机等)选择待删除的键,删除后写入新数据。 |
作用阶段 | 贯穿数据生命周期,在键过期后持续尝试清理(可能延迟)。 | 仅在内存不足时 “被动触发”,是内存压力下的应急机制。 |
与数据价值的关联 | 不直接关联数据 “价值”,仅关注是否过期(过期即失效)。 | 直接关联数据 “价值”(如通过 LRU/LFU 判断访问频率,保留高价值数据)。 |
那为什么我不过期立即删除?
在缓存系统中不采用 “过期立即删除”,是因为其需要实时监控所有过期键的 TTL 变化并在精确时间点执行删除,这会带来极高的定时器管理成本、引发瞬间并发删除的性能冲击,且对未访问的过期键做无效操作,严重影响系统稳定性与资源利用率。因此主流系统采用 “惰性删除 + 定期删除” 的混合策略,仅在访问时检查删除过期键,并定期随机扫描清理部分过期键,在 “清理过期数据” 与 “降低系统开销” 间实现平衡,避免为绝对时效性牺牲性能。
Redis的缓存失效会不会立即删除?
不会,Redis 的过期删除策略是选择「惰性删除+定期删除」这两种策略配和使用。
惰性删除策略的做法是,不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
定期删除策略的做法是,每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
常见内存淘汰策略分类及说明
1. 仅淘汰 “已设置过期时间” 的键(针对临时数据)
这类策略仅从设有过期时间(TTL)的键中选择淘汰对象,适用于需要优先保留永久数据(未设过期时间)的场景。
- volatile-lru:从已设置过期时间的键中,淘汰最近最少使用(LRU,Least Recently Used) 的键。
例:用户最近 30 分钟未访问的临时会话缓存,优先被淘汰。 - volatile-ttl:从已设置过期时间的键中,淘汰剩余生存时间(TTL)最短的键。
例:10 分钟后过期的优惠券缓存,比 1 小时后过期的更先被淘汰。 - volatile-random:从已设置过期时间的键中,随机淘汰一个键。适用于对数据价值无明显优先级的场景。
- volatile-lfu(Redis 4.0+):从已设置过期时间的键中,淘汰最近最少频率使用(LFU,Least Frequently Used) 的键。
例:一个月内仅被访问 1 次的缓存,比每天访问 10 次的更先被淘汰(比 LRU 更关注长期热度)。
2.淘汰所有键(包括未设置过期时间的键)
这类策略不区分键是否设置过期时间,会从所有键中选择淘汰对象,适用于允许清理永久数据以保证新数据写入的场景。
- allkeys-lru:从所有键中,淘汰最近最少使用(LRU) 的键。
例:全局缓存中,长期无人访问的冷门数据优先被淘汰,保留热门数据。 - allkeys-random:从所有键中随机淘汰一个键。适用于数据访问分布均匀、无明显冷热区分的场景(如随机生成的临时 ID 缓存)。
- allkeys-lfu(Redis 4.0+):从所有键中,淘汰最近最少频率使用(LFU) 的键。更精准反映数据的长期访问热度,适合高频访问场景。
3.不淘汰数据:拒绝写入新数据
- noeviction:这是 Redis 的默认策略。当内存达到
maxmemory
后,不再淘汰任何数据,对新的写入请求返回错误(如OOM command not allowed when used memory > 'maxmemory'
),但读请求仍可正常执行。
适用于不允许数据丢失的场景(如核心业务缓存),但需提前做好内存规划,避免触发写入失败。
Redis定时删除的核心规则
Redis 定时删除的核心规则是:抽样 20 个键,若过期比例>50% 则继续迭代,直至比例<50% 或达到最大迭代次数。
Redis主从同步中的增量和完全同步怎么实现?
一、主从同步的基础架构
Redis 主从复制采用异步复制模式:
- 主节点负责处理写请求,并将写命令记录到复制积压缓冲区(replication backlog);
- 从节点通过向主节点发送
PSYNC
命令请求同步数据,根据自身状态选择增量同步或完全同步。
二、完全同步(Full Resynchronization)
- 触发场景
- 从节点初次连接主节点时;
- 从节点断开后重新连接,且复制偏移量(replication offset)已不在主节点的积压缓冲区中;
- 主节点执行
SYNC
命令强制全量同步(Redis 2.8 前的旧机制)。
- 实现流程
主节点 从节点
│ │
│ 1. 从节点发送PSYNC命令,携带自身run_id和offset(初次为-1)
├──────────────────────────────►│
│ │
│ 2. 主节点判断需全量同步,回复+FULLRESYNC响应(包含run_id和offset)
◄──────────────────────────────┤
│ │
│ 3. 主节点执行BGSAVE,生成RDB文件(内存快照)
│ │
│ 4. 主节点将RDB文件发送给从节点
├──────────────────────────────►│
│ │
│ 5. 从节点清空当前数据,加载RDB文件
│ │
│ 6. 主节点持续将写命令(生成RDB期间的增量)发送给从节点
├──────────────────────────────►│
│ │
│ 7. 从节点执行接收到的写命令,完成同步
│ │
- 关键机制
- RDB 持久化:主节点通过
BGSAVE
在后台生成内存快照,避免阻塞主线程; - 写命令缓冲:主节点在生成 RDB 期间,会将新的写命令同时记录到复制积压缓冲区,确保从节点加载 RDB 后能追上最新数据;
- 原子性:从节点在加载 RDB 期间,会拒绝客户端请求,保证数据一致性。
三、增量同步(Partial Resynchronization)
- 触发场景
- 从节点断开后重新连接,且其复制偏移量(offset)仍在主节点的复制积压缓冲区中;
- 主从网络短暂抖动,导致部分命令丢失。
- 实现流程
主节点 从节点
│ │
│ 1. 从节点发送PSYNC命令,携带上次保存的主节点run_id和offset
├──────────────────────────────►│
│ │
│ 2. 主节点验证offset是否在积压缓冲区中:
│ - 若存在,回复+CONTINUE,表示支持增量同步
│ - 若不存在,回复+FULLRESYNC,触发全量同步
│ │
│ 3. 主节点从积压缓冲区中提取offset之后的所有写命令
│ │
│ 4. 主节点将增量写命令发送给从节点
├──────────────────────────────►│
│ │
│ 5. 从节点执行增量命令,完成同步
│ │
- 关键机制
- 复制积压缓冲区:主节点维护一个固定大小(默认 1MB)的环形缓冲区,存储最近的写命令。从节点通过 offset 定位需要的增量命令;
- run_id:每个 Redis 实例启动时生成的唯一标识符,用于判断主节点是否发生变化(如故障转移后新主节点的 run_id 不同);
- 高效性:仅同步断开期间的增量数据,无需生成和传输 RDB 文件,开销远低于全量同步。
redis主从和集群可以保证数据一致性吗 ?
Redis的主从架构和集群模式均以异步复制为核心机制,主要保证数据的最终一致性:在网络稳定、无故障时,从节点或集群分片内的从节点会通过同步逐渐追平主节点数据,最终达到一致状态。但受限于异步设计,二者均无法提供强一致性——主从架构中,读写分离可能因同步延迟导致“脏读”,主节点故障未同步的写操作会丢失;集群模式下,跨分片操作缺乏原子性,网络分区可能引发数据冲突,故障转移时也可能丢失未同步数据。尽管可通过WAIT
命令、监控延迟等手段降低不一致风险,但本质上无法避免短暂的数据延迟或故障导致的永久性不一致,需根据业务对一致性的容忍度权衡使用。
哨兵机制原理
一、核心目标
哨兵机制通过分布式的哨兵节点集群,实现三大功能:
- 监控(Monitoring):持续检查主节点(Master)和从节点(Slave)是否正常运行;
- 故障检测(Notification):当主节点故障时,通过哨兵集群协商判定 “客观下线”,并通知其他组件(如客户端、从节点);
- 故障转移(Failover):自动将一个从节点升级为新主节点,调整其他从节点同步新主节点,并更新客户端的连接信息,恢复服务可用性。
二、架构组成
哨兵机制的运行依赖两类节点协同工作:
- 哨兵节点(Sentinel Nodes):一组独立的 Redis 进程(通常 3~5 个,避免单点),不存储数据,仅负责监控和决策。
- 主从节点(Master/Slave):即原有的 Redis 主从架构,哨兵通过监控主节点间接管理从节点。
三、核心原理流程
- 初始化与监控建立
- 哨兵启动后,通过配置文件指定的主节点地址,与主节点建立网络连接(包括命令连接和订阅连接),定期发送
PING
命令检测主节点存活状态,并通过INFO replication
命令获取主节点的从节点列表。 - 哨兵基于主节点返回的从节点信息,自动与所有从节点建立同样的监控连接,形成对整个主从集群的 “全量监控网”。
- 哨兵之间也会通过相互发现(基于主节点的
__sentinel__:hello
频道订阅)建立连接,形成哨兵集群,同步监控信息。
- 故障检测:主观下线与客观下线
-
主观下线(Subjective Down, SDOWN):单个哨兵节点通过
PING
命令检测主 / 从节点,若在配置的down-after-milliseconds
时间内未收到有效响应(如PONG
),则该哨兵单方面判定节点 “主观下线”。 -
客观下线(Objective Down, ODOWN)
:当主节点被某个哨兵标记为 “主观下线” 后,该哨兵会向其他哨兵发送
SENTINEL is-master-down-by-addr
命令,询问其他哨兵是否也认为主节点下线。若
超过指定数量(quorum 配置,通常为哨兵总数的半数以上)
的哨兵同意主节点下线,则判定主节点 “客观下线”(即确认主节点真正故障)。
- 例:3 个哨兵,
quorum=2
,若 2 个哨兵均认为主节点下线,则触发客观下线。
- 例:3 个哨兵,
- 故障转移:从节点升级为主节点
主节点被判定为客观下线后,哨兵集群会选举出一个 “领头哨兵(Leader Sentinel)”,由其执行故障转移流程:
- 步骤 1:筛选合格从节点
排除 “主观下线”“断线时间过长”“优先级为 0(不参与选举)” 的从节点,剩下的从节点按优先级(slave-priority
配置,值越小优先级越高)、复制进度(与原主节点的同步偏移量越接近越优先)、运行 ID(ID 越小越优先) 排序,选择最优从节点作为候选新主节点。 - 步骤 2:升级新主节点
领头哨兵向候选从节点发送SLAVEOF NO ONE
命令,使其停止同步原主节点,升级为新主节点。 - 步骤 3:调整其他从节点
领头哨兵向其他从节点发送SLAVEOF <新主节点IP> <端口>
命令,让它们从新主节点同步数据,形成新的主从架构。 - 步骤 4:更新主节点信息
哨兵集群将新主节点的地址写入自身配置,并通过__sentinel__:hello
频道广播给其他哨兵和客户端,确保客户端后续连接新主节点。
- 原主节点恢复后的处理
若原主节点故障恢复,哨兵会检测到其存活,此时不会将其直接恢复为主节点,而是发送SLAVEOF <新主节点IP> <端口>
命令,让其作为从节点同步新主节点的数据,避免数据冲突。
哨兵机制的选主节点的算法介绍一下
一、第一步:领头哨兵选举(Leader Sentinel Election)
当主节点客观下线后,哨兵集群需先选举出一个领头哨兵(Leader Sentinel),由其负责执行后续的故障转移操作(如升级从节点、调整集群拓扑)。这一选举过程基于Raft 协议的简化版投票机制,核心规则如下:
- 触发条件
任何一个哨兵在发现主节点客观下线后,会向其他哨兵发送 SENTINEL is-master-down-by-addr
命令,请求其他哨兵投票给自己成为领头哨兵。
- 投票规则
- 投票权唯一:每个哨兵在一次故障转移周期内,只能给第一个请求投票的哨兵投 1 票(先到先得),不可重复投票。
- 多数胜出:若某个哨兵获得的票数超过哨兵集群总数的半数以上(
(N/2)+1
,N 为哨兵总数),则立即当选为领头哨兵。 - 重试机制:若第一轮投票无哨兵获得多数票,等待一段时间(默认 1 秒)后重新发起投票,直到选出领头哨兵。
- 关键作用
领头哨兵的唯一性确保了故障转移操作由单一节点执行,避免多个哨兵同时操作导致的集群混乱(如重复升级从节点)。
二、第二步:新主节点筛选(Slave Selection)
领头哨兵当选后,需从原主节点的所有从节点中筛选出一个最优的从节点作为新主节点。筛选过程分三轮淘汰与排序,优先级依次递减,最终确定唯一候选者。
- 第一轮:基础筛选(排除无效从节点)
首先排除不符合条件的从节点,确保候选者 “健康且可用”:
- 排除处于 “主观下线(SDOWN)” 或 “断线状态” 的从节点(无法正常通信)。
- 排除最近 5 秒内未回复过哨兵
INFO
命令的从节点(可能网络异常)。 - 排除复制偏移量过小的从节点(与原主节点数据同步差距过大,数据丢失风险高),具体通过对比从节点的
master_repl_offset
与原主节点最后记录的偏移量,差距超过配置阈值(可选)则淘汰。 - 排除优先级为 0的从节点(通过
slave-priority
配置,手动标记为 “不参与选主”)。
- 第二轮:优先级排序(
slave-priority
优先)
经过基础筛选后,剩余从节点按 **slave-priority
配置值 ** 升序排序(值越小优先级越高)。这是用户可手动干预的核心参数,若某从节点优先级最高,则直接作为第一候选者。
- 第三轮:数据完整性排序(复制偏移量优先)
若多个从节点优先级相同(或未配置优先级),则比较从节点的复制偏移量(master_repl_offset
):偏移量越大,说明该从节点与原主节点的同步进度越接近(数据丢失越少),优先被选中。
- 第四轮:稳定性排序(运行 ID 最小优先)
若偏移量仍相同,则比较从节点的运行 ID(runid
):选择运行 ID 最小的从节点。这是一种兜底策略,确保最终有唯一候选者(运行 ID 是 Redis 节点启动时生成的唯一标识,越小表示节点启动越早,相对更稳定)。
Redis集群的模式了解吗 优缺点了解吗
一、核心模式:分片集群 + 主从复制
Redis 集群采用去中心化架构,将数据划分为 16384 个哈希槽(Hash Slot),每个主节点负责一部分哈希槽。例如:
- 3 个主节点时,节点 A 负责 0~5460 号槽,节点 B 负责 5461~10922 号槽,节点 C 负责 10923~16383 号槽。
- 客户端可直接连接任意节点,通过计算
CRC16(key) % 16384
确定键所在的哈希槽,进而路由到对应节点。
高可用性保障:
每个主节点可配置 1 个或多个从节点,当主节点故障时,其从节点会自动升级为新主节点(通过哨兵机制或内置投票算法),确保服务不中断。
优点总结
Redis 集群的核心优势在于通过哈希槽分片实现高效的水平扩展,突破单机内存和性能瓶颈,理论上吞吐量随节点数线性增长;内置主从复制与自动故障转移机制,无需额外部署哨兵,能在秒级完成主节点故障后的从节点升级,保障高可用性;去中心化架构省去代理层,客户端直接与节点通信,且支持哈希槽自动迁移,简化了扩缩容运维;数据按槽位集中存储,优化了同槽批量操作的效率,适合大规模数据存储和高并发读写场景。
缺点总结
Redis 集群的局限性主要源于分布式复杂性:跨槽事务支持有限,仅能在单个哈希槽内保证原子性,跨槽操作需依赖业务层补偿或 Lua 脚本,增加开发难度;节点扩缩容时的数据迁移可能导致短暂性能波动,网络分区易引发频繁故障转移甚至脑裂;客户端需实现哈希槽计算、请求路由和重定向处理逻辑,对客户端库兼容性要求较高;运维复杂度高于单机架构,需平衡节点数、主从配比以避免数据倾斜,且冗余部署(每个主节点至少 1 个从节点)会显著增加硬件成本。
什么是脑裂 ?
在分布式系统中,脑裂(Split Brain) 是指集群因网络故障等原因分裂成多个独立子集群,每个子集群都认为自己是 “唯一存活的集群”,并各自选举新的主节点或执行关键操作,最终导致数据不一致、资源冲突等严重问题。这一概念源于 “大脑分裂” 的类比 —— 原本统一的集群 “意识” 分裂成多个相互独立的 “意识”
为什么使用Redis?
- 高并发:单线程执行命令,减少多线程上下文切换,I/O多路复用,Redis 可以同时接收成千上万的客户端请求,将 I/O 等待时间用于处理其他请求,大幅提升并发连接处理能力。
- 高性能:Redis数据直接存储在内存里,操作Redis缓存相对于直接操作内存。
为什么redis比mysql要快?
内存存储:Redis 是基于内存存储的 NoSQL 数据库,而 MySQL 是基于磁盘存储的关系型数据库。由于内存存储速度快,Redis 能够更快地读取和写入数据,而无需像 MySQL 那样频繁进行磁盘 I/O 操作。
%0D%0A简单数据结构:Redis 是基于键值对存储数据的,支持简单的数据结构(字符串、哈希、列表、集合、有序集合)。相比之下,MySQL 需要定义表结构、索引等复杂的关系型数据结构,因此在某些场景下 Redis 的数据操作更为简单高效,比如 Redis 用哈希表查询, 只需要O1 时间复杂度,而MySQL引擎的底层实现是B+Tree,时间复杂度是O(logn)
%0D%0A线程模型:Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。
本地缓存和Redis缓存的区别?
-
本地缓存优点
- 本地缓存数据存储在应用进程内的内存中,访问时直接通过进程内调用(如内存地址引用),无网络通信开销,延迟可低至纳秒级或微秒级。
- 本地缓存通常通过编程语言原生工具或轻量级库实现(如 Python 的
lru_cache
、Java 的 Guava Cache),无需独立部署服务,维护成本几乎为零, - 不依赖外部服务或网络,避免了 Redis 可能遇到的网络抖动、连接超时、服务器宕机等问题,稳定性更高(只要应用进程存活,缓存就可用)。
-
本地缓存缺点
- 本地缓存依赖应用进程存活,若应用重启、崩溃或服务器宕机,缓存数据会全部丢失,需重新从数据库加载,可能引发 “缓存雪崩”(瞬间大量请求穿透到数据库)。
- 在多实例部署(如微服务集群、负载均衡后的多节点)时,每个应用实例的本地缓存独立存在,数据更新后难以同步到所有实例,易出现 “数据不一致”。
- 本地缓存的容量受限于应用进程的内存上限(如 JVM 堆内存通常为 4~16GB),若缓存数据量过大,会挤压应用业务逻辑的内存空间,甚至导致内存溢出(OOM)崩溃。
-
redis缓存优点
- 分布式一致性天然保障
Redis 作为独立的中心化服务,所有应用实例通过网络访问同一数据源,数据更新后只需刷新 Redis 缓存,所有实例即可获取最新数据,彻底解决多实例场景下的一致性问题。 - 容量可扩展性强
Redis 缓存的容量不受应用内存限制,可通过两种方式扩展:- 纵向扩展:增加单台 Redis 服务器的内存(如从 32GB 升级到 256GB);
- 横向扩展:通过 Redis Cluster 分片机制,将数据分散到多台服务器,理论上支持TB 级甚至 PB 级缓存容量,满足大规模数据场景。
- 高可用与数据持久化
- 高可用:支持主从复制(一主多从)、哨兵模式、Cluster 集群,可实现故障自动检测和切换,避免单点故障,服务可用性可达 99.99% 以上。
- 持久化:通过 RDB(快照)和 AOF(日志)机制将数据持久化到磁盘,即使 Redis 服务器重启,数据也可恢复,减少缓存重建成本。
- 分布式一致性天然保障
-
redis缓存缺点
- 网络开销导致延迟较高
Redis 缓存依赖网络通信(TCP 协议),访问流程为 “应用 → 网络 → Redis 服务器 → 网络 → 应用”,即使 Redis 单机命令处理速度极快(微秒级),网络延迟仍会增加整体耗时(通常 10~100 微秒,跨机房场景可能达毫秒级),性能略低于本地缓存。 - 部署与维护成本高
需要独立部署 Redis 服务或集群,需考虑资源分配、集群配置、监控告警、扩容缩容等问题,运维成本显著高于本地缓存。 - 存在网络依赖风险
依赖网络稳定性,若网络抖动、Redis 服务器宕机或连接池耗尽,可能导致缓存访问失败,需通过熔断、降级等机制保障可用性,增加系统复杂度。
- 网络开销导致延迟较高
Redis的大Key问题是什么?
Redis 的大 Key 问题是指在 Redis 中存储的某个键(Key)对应的值(Value)占用内存过大,或包含过多元素(如 Hash、List、Set、ZSet 等复杂数据结构中元素数量极多),从而引发一系列性能、稳定性和运维问题的场景。
危害
影响维度 | 具体问题描述 |
---|---|
内存占用失衡 | 大 Key 会占用大量内存,可能导致: - 单个 Redis 实例内存使用率骤增,触发内存淘汰策略(如 LRU),误删其他正常键; - 集群环境下,大 Key 所在节点内存压力远高于其他节点,导致数据分布不均,集群负载失衡。 |
读写性能下降 | - 读操作:读取大 Key 时,需从内存中加载大量数据,单线程处理耗时变长,阻塞后续请求(Redis 单线程无法并行处理命令); - 写操作:修改大 Key(如 Hash 新增字段、List 追加元素)时,若数据量大,会占用更多 CPU 时间,导致命令执行延迟升高。 |
网络传输瓶颈 | 大 Key 的读写会产生大量网络数据(如读取一个 10MB 的大 Key,需传输 10MB 数据),导致: - 客户端与 Redis 之间的网络带宽占用过高,延迟增加; - 集群环境下,节点间数据同步(如主从复制、集群迁移)时,大 Key 会加剧网络负载,甚至导致同步超时。 |
删除 / 过期阻塞 | Redis 删除大 Key 时(尤其是复杂数据结构),需遍历所有元素释放内存,单线程下会阻塞整个事件循环,导致其他命令长时间等待(即使使用UNLINK 异步删除,仍可能在后台线程中占用资源)。 若大 Key 设置了过期时间,过期删除时也可能因元素过多导致阻塞。 |
持久化效率降低 | - RDB 持久化:生成快照时需遍历所有键,大 Key 会增加 RDB 文件大小,延长生成时间,甚至导致 Redis 在持久化期间响应变慢; - AOF 持久化:大 Key 的修改会产生大量 AOF 日志,增加 AOF 文件体积和刷盘压力,影响重放效率(恢复时加载大 Key 耗时更长)。 |
集群迁移困难 | 在 Redis 集群中,当大 Key 所在的槽位需要迁移(如扩缩容、平衡负载)时,迁移大 Key 会消耗大量时间和资源,甚至导致迁移失败或集群暂时不可用。 |
解决方法
- 拆分大key
- 定期异步清理大key
什么是热key?
在 Redis 中,热 Key(热点 Key) 指的是在短时间内被高频访问(读或写)的 Key。这类 Key 由于访问量过大,可能成为系统性能瓶颈,甚至导致 Redis 节点负载过高、响应延迟增加,严重时可能引发服务雪崩。
危害
热 Key 作为高频访问的缓存 Key,其核心危害在于流量高度集中引发的连锁式系统压力传导:首先导致 Redis 节点因 CPU 飙升、带宽耗尽、内存碎片增加而单点过载,响应延迟剧增甚至节点宕机;进而引发缓存穿透 / 击穿,大量请求绕过缓存直接冲击数据库,同时导致应用线程池耗尽、重试机制加剧二次流量冲击,造成应用 “假死” 和数据库宕机;最终引发核心业务功能不可用(如秒杀下单失败、直播进房异常)、用户流失,以及紧急扩容、数据修复等运维成本激增,形成从缓存到应用再到业务的全链路雪崩效应,严重威胁系统稳定性与业务连续性
如何解决
一、架构层:分散热点流量
通过架构设计将集中的热 Key 访问分散到多个节点或层级,避免单点过载。
解决方案 | 核心原理 | 实现方式 | 适用场景 |
---|---|---|---|
多级缓存架构 | 增加本地缓存 / 接入层缓存,减少 Redis 直接访问 | - 应用本地缓存:用 Caffeine、Guava 等工具缓存热 Key 数据(设置较短过期时间,如 10 秒)。 - 接入层缓存:在 Nginx 或 API 网关层缓存热 Key 结果(如 Nginx 的 ngx_http_redis_module )。 | 读热点场景(如商品详情、新闻内容) |
Key 分片(拆分热点) | 将单个热 Key 拆分为多个子 Key,分散到不同 Redis 节点 | - 例如:热 Key stock:1001 拆分为 stock:1001:0 至 stock:1001:9 ,存储相同库存值。 - 访问时随机选择一个子 Key 读写,通过 SUM 或 MAX 聚合结果(读场景);写时同步更新所有子 Key(需保证最终一致性)。 | 读多写少的热 Key(如库存、计数器) |
Redis 集群扩容 | 增加热 Key 所在节点的从节点,分摊读流量 | - 在 Redis Cluster 中,为热 Key 所在的主节点添加多个从节点,通过读写分离将读请求路由到从节点。 - 若用哨兵模式,可手动将读请求定向到从节点。 | 读热点且集群化部署场景 |
热点隔离集群 | 为热 Key 单独部署 Redis 集群,与普通 Key 物理隔离 | - 秒杀场景中,将商品库存 Key 迁移到独立的 “秒杀 Redis 集群”,避免影响其他业务缓存。 - 独立集群配置更高性能的硬件(如高 CPU、大带宽)。 | 业务隔离性强的热 Key(如秒杀、直播) |
二、缓存设计层:优化热 Key 生命周期与访问逻辑
通过缓存设计减少热 Key 的产生概率,降低访问压力。
- 缓存预热 + 过期时间优化
- 缓存预热:在流量高峰前(如秒杀开始前 10 分钟),主动将热 Key 数据加载到 Redis 及本地缓存,避免 “冷启动” 时大量请求穿透到数据库。
例:通过脚本批量执行SET key value EX 3600
预加载热点商品数据。 - 过期时间错开:对同类热 Key 设置随机过期时间偏移量(如
EX 3600 + random(0, 600)
),避免大量 Key 同时过期导致 “缓存雪崩” 和集中重建压力。
- 热 Key 读写逻辑优化
- 读热点:避免缓存穿透 / 击穿
- 对热 Key 设置 “永不过期”(或超长过期时间),通过异步任务定期更新数据(保证最终一致性)。
- 缓存重建加锁:若热 Key 过期,用分布式锁(如 Redis 的
SET NX
)限制单个请求重建缓存,其他请求等待结果(避免 “缓存击穿”)。
例:GET stock:1001
失效时,只有一个请求执行SET stock:1001 ... NX PX 5000
后查库更新,其他请求重试或返回旧值。
- 写热点:异步化 + 批量更新
- 高频写入热 Key 时,先写入本地队列(如 Kafka),再通过消费端异步批量更新 Redis(减少 Redis 写次数)。
例:直播在线人数 Keyonline:room100
,用户进出时先记录到 Kafka,消费端每 1 秒聚合一次数据更新 Redis。
- 高频写入热 Key 时,先写入本地队列(如 Kafka),再通过消费端异步批量更新 Redis(减少 Redis 写次数)。
三、限流与容错:保护系统不被流量击垮
当热 Key 流量超过预期时,通过限流、降级手段强制保护 Redis 和下游服务。
解决方案 | 核心原理 | 实现方式 | 注意事项 |
---|---|---|---|
热点 Key 限流 | 限制热 Key 的每秒访问次数,超过阈值则拒绝或降级 | - Redis 层面:用 Redis-Cell 模块(CL.THROTTLE 命令)对热 Key 设限(如每秒 1 万次)。 - 应用层:用 Sentinel、Resilience4j 等工具对热 Key 相关接口限流。 | 限流阈值需结合 Redis 性能动态调整 |
服务降级 | 热 Key 访问超限时,返回默认值或降级结果,避免请求穿透到数据库 | - 例:秒杀库存查询超限时,返回 “当前拥挤,请稍后再试”,而非查询 Redis / 数据库。 - 降级逻辑需提前定义(如静态默认值、本地缓存旧值)。 | 降级策略需与业务沟通,避免影响核心流程 |
熔断机制 | 当 Redis 节点响应延迟过高时,暂时切断热 Key 请求,保护节点恢复 | - 用 Spring Cloud Circuit Breaker 等框架监控 Redis 响应时间,超过阈值(如 500ms)时触发熔断,直接返回降级结果。 | 熔断后需有自动恢复机制(如渐进式探活) |
缓存雪崩、击穿、穿透是什么?怎么解决?
一、缓存雪崩
定义:指在某一时间段内,缓存中大量 Key 集中过期失效,或缓存服务整体宕机,导致大量请求瞬间穿透到数据库,造成数据库压力骤增甚至宕机的现象。
解决方案:
- 过期时间错开:为 Key 设置随机过期时间(如
EX 3600 + random(0, 600)
),避免集中过期。 - 多级缓存:增加本地缓存(如 Caffeine)或接入层缓存(如 Nginx),即使分布式缓存失效,也能通过本地缓存挡一部分流量。
- 缓存降级 / 限流:缓存失效时,通过熔断器(如 Sentinel)限制数据库请求量,返回默认值(如 “系统繁忙”)。
- 缓存集群高可用:部署 Redis 主从 + 哨兵或 Cluster 集群,避免单点故障导致缓存整体不可用。
- 缓存预热:在流量高峰前(如秒杀开始前),主动加载热点数据到缓存,避免 “冷启动” 时的缓存缺失。
二、缓存击穿
定义:指一个热点 Key(被高频访问)在缓存中过期的瞬间,大量请求同时访问该 Key,导致所有请求穿透到数据库,造成数据库瞬间压力过大的现象。
解决方案:
- 热点 Key 永不过期:对热点 Key 不设置过期时间,通过异步任务定期更新缓存(保证最终一致性)。
- 互斥锁:缓存失效时,只有一个请求通过分布式锁(如 Redis 的
SET NX
)获得权限查询数据库并重建缓存,其他请求等待或返回旧值。
例:GET key
失效时,先执行SET lock:key 1 NX PX 5000
,成功则查库更新缓存,失败则重试或返回默认值。 - 热点数据提前预热:通过监控识别热点 Key,提前将其加载到缓存并延长过期时间。
三、缓存穿透
定义:指请求查询的数据在缓存和数据库中都不存在(如查询一个不存在的用户 ID),导致请求每次都穿透到数据库,长期积累可能压垮数据库。
解决方案:
- 缓存空值:对不存在的数据,在缓存中存储空值(如
SET key "" EX 60
),避免相同请求重复穿透到数据库(注意设置较短过期时间,防止缓存空间浪费)。 - 布隆过滤器:在缓存前增加布隆过滤器,预先存储所有存在的 Key(如数据库中的用户 ID 集合),请求先经过过滤器,不存在的 Key 直接拦截,不进入缓存和数据库。
- 接口层校验:在 API 网关或应用层对请求参数进行校验(如校验 ID 格式、范围),直接拦截非法请求(如负数 ID、超长字符串)。
如何保证 redis 和 mysql 数据缓存一致性问题?
- Cache Aside Pattern(旁路缓存模式,最常用)
核心逻辑:读写操作绕过缓存直接操作数据库,缓存仅作为 “读加速” 角色,通过 “更新数据库后删除缓存” 保证一致性。
- 读流程:
- 先查 Redis,命中则返回;
- 未命中则查 MySQL,将结果写入 Redis 后返回。
- 写流程:
- 先更新 MySQL 数据库;
- 再删除 Redis 中的对应缓存(而非直接更新缓存)。
优势:实现简单,适合读多写少场景,避免 “更新缓存但数据库更新失败” 导致的不一致。
潜在问题:
- 若步骤 2 删缓存失败,会导致缓存存旧值、数据库存新值;
- 并发读写时可能出现 “旧值覆盖新值”(如 A 写新值到数据库→B 读旧值到缓存→A 删缓存失败)。
优化手段: - 删缓存失败时通过重试机制(如本地重试 + 消息队列异步重试)保证删除成功;
- 缓存设置短期过期时间(兜底机制),即使删除失败,过期后也会从数据库加载新值。
- Read/Write Through Pattern(读写穿透)
核心逻辑:应用不直接操作数据库,所有读写通过缓存层(Redis)代理完成,由缓存负责同步更新数据库。
- Read Through:读缓存未命中时,缓存主动查数据库并写入缓存后返回;
- Write Through:写操作时,缓存先更新自身数据,再同步更新数据库(同步写双存储)。
优势:应用无需关心数据库操作,一致性由缓存层保证,适合对一致性要求较高的场景。
潜在问题:
- 写操作需等待数据库更新完成,同步耗时增加,降低写性能;
- 缓存层逻辑复杂,需实现数据库交互、异常处理等逻辑。
- Write Back Pattern(写回 / 异步更新)
核心逻辑:写操作时先更新缓存,缓存异步批量更新数据库(如定期刷新、累计一定次数后刷新)。
- 写流程:更新 Redis 缓存→标记缓存为 “脏数据”→后台线程异步将 “脏数据” 同步到 MySQL。
优势:写性能极高(无需等待数据库),适合高并发写场景(如计数器、日志记录)。
潜在问题:
- 数据一致性最差:若缓存宕机,未同步到数据库的 “脏数据” 会丢失;
- 需复杂的缓存恢复机制(如持久化日志),否则故障后数据难以找回。
布隆过滤器原理
二、核心组件与操作流程
- 核心组件
- 二进制位数组(m 位):初始时所有位均为 0,用于标记元素是否 “可能存在”。
- k 个独立哈希函数:每个哈希函数将输入元素映射到位数组的某个索引(范围 0~m-1)。哈希函数需满足均匀分布,以减少冲突。
- 操作流程
- 插入元素(如元素 x):
- 用 k 个哈希函数分别计算 x 的哈希值,得到 k 个位数组索引:
h₁(x), h₂(x), ..., hₖ(x)
; - 将位数组中这 k 个索引对应的位都设为 1(若已为 1 则不变)。
- 用 k 个哈希函数分别计算 x 的哈希值,得到 k 个位数组索引:
- 查询元素(如元素 y):
- 用同样的 k 个哈希函数计算 y 的哈希值,得到 k 个索引:
h₁(y), h₂(y), ..., hₖ(y)
; - 检查位数组中这些索引的位:
- 若所有位均为 1:返回 “可能存在”(存在假阳性);
- 若至少有一位为 0:返回 “一定不存在”(绝对准确)。
- 用同样的 k 个哈希函数计算 y 的哈希值,得到 k 个索引:
数据库超卖如何解决?
- 原子操作控制:将库存查询与扣减封装为不可分割的原子操作(如通过数据库事务 + 行锁、悲观锁或乐观锁机制),防止中间过程被其他请求打断或篡改。
- 库存校验前置:在扣减前强制校验当前库存是否充足,且校验与扣减必须在同一原子单元内完成,避免 “读取 - 扣减” 间隙因库存变化导致的超卖。
- 并发顺序控制:通过队列、分布式锁等机制将并发请求串行化处理,确保同一商品的库存操作按顺序执行,避免多请求同时操作同一库存数据。
- 最终一致性兜底:通过库存预扣减 + 异步确认、定期对账等方式,在极端场景下修正可能出现的库存偏差,保证最终数据一致。