Redis 面试题
更新: 8/12/2025 字数: 0 字 时长: 0 分钟
Redis 中常见的数据类型有哪些?
Redis 常见的五种基本数据类型:
String(字符串)
- 大白话:最简单的,就是存文本、数字什么的。可以是一个单词,一句话,一个 JSON 字符串,或者一个整数。
- 能干啥:存用户信息(用户名、年龄)、文章内容、计数器(点赞数、访问量)。
- 特点:可以存二进制数据,最大 512MB。对整数有专门的优化,进行加减操作时很快。
Hash(哈希)
- 大白话:就像一个“字典”或者“对象”。一个键(key)对应一个哈希,这个哈希里又有很多“字段 - 值”对。
- 能干啥:存一个对象的多个属性,比如用户的
ID
对应一个Hash
,这个Hash
里有name
、age
、email
等字段。 - 特点:适合存储对象数据,可以一次性获取或修改对象的某个字段,节省内存。
List(列表)
- 大白话:就像一个“双向链表”或者“队列”。可以从两头(左边或右边)添加或删除元素。
- 能干啥:
- 消息队列:生产者往列表右边添加消息,消费者从左边取消息。
- 最新动态:记录用户最新发布的几条微博、新闻列表。
- 任务队列:先进先出的任务排队。
- 特点:顺序存储,可以实现栈、队列等功能。
Set(集合)
- 大白话:就像数学里的“集合”。里面存了一堆不重复的元素,而且这些元素是无序的。
- 能干啥:
- 好友关系:共同好友、关注的人。
- 抽奖活动:存储参与抽奖的用户 ID,自动去重。
- 标签系统:一篇文章有多个标签,一个标签对应多篇文章。
- 特点:元素唯一,支持集合运算(交集、并集、差集)。
ZSet / Sorted Set(有序集合)
- 大白话:类似于 Set,也是存不重复的元素,但每个元素都会关联一个“分数”(score),然后根据这个分数来排序。分数相同就按字典序排。
- 能干啥:
- 排行榜:游戏积分榜、微博热搜榜(分数是热度)。
- 带权重的任务队列:优先级高的任务先执行。
- 特点:元素唯一且有序,可以根据分数范围查询。
Redis 为什么这么快?
主要有以下几个关键原因:
基于内存操作:
- 大白话:就像你把所有要用的文件都提前放到电脑内存里,而不是每次都去硬盘里找。
- 解释:Redis 的所有数据都存储在内存中,而内存的读写速度比硬盘快好几个数量级。这是 Redis 高性能的根本原因。
高效的数据结构:
- 大白话:Redis 不只是简单地把数据放进内存,它还为每种数据类型设计了非常适合快速操作的“容器”。
- 解释:Redis 内部使用了如跳跃表(skiplist)、哈希表、压缩列表等经过精心优化的数据结构。这些数据结构在进行增删改查操作时,效率非常高,通常能达到 O(1) 或 O(logN) 的时间复杂度。
单线程模型(核心处理部分):
- 大白话:就像一个厨房里只有一位顶尖大厨,他做菜速度飞快,而且不用担心和别人抢炉子。
- 解释:Redis 的核心处理(处理客户端请求、读写数据)是单线程的。这意味着它省去了多线程场景下常见的上下文切换开销、锁竞争问题、死锁等复杂问题。这简化了设计,避免了同步开销,使得操作流程非常顺畅和高效。
- 误区:虽然核心是单线程,但 Redis 在处理持久化(RDB/AOF)、异步删除等后台任务时,会使用子进程或额外的线程来处理,避免阻塞主线程。
非阻塞 I/O 多路复用:
- 大白话:就像餐馆里只有一个服务员,但他非常聪明,他不是等一个客人点完菜才去服务下一个客人,而是同时留意所有客人的状态,谁叫他他就去谁那里。
- 解释:Redis 使用 I/O 多路复用技术(如 epoll/kqueue/select)来处理网络连接。它可以在一个线程中同时监听多个客户端连接的 I/O 事件。当某个连接有数据可读或可写时,Redis 才会去处理,而不是为每个连接创建一个线程去等待,从而避免了大量线程创建和上下文切换的开销。
C 语言编写:
- 大白话:用最高效的语言写的,底层优化做得好。
- 解释:Redis 使用 C 语言编写,C 语言对内存和系统资源的控制力更强,运行效率更高。
为什么 Redis 设计为单线程?6.0 版本为何引入多线程?
为什么 Redis 早期设计为单线程?
Redis 早期(直到 6.0 版本之前,核心处理部分)设计为单线程,主要基于以下几点考虑:
避免并发控制开销:
- 大白话:单线程就不用操心多个员工同时操作一个文件时,怎么防止他们互相覆盖、互相打架的问题了。
- 解释:多线程编程最大的挑战就是并发控制,需要引入锁、信号量等机制来保护共享资源,而这些机制会带来额外的开销(上下文切换、锁竞争、死锁等),并且会增加程序的复杂性。单线程模型避免了这些问题,代码更简洁,执行效率更高。
内存访问速度快:
- 大白话:Redis 最大的瓶颈不是 CPU,而是内存。
- 解释:Redis 的数据全部在内存中,内存的访问速度非常快。在这种情况下,CPU 往往不是瓶颈,I/O 操作(网络请求、持久化)才是。单线程通过 I/O 多路复用技术,已经能够很好地处理并发连接,避免了不必要的 CPU 等待。
更强的可维护性:
- 大白话:一个人干活,出错了更容易排查。
- 解释:单线程模型使得 Redis 的代码逻辑更简单,更容易理解和维护,也减少了出现复杂并发 Bug 的可能性。
CPU 瓶颈不是主要限制:
- 大白话:Redis 的速度太快了,一般情况下,CPU 还没忙完,网络和内存就已经把数据准备好了。
- 解释:对于 Redis 这种内存型数据库,它的瓶颈通常在于内存容量、网络带宽,而非 CPU 的计算能力。单线程足以应对绝大多数场景,而且可以通过部署多个 Redis 实例(Redis Cluster)来利用多核 CPU。
Redis 6.0 版本为何引入多线程?
虽然单线程有很多优点,但随着网络硬件的升级(万兆网卡普及)和 CPU 核心数的增加,网络 I/O 的处理逐渐成为了 Redis 单线程模型的瓶颈。
- 大白话:以前网速慢,一个大厨就能忙过来。现在网速太快了,数据像洪水一样涌进来,大厨一个人处理网络请求和数据操作就有点忙不过来了,尤其是在数据量特别大、网络带宽又充足的情况下。
- 解释:以前 Redis 瓶颈更多是 CPU 核数不够用或网络带宽不足。但现在,CPU 核心数普遍增多,网卡性能大幅提升,导致主线程在处理网络读写(数据包解析、发送)时,成为了新的性能瓶颈。
Redis 6.0 引入多线程的目的:
Redis 6.0 引入的多线程不是为了让数据操作(命令执行)变成多线程,而是为了分担网络 I/O 读写和解析的工作。
- 多线程处理网络 I/O:Redis 6.0 将网络数据的读写和解析任务,从主线程中剥离出来,交给多个 I/O 线程去处理。
- 主线程依然处理核心命令:主线程依然负责执行 Redis 命令(例如
SET
,GET
,LPUSH
等),这意味着数据结构的访问和操作仍然是单线程的,这继续保留了单线程模型简单、无锁竞争的优点。 - 提升吞吐量:通过将网络 I/O 任务并行化,Redis 可以在处理相同数量的客户端连接时,显著提高吞吐量,尤其是在高并发和大数据量传输的场景下。
总结:Redis 6.0 引入多线程是对单线程模型的一种优化和增强,旨在解决网络 I/O 瓶颈,而不是改变其核心命令执行的单线程特性。它在保持原有的简单高效优势的同时,更好地利用了现代多核 CPU 的性能。
Redis 中跳表的实现原理是什么?
大白话理解:
想象一下你有一本厚厚的、按字母顺序排列的电话簿。如果你要找一个人的电话,你当然可以从头开始一页一页翻(这就是普通链表)。但这太慢了!
为了加速查找,你在电话簿上加了几层目录:
- 第一层目录:最详细的目录,就是电话簿本身,按字母顺序排列所有名字。
- 第二层目录:粗略目录,每隔几页标上“A 字头”、“B 字头”等等。
- 第三层目录:更粗略的目录,可能只有“A-D”、“E-H”这样的范围。
- 第四层目录:最粗略的,比如“上册”、“下册”。
当你要找一个名字时,你不是从头翻,而是先从最粗略的目录开始,快速跳到大概的范围,然后进入下一层目录,再跳到更小的范围,直到最终在最详细的目录中找到。
Redis 跳表(SkipList)的实现原理就是这个思想:
Redis 的 ZSet (有序集合) 底层就是用跳表来实现的。
数据结构:
- 节点 (Node):跳表由多个节点组成,每个节点存储:
- 值 (value/member):ZSet 中实际存储的元素(比如排行榜上的用户名称)。
- 分数 (score):决定排序的数值(比如积分)。
- 层 (level):每个节点都有一个随机生成的层数(高度)。层数越高,这个节点在“目录”中出现的频率就越低(跳跃的距离就越大)。
- 前进指针 (forward pointer):每个节点在每一层都有一个指向下一个节点的指针。
- 头节点 (Header):一个特殊的节点,不存储实际数据,它的层数是跳表的最高层数。
- 尾节点 (Nil):一个特殊的节点,表示跳表的结束。
- 节点 (Node):跳表由多个节点组成,每个节点存储:
查找过程:
- 当查找一个元素时,从跳表的最高层开始。
- 在当前层,沿着前进指针向右遍历,如果当前节点的分数小于或等于要查找的分数,就继续向右。
- 如果当前节点的分数大于要查找的分数,或者已经到了当前层的末尾,就向下一层。
- 重复这个过程,直到到达最底层,就能找到目标元素或其插入位置。
插入过程:
- 首先通过查找过程,找到新元素在每一层应该插入的位置。
- 随机生成新节点的层数。
- 修改相关层中节点的前进指针,将新节点插入到相应的位置。
删除过程:
- 通过查找过程定位到要删除的节点。
- 更新受影响的层中节点的前进指针,跳过要删除的节点。
- 释放被删除节点的内存。
跳表的优点:
- 查找、插入、删除的平均时间复杂度都是 O(logN)。 和红黑树、B+ 树等平衡二叉树类似,但实现起来更简单。
- 维护简单:相比平衡二叉树,跳表的插入和删除操作只需要修改局部指针,不需要进行复杂的旋转或重新平衡操作。
- 空间换时间:通过增加额外的指针(层数),来减少查找时遍历的节点数量。
为什么 Redis 不用红黑树而用跳表?
虽然红黑树的性能和跳表类似,但跳表在以下方面可能更具优势:
- 实现简单:跳表的实现比红黑树更简单、更直观,Bug 更少。
- 范围查找:跳表在进行范围查找时非常高效,例如查找分数在某个范围内的所有元素,只需在最底层遍历。红黑树需要中序遍历,效率可能略低。
- 并发性能:在多线程环境下,跳表的并发性能可能更好,因为其局部性更强,加锁粒度可以更小(但 Redis 是单线程处理命令,这点不明显)。
Redis 的 hash 是什么?
大白话理解:
Redis 的 Hash 类型就像一个“哈希表”或者“字典”(在编程语言中常被称为 Map 或 Dictionary)。你可以把它想象成一个箱子,这个箱子有自己的名字(Redis key),箱子里面又分了很多小格子,每个小格子都有自己的标签(field)和里面放的东西(value)。
结构:
key
(顶层 Redis key) → Hash
(哈希表)
field1
→value1
field2
→value2
field3
→value3
- ...
能干啥?
最典型的应用是存储对象。
传统方式(String):存储一个用户对象,你可能需要存多个 String key:
user:1:name
→Alice
user:1:age
→30
user:1:email
→[email protected]
- 这样不仅占用了更多 Redis key,而且获取用户所有信息需要多次网络请求。
Redis Hash 方式:存储一个用户对象:
user:1
(Hash key)name
→Alice
age
→30
email
→[email protected]
- 一次
HGETALL user:1
就能获取用户所有信息,大大减少了网络开销。
底层实现:
Redis 的 Hash 类型底层使用了两种数据结构来优化存储,会根据存储的数据量和字段长度自动切换:
ziplist
(压缩列表):- 大白话:如果你箱子里的小格子(field-value 对)很少,而且格子里的东西也不大,Redis 会把它们紧凑地挨在一起放,非常省空间。
- 条件:当 Hash 存储的字段数量较少(
hash-max-ziplist-entries
配置,默认 512)且所有字段和值的大小都较小(hash-max-ziplist-value
配置,默认 64 字节)时,Redis 会使用ziplist
。 - 特点:内存效率极高,但在增删改时可能需要重新分配内存,效率会略低。
hashtable
(哈希表):- 大白话:如果箱子里的小格子很多,或者格子里的东西很大,Redis 就会用一个真正的哈希表(像 Java 的 HashMap),这样查找速度更快。
- 条件:当 Hash 满足不了
ziplist
的使用条件时(字段数量或字段值大小超过阈值),Redis 会将底层存储从ziplist
转换为hashtable
。 - 特点:查找效率高(O(1)),但会占用更多内存。
优点:
- 节省内存:尤其在存储大量小对象时,使用 Hash 比多个 String 更节省内存,因为每个 Hash 只需要一个顶层 Key。
- 减少网络开销:获取或修改对象的多个属性时,一次操作就能完成,减少了客户端与 Redis 服务器之间的网络往返次数。
Redis Zset 的实现原理是什么?
大白话理解:
Redis 的 ZSet(有序集合)就像一个带分数的排行榜。每个榜单项目(member)都有一个独一无二的名字,并且有一个对应的分数(score)。Redis 会根据这个分数把它们从小到大排好。分数相同的话,就按名字的字母顺序排。
结构:
key
(顶层 Redis key) → ZSet
(有序集合)
member1
(score1)member2
(score2)member3
(score3)- ... (按 score 排序)
能干啥?
最典型的应用就是各种排行榜,比如游戏积分榜、商品销售榜、微博热搜榜等等。你还可以根据分数范围查询,比如找出积分在 100 到 200 之间的玩家。
底层实现:
Redis 的 ZSet 也是根据存储的数据量和元素大小,以及 ZSet 的具体操作特点,自动切换底层的数据结构:
ziplist
(压缩列表):- 大白话:当你的排行榜项目很少,而且每个项目的名字和分数也都不长时,Redis 会把它们紧凑地放在一起,非常省内存。
- 条件:当 ZSet 存储的元素数量较少(
zset-max-ziplist-entries
配置,默认 128)且每个元素的值和分数都较小(zset-max-ziplist-value
配置,默认 64 字节)时,Redis 会使用ziplist
。 - 存储方式:在
ziplist
中,member 和 score 是紧挨着存放的,通过有序插入保证了顺序。 - 特点:内存效率高,但在元素较多时,增删改查需要移动大量数据,效率会下降。
skiplist
(跳表) +hashtable
(哈希表):- 大白话:当排行榜项目增多,或者单个项目内容变大时,Redis 会切换到更强大的组合拳:跳表用来快速查找和范围查询,哈希表用来快速根据项目名字找到它的分数。
- 条件:当 ZSet 不满足
ziplist
的条件时,Redis 会转换为skiplist
和hashtable
的组合结构。 skiplist
(跳表):负责根据score
来排序和查找。它能以 O(logN) 的时间复杂度进行插入、删除、查找和范围查找(通过分数)。hashtable
(哈希表):负责根据member
(元素名称)快速查找其对应的score
。这样,你可以根据元素名直接获取其分数,时间复杂度为 O(1)。- 特点:这种组合实现了高效的插入、删除、查找和范围查询。跳表保证了有序性,哈希表保证了根据
member
的快速访问。
总结:
Redis 巧妙地利用了多种底层数据结构,并根据数据的特性和操作的频繁程度进行自动切换,以达到在不同场景下内存占用和性能的最佳平衡。这种设计是 Redis 如此高效和灵活的关键之一。
Redis 中如何保证缓存与数据库的数据一致性?
缓存(Redis)是数据库(MySQL)的“副本”,为了保证数据不“脱节”,它们需要保持同步。
常见方案(及其优缺点):
先更新数据库,再删除缓存 (推荐)
- 步骤:修改数据库 → 删除缓存。
- 大白话:先把真实数据改了,然后告诉缓存“你那里是旧数据了,删掉吧,下次有人问我直接给你最新版”。
- 优点:这是最常用且相对安全的方案。
- 缺点:可能会有短暂的不一致(删除缓存失败时)。可以配合消息队列(MQ)来重试删除,保证最终一致性。
先删除缓存,再更新数据库 (不推荐)
- 步骤:删除缓存 → 修改数据库。
- 大白话:先告诉缓存“你那里没用了,清空”,然后才去改真实数据。
- 缺点:并发场景下,非常容易导致数据库和缓存不一致,因为在删除缓存后,数据库更新前,可能有读请求来了,把旧数据又加载到了缓存。
其他方案:
- 先更新缓存,再更新数据库:复杂,容易不一致。
- 先更新数据库,再更新缓存:复杂,更新缓存的开销大。
- 缓存双删策略(延迟双删):针对“先更新数据库,再删除缓存”的优化,删除缓存后,再延迟一小段时间(如几百毫秒)再次删除缓存,以应对极端并发场景。
- 使用 Binlog 异步更新缓存:
- 大白话:让数据库自己通知缓存。数据库的所有修改都会记录在
Binlog
(操作日志)里。我们可以弄一个程序,专门“监听”这些Binlog
的变化,一旦发现数据库有改动,就通过异步方式去更新或删除缓存。 - 优点:数据库事务和缓存更新解耦,可靠性高,能保证最终一致性。
- 缺点:实现相对复杂,需要引入额外的组件(如 Canal)。
- 大白话:让数据库自己通知缓存。数据库的所有修改都会记录在
选择原则:
如果追求实时一致性(即时可见新数据):
- 推荐:先写 MySQL(数据库),再删除 Redis(缓存)。虽然短期内可能存在短暂不一致(比如删除失败或在删除瞬间有旧数据被读取到),但配合消息队列或延迟双删,能最大限度保证数据一致。
如果追求最终一致性(数据迟早会一致):
- 推荐:使用 Binlog + 消息队列的方式。这种方案更健壮,可以重试和顺序消费,能最大程度地保证缓存与数据库的最终一致性。
总结:最常用的方法是“先更新数据库,再删除缓存”,并结合其他机制(如消息队列、延迟双删)来增强其可靠性。对于更高一致性或复杂业务,可以考虑基于 Binlog 的异步同步方案。
Redis 中的缓存击穿、缓存穿透和缓存雪崩是什么?
这三个是使用缓存时常见的“灾难”,需要我们提前预防。
缓存击穿 (Cache Breakdown)
- 大白话:某个热点数据在缓存中失效了,但此时有大量并发请求同时来访问这个数据。这些请求都会穿过缓存,直接打到数据库上,导致数据库瞬间压力过大,甚至崩溃。
- 场景:某个明星的微博数据,本来缓存着。突然缓存过期了,成千上万的用户同时访问这条微博。
- 如何解决:
- 设置热点数据永不过期:对于特别热点且不经常变化的数据,可以考虑永不过期(或设置一个非常长的过期时间)。
- 使用互斥锁 (Mutex Lock):当第一个请求发现缓存失效时,它先去获取一个分布式锁。获取到锁的请求才去数据库查询,其他请求则等待。等获取到锁的请求把数据从数据库加载到缓存后,再释放锁,其他等待的请求就可以从缓存中获取数据了。
- 大白话:就好像电梯坏了,大家一窝蜂去爬楼梯。现在大家约定,谁先到电梯口,谁去拿维修工具修电梯,其他人先在外面等着,修好了大家再排队坐电梯。
- 异步更新:不直接淘汰热点数据,而是后台线程定时刷新或在数据即将过期时异步加载新数据。
缓存穿透 (Cache Penetration)
- 大白话:大量请求去查询一个根本不存在的数据(比如,用户 ID 为 -1 的数据)。缓存和数据库里都没有这个数据。每次请求都会穿透缓存,直接打到数据库,而且因为数据不存在,缓存也不会存储结果,导致每次都穿透,数据库压力倍增。
- 场景:恶意攻击者不断用不存在的商品 ID 来查询。
- 如何解决:
- 布隆过滤器 (Bloom Filter):在缓存层和数据库之间加一个布隆过滤器。布隆过滤器可以判断某个数据“可能存在”或“一定不存在”。如果布隆过滤器判断“一定不存在”,就直接返回空,不再查询数据库。
- 大白话:就像在查询前加个“黑名单”,如果请求查的是黑名单上的数据(不存在的),直接拒绝。
- 注意:布隆过滤器有误判率,可能会把存在的误判为不存在,但可以通过增大空间来降低误判率。
- 缓存空值或默认值:如果从数据库查询的结果为空,也把这个空值或一个默认值缓存起来,并设置一个较短的过期时间。这样下次再有请求来查询这个不存在的数据时,就可以直接从缓存中返回空,避免了对数据库的压力。
- 大白话:查不到就给个“查无此人”的牌子,下次再有人问同样的人,直接把牌子给他看。
- 布隆过滤器 (Bloom Filter):在缓存层和数据库之间加一个布隆过滤器。布隆过滤器可以判断某个数据“可能存在”或“一定不存在”。如果布隆过滤器判断“一定不存在”,就直接返回空,不再查询数据库。
缓存雪崩 (Cache Avalanche)
- 大白话:大量缓存数据在同一时间集中失效(比如,缓存服务重启,或者某个时间点设置了大量缓存一起过期)。由于缓存中没有数据,所有的请求都像雪崩一样,瞬间涌向数据库,导致数据库压力激增甚至宕机。
- 场景:双十一零点,所有商品缓存都在 24 小时后同时过期。或者 Redis 服务器宕机了。
- 如何解决:
- 缓存过期时间随机化:给缓存的过期时间加上一个小的随机值,避免大量缓存同时失效。
- 大白话:大家约定好的过期时间,别都设成零点,错开个几分钟,分批过期。
- 构建多级缓存:引入多级缓存体系(如本地缓存 + Redis 缓存),当 Redis 出现问题时,请求可以先打到本地缓存。
- 熔断、降级、限流:
- 熔断:当数据库压力过大时,直接拒绝一部分请求,保护数据库。
- 降级:某些非核心业务可以暂停使用缓存,直接返回静态数据或默认数据,减轻数据库压力。
- 限流:控制进入数据库的请求 QPS(每秒查询数),超过阈值就排队或拒绝。
- 使用高可用架构:Redis 集群、主从复制 + 哨兵模式等,确保 Redis 服务本身的稳定性和可用性。
- 数据预热:在业务高峰期到来之前,提前将热点数据加载到缓存中,避免集中失效。
- 缓存过期时间随机化:给缓存的过期时间加上一个小的随机值,避免大量缓存同时失效。
Redis String 类型的底层实现是什么?(SDS)
虽然 Redis String 看起来就是简单的字符串,但它在底层并不是直接使用 C 语言的字符串(以 \0
结尾的字符数组)。Redis 为它设计了一种更聪明、更高效的结构,叫做 SDS (Simple Dynamic String),简单动态字符串。
为什么不用 C 语言字符串(char*
)?
C 语言字符串有几个缺点:
- 获取长度 O(N):C 字符串要遍历到
\0
才能知道长度。 - 不安全:字符串拼接时如果空间不够容易发生缓冲区溢出。
- 修改字符串:频繁修改(追加、截断)字符串时,需要频繁地重新分配内存,效率低。
SDS (Simple Dynamic String) 的结构:
SDS 是一个结构体,它比普通的 char*
多了一些信息:
struct sdshdr {
long len; // 已使用的字节数 (当前字符串的长度)
long alloc; // 总分配的字节数 (字符串总容量,不包括\0)
unsigned char flags; // 3 位用来表示类型
char buf[]; // 存储字符串数据,以 \0 结尾
};
SDS 相对于 C 字符串的优点:
获取长度 O(1):直接通过
len
字段就可以获取字符串长度,效率非常高。- 大白话:字符串自己知道自己有多长,不用每次都去数一遍。
杜绝缓冲区溢出:
- 大白话:在修改(比如追加)字符串时,SDS 会提前检查空间是否足够。如果不够,它会先进行扩容,然后才进行操作,避免了溢出。
- 空间预分配:在扩容时,SDS 不仅仅分配“刚好够用”的空间,而是会额外预留一些空间。比如,如果你要追加字符串,当空间不足时,如果当前字符串长度小于 1MB,会预分配和当前长度相等的额外空间;如果大于 1MB,则预分配 1MB 的空间。
- 惰性空间释放:当字符串缩短时,SDS 不会立即释放多余的空间,而是把这些空间标记为“可用的”,留着下次扩容用。
- 优点:减少了内存重新分配的次数,提高了修改操作的效率。
二进制安全:
- 大白话:C 字符串只能存文本,遇到
\0
就认为字符串结束了。但 SDS 可以存任意二进制数据(图片、视频、序列化的对象),它只通过len
字段来判断字符串的结束,而不是\0
。 - 优点:使得 Redis 可以存储各种类型的数据,不仅仅是文本。
- 大白话:C 字符串只能存文本,遇到
兼容 C 字符串函数:
- 虽然 SDS 内部有
len
字段,但buf
数组仍然以\0
结尾,这使得它可以直接作为 C 字符串传递给 C 语言标准库函数使用,增强了兼容性。
- 虽然 SDS 内部有
总结:SDS 是 Redis 字符串类型高性能和灵活性的重要基石。它通过牺牲一点点内存空间(额外存储 len
、alloc
、flags
和预留空间)来换取操作的高效和安全。
Redis 中如何实现分布式锁?
大白话理解:
分布式锁就像是你在一个大办公室里(分布式系统),有一份绝无仅有的重要文件(共享资源)。为了防止多个人同时去修改这份文件导致错误,你需要一个“门卫”(分布式锁)。谁想修改文件,就得先找门卫要“钥匙”。门卫把唯一的钥匙给一个人,其他人就只能等着。等拿到钥匙的人改完文件,把钥匙还给门卫,其他人才能继续去拿钥匙。
Redis 实现分布式锁的基本原理:
Redis 提供了一些命令,可以很巧妙地实现这个“门卫”和“钥匙”的机制。最核心的命令是 SETNX
(Set if Not eXists) 和 SET
命令的扩展。
实现步骤:
加锁 (Acquire Lock):
- 使用
SET key value NX PX milliseconds
命令。 key
:锁的名称。value
:一个随机字符串,用于标识当前请求(比如一个 UUID),这是为了在释放锁时识别是不是自己的锁。NX
:表示“只在键不存在时才设置”。如果键已经存在(说明锁已经被别人持有),则设置失败,返回nil
。PX milliseconds
:设置锁的过期时间(毫秒),这是一个非常重要的步骤,用于防止死锁。即使持有锁的客户端崩溃了,锁也能在一定时间后自动释放。- 大白话:“门卫,把‘商品库存锁’的钥匙给我,钥匙上写上我的名字和独一无二的编号。如果钥匙已经被别人拿走了,你就别理我了。另外,这把钥匙只在我手里呆 10 秒钟,10 秒后你自动收回。”
- 使用
执行业务逻辑:
- 如果
SET
命令返回成功,表示加锁成功,客户端可以执行需要保护的业务逻辑(比如扣减库存)。
- 如果
释放锁 (Release Lock):
为了防止误删(即删除了别人的锁),需要先获取锁的
value
,判断是不是自己设置的那个value
,如果是,才执行DEL key
命令删除锁。这个操作必须是原子性的! 否则可能出现:A 判断是自己的锁 → 锁过期自动释放 → B 拿到锁 → A 执行
DEL
删除了 B 的锁。解决方案:使用 Lua 脚本 来保证“判断”和“删除”的原子性。
luaif redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end
大白话:“门卫,我把‘商品库存锁’还给你。你看看钥匙上是不是我自己的编号?如果是,你再收回,不是就别动。”(这个“看是不是自己的钥匙”和“收回钥匙”的动作,必须在一个步骤里完成,不能被别人打断)
Redis 的 Red Lock 是什么?你了解吗?
大白话理解:
RedLock (Redlock Algorithm) 是 Redis 作者 Antirez 提出的一种更高级、更严格的分布式锁算法,主要是为了解决单点 Redis 分布式锁在宕机时可能出现的安全性问题。
- 单点 Redis 的问题:如果你只有一个 Redis 实例作为锁服务,当这个 Redis 实例突然宕机了,并且它的 RDB 持久化还没来得及保存锁信息,那么在它重启后,之前的所有锁都可能丢失。这样,多个客户端就可能同时获得锁,导致数据不一致。
- RedLock 的目标:即使在 Redis 节点部分宕机的情况下,也能保证锁的安全性(同一时刻只有一个客户端持有锁)。
RedLock 的核心思想:
RedLock 不依赖于单个 Redis 实例,而是依赖于 N 个独立的 Redis Master 节点(通常是奇数,如 5 个)。客户端需要从大多数(N/2 + 1 个) Redis 实例上成功获取锁,才算真正获取到锁。
RedLock 的加锁过程:
- 获取时间戳:客户端获取当前系统时间 T1(毫秒)。
- 逐个尝试加锁:客户端尝试按顺序在 N 个独立的 Redis Master 节点上获取锁。
- 使用的命令依然是
SET key value NX PX timeout
。 - 这里的
value
也是一个唯一标识符。 timeout
是锁的过期时间(例如 10 秒)。- 每次尝试加锁时,都设置一个小的超时时间,避免某个节点卡住。
- 使用的命令依然是
- 计算加锁耗时:客户端获取所有锁花费的时间 T2(毫秒)。
- 判断是否成功:
- 如果客户端成功从大多数(N/2 + 1 个)节点获取到锁。
- 并且,获取所有锁的总耗时
(T2 - T1)
小于锁的有效时间(timeout
)。 - 那么,客户端才认为自己成功获取了分布式锁。
- 失败处理:如果未能从大多数节点获取锁,或者总耗时超过了锁的有效时间,客户端会立即向所有已经获取到锁的 Redis 实例发送
DEL
命令,释放这些锁。
RedLock 的释放锁过程:
客户端向所有 N 个 Redis 实例都发送 DEL
命令,无论是否在上面成功获取了锁。这是因为,即使某个实例没有成功加锁,也可能是网络问题导致,为了确保万无一失。
Redis 实现分布式锁时可能遇到的问题有哪些?
虽然 Redis 实现分布式锁非常方便,但也伴随着一些坑,需要注意:
死锁 (Deadlock):
- 问题:客户端获取锁后,如果在业务逻辑执行过程中崩溃了,没有来得及释放锁,那么这个锁就会一直存在,导致其他客户端永远无法获取锁。
- 解决方案:
- 设置过期时间 (TTL/EX/PX):这是必须的。在加锁时就给锁设置一个过期时间,即使客户端崩溃,锁也会在一段时间后自动释放。
- 续期机制 (Redisson):如果业务逻辑执行时间不确定或可能超过锁的过期时间,可以引入一个续期线程。在锁即将过期时,续期线程会自动为锁续期,直到业务逻辑执行完毕。
锁误删 (Misdeletion):
- 问题:客户端 A 获取了锁,但由于网络延迟或业务执行时间过长,锁过期了。这时客户端 B 获取了同一个锁。随后客户端 A 的业务逻辑执行完毕,去释放锁,结果删除了客户端 B 持有的锁。
- 解决方案:
- 使用唯一标识符:在加锁时,为锁设置一个随机且唯一的
value
(比如 UUID)。在释放锁时,先检查锁的value
是否与自己设置的value
一致,只有一致才删除。 - 使用 Lua 脚本保证原子性:“检查
value
”和“删除锁”这两个操作必须是原子性的,否则依然可能出现误删。使用 Lua 脚本可以将这两个操作捆绑在一起,确保原子执行。
- 使用唯一标识符:在加锁时,为锁设置一个随机且唯一的
非原子操作导致的问题:
- 问题:如果加锁或解锁操作不是原子性的(例如,
SETNX
和EXPIRE
分开执行),在执行过程中 Redis 宕机,可能导致锁没有过期时间,变成“永生锁”,从而引发死锁。 - 解决方案:
- 使用
SET key value NX PX milliseconds
:这是 Redis 2.6.12 及以后版本提供的原子性设置锁和过期时间的命令,强烈推荐使用。 - 使用 Lua 脚本:对于更复杂的原子操作(如释放锁时的判断和删除),使用 Lua 脚本。
- 使用
- 问题:如果加锁或解锁操作不是原子性的(例如,
锁的粒度问题:
- 问题:锁的粒度过大,会导致并发度降低;锁的粒度过小,会增加锁的开销和管理的复杂性。
- 解决方案:根据业务需求,合理设计锁的
key
,精确锁定需要保护的资源。例如,扣减库存,锁的key
可以是product:stock:item_id
。
Redis 实例宕机导致锁丢失 (单点故障):
- 问题:如果你只用一个 Redis 实例做分布式锁,当这个实例突然宕机,并且它的数据还没来得及持久化,那么所有未释放的锁都会丢失。重启后,多个客户端可能会同时获取到锁,导致数据不一致。
- 解决方案:
- Redis Sentinel (哨兵模式):主从切换后,新的主节点仍然能提供服务,但主从切换期间可能出现锁丢失(因为旧主节点上的锁可能还没同步到新主节点,或者同步过去后,新主节点成为主节点时,如果旧主节点又恢复了,会出现双主)。
- Redis Cluster (集群模式):提高了可用性和扩展性,但跨槽位的分布式锁实现更复杂。
- RedLock 算法:专门设计来解决多 Redis 实例下的安全性问题,但增加了复杂性和存在一些争议。
时钟跳跃问题:
- 问题:如果 Redis 服务器的系统时间突然跳变(比如 NTP 同步导致时间回拨),可能导致锁的过期时间计算错误,提前释放或延后释放。
- 解决方案:在 RedLock 算法中会考虑这个问题,但在单点 Redis 锁中,一般依赖 NTP 同步的稳定性,或者在业务层面增加额外的校验。
总的来说:对于大多数业务场景,基于 SET key value NX PX
和 Lua 脚本的单点 Redis 分布式锁,配合过期时间、唯一标识符和续期机制(如果需要),已经能够满足需求。如果对数据一致性有极高要求,且能接受更高复杂度,可以考虑 RedLock 或其他更成熟的分布式锁框架(如 Zookeeper 实现的 Curator 锁)。
Redis 的持久化机制有哪些?
Redis 是一个内存数据库,数据都存在内存里,速度快。但内存有个缺点:服务器一关机,数据就没了。所以,为了防止数据丢失,Redis 需要把内存里的数据“备份”到硬盘上,这个过程就叫持久化。Redis 有两种主要的备份方式:
方式一:RDB (Redis Database) 快照
- 大白话:就像给你的数据拍个“快照”或“照片”。在某个时间点,把 Redis 内存里的所有数据,一下子完整地保存到一个文件里(
.rdb
文件)。 - 如何触发:
- 手动触发:
SAVE
命令:阻塞 Redis 主进程,直到保存完成。生产环境慎用,因为它会阻塞所有客户端请求。BGSAVE
命令:后台异步保存。Redis 会 fork 一个子进程去执行保存操作,主进程可以继续处理请求。这是常用的手动方式。
- 自动触发:在
redis.conf
配置中设置策略,例如:save 900 1
(900 秒内至少有 1 次改动就保存)save 300 10
(300 秒内至少有 10 次改动就保存)save 60 10000
(60 秒内至少有 10000 次改动就保存)- 当达到任一条件时,Redis 会自动执行
BGSAVE
。
- 手动触发:
- 恢复数据:启动 Redis 时,会自动加载最新的
.rdb
文件。 - 优点:
- RDB 文件紧凑,适合备份:文件小,方便传输,适合做数据备份和灾难恢复。
- 恢复速度快:直接加载
.rdb
文件,恢复数据效率高。 - 对性能影响小:
BGSAVE
是子进程操作,对主进程影响较小。
- 缺点:
- 可能丢失数据:RDB 是周期性快照,如果在两次快照之间 Redis 宕机,这期间的数据就会丢失。数据越重要,丢失的风险越高。
- Fork 子进程的开销:在数据量大时,fork 子进程会消耗一定的系统资源,并可能导致短暂的阻塞。
方式二:AOF (Append Only File) 只追加文件
- 大白话:就像写日志。你对 Redis 做的每一个“写”操作,Redis 都会把这个操作命令原封不动地记录到一个日志文件里(
appendonly.aof
)。 - 如何触发:在
redis.conf
中开启appendonly yes
。 - 写入策略:AOF 提供了三种写入磁盘的策略,通过
appendfsync
配置:always
:每个写命令都立即同步到磁盘。最安全,但性能最低。everysec
:每秒同步一次。推荐,兼顾安全性和性能。 最多丢失 1 秒数据。no
:由操作系统决定何时同步。性能最高,但最不安全。
- 恢复数据:启动 Redis 时,会重新执行 AOF 文件中的所有命令,来重建内存数据。
- AOF 重写 (Rewrite):
- 大白话:随着日志文件越来越大,会有很多重复或无效的命令(比如先
SET A 1
,再SET A 2
,再DEL A
)。AOF 重写就是把这些冗余命令压缩,生成一个新的、更小的 AOF 文件。 - 原理:Redis 会 fork 一个子进程,把当前内存中的数据,转换成一系列简化的命令写入新的 AOF 文件,完成后替换旧的 AOF 文件。
- 触发:可以手动
BGREWRITEAOF
,也可以配置自动触发(例如auto-aof-rewrite-percentage 100
和auto-aof-rewrite-min-size 64mb
)。
- 大白话:随着日志文件越来越大,会有很多重复或无效的命令(比如先
- 优点:
- 数据丢失少:根据
appendfsync
策略,可以做到最多丢失 1 秒数据(everysec
)。 - AOF 文件可读性好:都是 Redis 命令,方便调试和恢复。
- 数据丢失少:根据
- 缺点:
- AOF 文件通常比 RDB 大:记录的是命令而不是压缩后的数据。
- 恢复速度相对慢:需要重新执行所有命令,在大文件时可能耗时。
混合持久化 (Redis 4.0+)
- 大白话:结合了 RDB 和 AOF 的优点。
- 原理:在 AOF 重写时,不再是把所有内存数据转换成命令,而是先以 RDB 格式写入 AOF 文件的开头,然后再将重写期间发生的增量命令以 AOF 格式追加到 RDB 数据的后面。
- 恢复数据:Redis 启动时,先加载 AOF 文件中的 RDB 部分,然后追加执行 AOF 部分的命令。
- 优点:
- 兼顾启动速度和数据安全性:RDB 部分快速加载,AOF 部分保证数据最新。
- 文件大小适中。
- 推荐:生产环境通常会同时开启 AOF 和 RDB,并使用 AOF 的
everysec
策略,并开启混合持久化。
Redis 主从复制的实现原理是什么?
主从复制就像是“老师和学生”的关系。一台 Redis 服务器(Master,老师)负责处理所有写请求,并把它的数据变化实时同步给其他多台 Redis 服务器(Slave,学生)。学生们只负责读请求。这样做的目的是为了:
- 读写分离:老师负责写,学生负责读,分担压力。
- 高可用:万一老师累倒了(Master 宕机),可以快速从学生中选一个出来当新老师,保证服务不中断。
- 数据备份:学生们手里都有老师的完整“笔记”,相当于多了一份备份。
实现原理步骤:
建立连接:
- 当一个 Slave 启动时,或者在配置中指定了 Master 地址 (
replicaof masterip masterport
),Slave 会向 Master 发送PSYNC
命令请求同步。 - 大白话:学生刚进教室,就跟老师说:“老师,我要抄你的笔记!”
- 当一个 Slave 启动时,或者在配置中指定了 Master 地址 (
全量复制 (Full Resynchronization):
- 如果 Slave 是第一次连接 Master,或者 Master 发现 Slave 的数据版本和自己差异太大,就会进行全量复制。
- Master 过程:
- 执行
BGSAVE
命令,生成当前的 RDB 快照文件。 - 在生成 RDB 期间,Master 会把这期间的所有写命令都缓存起来。
- 将生成的 RDB 文件发送给 Slave。
- 执行
- Slave 过程:
- 接收 Master 发送的 RDB 文件。
- 清空自己原有的所有数据。
- 加载 RDB 文件到内存中。
- 接收 Master 缓存的那些增量写命令,并在内存中执行这些命令。
- 大白话:老师说:“好,你先拿我最新的完整笔记(RDB 文件)去抄,抄完后,我再把你在抄笔记期间我新讲的知识点(增量命令)告诉你。”
增量复制 (Partial Resynchronization):
- 全量复制完成后,Master 和 Slave 之间会保持一个长连接。
- Master 会将所有后续发生的写命令实时地发送给所有连接的 Slave。
- Slave 接收到命令后,立即在自己的内存中执行这些命令,保持与 Master 的数据一致。
- 复制偏移量和复制积压缓冲区:Master 和 Slave 都会维护一个复制偏移量,记录已经复制的数据量。Master 还会维护一个环形缓冲区,存储最近执行的命令。如果 Slave 短暂断开连接后又重新连接,它会告诉 Master 自己已经复制到哪个偏移量了,Master 就会从复制积压缓冲区中找到缺失的命令发送给 Slave,实现断点续传,避免不必要的全量复制。
- 大白话:抄完完整笔记后,学生和老师就保持实时同步了。老师每讲一个新知识点,就立刻写给学生,学生立马抄到自己的笔记上。即使学生中间打了个盹,醒来后也能快速补上漏掉的部分。
Master 宕机后的处理:
- 如果 Master 宕机,Redis Sentinel (哨兵) 或 Redis Cluster (集群) 可以自动进行主从切换(故障转移),从现有的 Slave 中选举一个新的 Master,保证服务的高可用。
Redis 数据过期后的删除策略是什么?
Redis 里的数据可以设置一个“保质期”(过期时间)。数据到了保质期后,就应该被删除。但 Redis 不是一到时间就立马删除,它有自己的“删除策略”,因为如果所有数据都到了时间就立即删除,会很耗费 CPU 资源。
Redis 主要采用两种策略配合使用:
策略一:惰性删除 (Lazy Deletion)
- 大白话:就像一个很懒的清洁工。他不主动去检查哪里脏了,只有当有人来问他“这个地方干净吗?”时,他才去看看,如果发现脏了,就顺手清理掉。
- 原理:
- 当客户端(你)尝试访问(
GET
、HGET
等命令)某个带有过期时间的 Key 时。 - Redis 会在返回数据之前,先检查这个 Key 是否已经过期。
- 如果过期了,Redis 会立即删除这个 Key,并返回空。
- 如果没过期,就正常返回数据。
- 当客户端(你)尝试访问(
- 优点:
- 对 CPU 最友好:只有访问时才进行删除,不占用额外的 CPU 资源去检查过期 Key。
- 缺点:
- 内存浪费:如果某个 Key 已经过期了,但一直没有被访问,它就会一直存在内存中,直到被动触发删除,造成内存浪费。
策略二:定期删除 (Active Deletion)
- 大白话:就像一个有固定工作时间的清洁工。他每隔一段时间(比如每分钟),就会去一些随机的地方转一圈,看看有没有过期的垃圾,发现了就清理掉。
- 原理:
- Redis 内部有一个定时任务,默认每 100 毫秒(每秒 10 次)运行一次。
- 每次运行,它会做以下事情:
- 从设置了过期时间的 Key 集合中,随机抽取一部分 Key 进行检查。
- 删除其中所有已经过期的 Key。
- 如果删除的 Key 数量超过了某个比例(例如 25%),会重复这个过程,直到低于这个比例,或者达到一定的 CPU 时间限制(例如 25 毫秒)。
- 优点:
- 内存回收及时:相较于惰性删除,可以更及时地回收一部分过期 Key 占用的内存。
- CPU 开销可控:每次只检查一部分 Key,并有时间限制,避免长时间占用 CPU。
- 缺点:
- 仍有内存浪费:毕竟是随机抽样,有些过期 Key 可能长时间没有被抽到,仍然会占用内存。
- 无法保证所有过期 Key 都被删除。
额外补充:内存淘汰策略 (Eviction Policy)
- 大白话:当 Redis 内存快满了,而又没有足够多的过期 Key 可供删除时,Redis 就会采取“壮士断腕”的方式,按照一定的策略,主动删除一些没有过期时间或者还没到过期时间的 Key,以腾出空间。
- 触发条件:当 Redis 的内存使用达到
maxmemory
限制,并且有新的数据写入时。 - 常见策略:
noeviction
:不删除,新写入会报错。volatile-lru
:从设置了过期时间的 Key 中,淘汰最近最少使用的。allkeys-lru
:从所有 Key 中,淘汰最近最少使用的(不管有没有设置过期时间)。volatile-ttl
:从设置了过期时间的 Key 中,淘汰即将过期的。allkeys-random
:从所有 Key 中,随机淘汰。等等...
- 作用:内存淘汰策略是 Redis 的“最后防线”,确保 Redis 在内存不足时依然可以运行,不至于崩溃。
总结:Redis 主要通过惰性删除(访问时删除)和定期删除(随机抽样删除)来管理过期 Key。这两种策略权衡了 CPU 资源和内存回收效率。当内存真正吃紧时,才会启动内存淘汰策略,按照配置好的规则删除 Key 来腾出空间。
如何解决 Redis 中的热点 key 问题?
热点 Key 就像一个“明星商品”或者“爆款新闻”,在短时间内有大量并发请求去访问它。在 Redis 中,这会导致:
- CPU 飙升:Redis 处理这个 Key 的请求量太大,CPU 使用率过高。
- 网络拥塞:针对这个 Key 的请求和响应数据量太大,占用大量网络带宽。
- 单点压力:如果你的 Redis 是集群,所有请求都集中到存储这个热点 Key 的那个节点上,导致该节点压力过大。
这些都可能导致 Redis 服务响应变慢,甚至短暂宕机。
解决热点 Key 的方案:
解决热点 Key 的核心思想就是:分散压力 和 减少直接访问。
数据预热与本地缓存 (Local Cache)
- 大白话:如果知道哪些 Key 会变成热点(比如秒杀商品、实时榜单),提前把它们放到应用服务器的内存里(本地缓存),或者专门用一个更靠近用户的缓存层。
- 实现:
- 服务启动时或定时任务将热点数据加载到本地内存(Guava Cache、Caffeine 等)。
- 请求优先从本地缓存获取,本地缓存未命中或过期才去 Redis。
- 优点:减少对 Redis 的直接访问,减轻 Redis 压力,响应速度快。
- 缺点:存在数据一致性问题(本地缓存过期策略、数据更新的通知机制)。
热点 Key 分散存储 (Hash Tag)
- 大白话:如果你的 Redis 是集群,一个 Key 只会存储在一个节点上。热点 Key 来了,这个节点就累死了。我们可以把一个热点 Key“复制”成多个,分散到不同的 Redis 节点上。
- 实现:
- 对热点 Key 进行二次散列,或在 Key 名后面加上后缀或随机数。
- 例如:原来 Key 是
product:100
,现在可以变成product:100:#1
、product:100:#2
...product:100:#N
。 - 客户端在访问时,随机选择一个后缀去访问。
- 优点:将热点请求分散到多个 Redis 节点,减轻单点压力。
- 缺点:增加了数据的冗余,写入时需要更新多个 Key,并且可能需要额外的逻辑来保证多个副本之间的一致性(例如通过异步任务或消息队列更新所有副本)。
加锁与队列限流 (针对写操作)
- 大白话:如果热点 Key 的问题主要发生在写操作上(比如热门商品的库存扣减),可以控制同一时间只有一个请求能操作,其他请求排队等待。
- 实现:
- 使用分布式锁(如 Redis 实现的分布式锁)来保证对热点 Key 的写操作的串行化。
- 使用消息队列对请求进行削峰填谷,将大量并发写请求异步化处理。
- 优点:保证数据一致性,防止超卖。
- 缺点:降低了并发度,增加了延迟。
数据分级缓存 (多级缓存)
- 大白话:把数据存在多个层级,越靠近用户的地方越快。比如 CDN → 本地缓存 → Redis → 数据库。
- 实现:结合 CDN 缓存静态资源、应用服务本地缓存热点数据、Redis 作为分布式缓存、数据库作为最终存储。
- 优点:充分利用各层优势,层层拦截请求,减轻后端压力。
- 缺点:增加了系统复杂性,数据一致性管理更复杂。
Redis 集群的实现原理是什么?
单机 Redis 性能再强,也有内存和并发的上限。Redis 集群就是把多个 Redis 实例组织起来,让它们像一个整体一样工作,共同存储海量数据,并处理巨大的并发请求。
核心目标:
- 数据分片 (Sharding):将数据分散存储到不同的 Redis 节点上,突破单机内存限制。
- 高可用 (High Availability):即使部分节点宕机,整个集群仍然能对外提供服务。
- 可扩展性 (Scalability):可以方便地增加或减少节点,来扩展或缩减集群容量。
实现原理:
Redis Cluster 采用去中心化的架构,每个节点都保存整个集群的状态信息。
数据分片:槽 (Slot)
- 大白话:Redis 集群把整个数据空间分成了 16384 个“小格子”(哈希槽)。每个 Key 在存储时,会根据它的哈希值计算出它属于哪个槽。每个 Redis 节点负责管理其中的一部分槽。
- 原理:
- Redis Cluster 有 16384 个哈希槽,编号从 0 到 16383。
- 每个 Key 都会通过 CRC16 算法计算哈希值,然后
hash_value % 16384
得到该 Key 所属的槽位。 - 每个 Master 节点负责管理一部分槽(例如,节点 A 负责 0-5000 槽,节点 B 负责 5001-10000 槽)。
- 优点:
- 数据均匀分布:理论上 Key 会比较均匀地分布在不同节点。
- 扩容/缩容方便:节点增减时,只需要在节点之间移动槽位即可,而不是移动大量实际数据。
- Hash Tag (哈希标签):如果你想让某些 Key 强制落在同一个槽中(例如,一个用户的所有数据),可以在 Key 的一部分用
{}
包裹起来,Redis 只对{}
中的内容计算哈希值。例如user:{123}:name
和user:{123}:age
都会落在同一个槽中。
客户端路由:
- 大白话:客户端第一次连接集群时,会从任意一个节点获取整个集群的槽位分布信息(哪个槽在哪个节点上)。之后,当客户端要操作某个 Key 时,它会自己计算 Key 属于哪个槽,然后直接连接到负责那个槽的节点上进行操作。
- 重定向:如果客户端请求的 Key 不在该节点上,该节点会返回一个
MOVED
或ASK
错误,告诉客户端正确的节点地址,客户端会重定向到正确的节点。
高可用:主从复制 + 故障转移
- 大白话:每个 Master 节点(负责一部分槽)都可以有自己的 Slave 节点。当 Master 宕机时,它的一个 Slave 会被选举出来接替 Master 的位置,继续提供服务。
- 原理:
- 每个 Master 节点至少有一个或多个 Slave 节点作为其备份。
- 节点之间通过Gossip 协议(流言协议)互相通信,交换集群的状态信息(比如哪些节点存活、哪些节点宕机)。
- 当某个 Master 节点宕机时,集群中的其他节点会感知到。
- 通过投票机制,从该宕机 Master 的 Slave 节点中选举出一个新的 Master 来接管其槽位。
节点通信:
- 大白话:集群中的所有节点都会互相“聊天”,交流集群健康状况、槽位信息等。
- 原理:每个节点都会定期向其他节点发送消息,包括 PING/PONG 消息,以检测节点是否存活。
集群的优点:
- 高性能:所有操作都是直接连接到目标节点,没有代理层开销。
- 高可用:部分节点故障不影响整个集群服务。
- 可扩展:方便地增加或减少节点来扩容或缩容。
集群的缺点:
- 部分复杂性:集群部署和管理相对复杂。
- 跨槽位操作限制:事务和多 Key 操作通常需要 Key 落在同一个槽位,否则无法直接执行。
Redis 中的 Big Key 问题是什么?如何解决?
Big Key (大 Key) 指的是 Redis 中存储的 Key 对应的值非常大。这个“大”可以是:
- 占用内存大:一个 String 类型的 Key 存储了 10MB 的文本。一个 Hash/List/Set/ZSet 存储了百万个元素。
- 元素数量多:一个 List 存储了千万条记录,虽然每条记录不大,但总数非常多。
Big Key 会带来什么问题?
- 网络阻塞:当你获取或删除一个 Big Key 时,Redis 需要在短时间内传输大量数据到客户端或从客户端接收大量数据,这会占用大量的网络带宽,影响其他请求。
- Redis 性能下降:
- 读写阻塞:对 Big Key 进行操作(如
GET
、DEL
、HGETALL
、LRANGE
大范围查询)时,需要消耗大量 CPU 和内存,导致 Redis 主线程阻塞,无法响应其他请求,造成“卡顿”。 - 内存碎片:Big Key 的删除和更新可能导致内存碎片,影响 Redis 性能和内存利用率。
- 读写阻塞:对 Big Key 进行操作(如
- 集群扩容困难:在 Redis Cluster 中,如果 Big Key 所在的槽需要迁移到其他节点,迁移过程会非常耗时,阻塞集群其他操作。
- AOF 重写/RDB 持久化压力:Big Key 会导致 AOF 文件或 RDB 文件变得非常大,持久化操作耗时增加。
如何发现 Big Key?
- 使用
redis-cli --bigkeys
命令:可以扫描整个 Redis 实例,找出各种数据类型中占用内存最大的 Key。redis-cli -h 127.0.0.1 -p 6379 --bigkeys
- 使用
MEMORY USAGE key
命令:针对特定 Key 查看其内存占用。 - 慢查询日志:观察 Redis 的慢查询日志,看是否有针对特定 Key 的操作耗时过长。
- 业务层面:结合业务场景,预判可能出现 Big Key 的地方。
如何解决 Big Key 问题?
解决 Big Key 的核心思想就是:“化整为零”,把大 Key 拆分成小 Key,或者分批处理大 Key。
拆分 Key:
- 大 Key 拆分成小 Key:将一个大 String 拆分成多个小 String;将一个大 Hash/List/Set/ZSet 拆分成多个小 Hash/List/Set/ZSet。
- 示例:
- 将
user_feed:userid
(List 存储用户所有动态) 拆分为user_feed:userid:page1
,user_feed:userid:page2
等多个小 List。 - 将一个大 Hash
big_object:id
拆分为多个小 Hash:big_object:id:field1
,big_object:id:field2
。
- 将
- 优点:读写操作更高效,减轻单点压力。
- 缺点:增加了 Key 的数量,管理稍微复杂。
使用合理的数据结构:
- 对于一些计数、统计场景,考虑使用 HyperLogLog 或 Bitmap,它们可以以非常小的内存占用处理大量数据。
Big Key 的删除 (非阻塞删除):
- 大白话:如果一个 Big Key 必须删除,不要直接用
DEL
命令,那样会阻塞 Redis。让 Redis 在后台慢慢删。 - 实现:使用 Redis 4.0+ 提供的异步删除命令:
UNLINK key [key ...]
:这个命令是非阻塞的,它会把 Key 的实际删除操作放到后台线程中异步执行,主线程可以立即返回并处理其他请求。FLUSHALL ASYNC
/FLUSHDB ASYNC
:同样是异步清理所有 Key。
- 优点:避免删除 Big Key 时阻塞主线程。
- 大白话:如果一个 Big Key 必须删除,不要直接用
限制集合类型的大小:
- 在业务层面或代码层面,对 Hash、List、Set、ZSet 等集合类型的大小进行强制限制,例如,一个 List 最多存储 10000 条记录,超过就分片或丢弃旧数据。
定期清理:
- 对不再需要的 Key 设置合理的过期时间,让 Redis 自动清理。
总结:发现 Big Key 是第一步,核心是将其拆小,并采用异步非阻塞的方式进行删除,以及在设计阶段就避免 Big Key 的产生。