Redis 常见八股(不定期更新)
Redis
基础八股
Redis 通常应用于哪些场景
最常见的场景是缓存了,可以减少数据库的负载,提高数据读取的速度。第二个是:分布式锁,可以确保在分布式系统中资源的安全访问,避免竞态条件。第三个是:计数器,Redis 的原子性操作非常适合用作计数器,例如:可以使用 Redis 来统计页面访问量、点赞数、评论数等。通过 INCR 命令可以轻松实现高效的计数。第四个是:Redis 支持快速的数据写入和读取,非常适合用于实时分析,比如网址点击统计,实施排行榜等。
计数器
Redis 由于其单线程执行命令的特性,实现计数器非常方便,不会有锁的竞争。像文章的点赞数量就可以使用 Redis 实现。
一些海量数据的统计,例如大网站的访问统计、日活、月活等。使用 Redis 提供的高级数据结构 HyPerLogLog。
实时系统的构建
由于 Redis 高性能的特性,其经常被用于构建实时系统,比如抽奖、秒杀等,最常见的还有排行榜的实现,其可以使用 Redis 的 Zset 数据结构,更具用户的分数、时间等参数构建一个实时的排行榜。
Redis 是单线程的吗?为什么要这么设计呢?
首先呢它的操作是基于内存的,其大多数操作的性能瓶颈主要不是 CPU 导致的。然后使用单线程模型的话,代码简洁的同时也减少了线程上下文切换带来的性能开销。最后 Redis 在单线程的情况下,使用 I/O 多路复用模型就可以提高 Redis 的 I/O 利用率了。但是在 6.0 之后呢,Redis 的执行瓶颈主要在于网络 I/O,因此引入多线程处理可以提高网络 I/O 处理的速度。
需要注意的是,Redis 在 4.0 之后就开始引入了多线程指令,6.0 之后便正式的引入了多线程的机制,这里的多线程其只是针对网络请求过程使用多线程,其对于数据读写命令依旧是单线程的
为什么 Redis 前期不使用多线程的方式而是等到 6.0 又引入呢

Redis 为什么这么快
主要是因为三个方面的原因吧。首先呢 Redis 将数据存储在内存中,提供快速的读写速度,相比于传统的磁盘数据库,内存访问速度快得多。然后呢 Redis 使用单线程事件驱动模型结合 I/O 多路复用,避免了多线程上下文切换和竞争条件,提高了并发处理效率。最后它里面的各种数据结构(如字符串、哈希、列表、集合等),这些结构经过优化,能够快速完成各种操作。
Redis 性能瓶颈时该如何处理呢
首先是想到扩容,比如增加 Redis 得配置,容纳更多的内存等。如果超过单机配置了,那么可以上 redis 主从, 通过从服务分担读取数据的压力,利用哨兵自动进行故障转移。或者是利用 redis 集群 进行数据分片,比如 Redis Cluster。如果配置方面要求比较高的话,可以增加本地内存,通过多级缓存分担压力。
Redis 中常见的数据类型有哪些
常见的数据结构主要有五种,这五种类型分别为: String(字符串)、List(列表)、Hash、Set(集合)、Zset(有序集合, 也叫 sorted set); 然后高级数据结构也有了解过 BitMap(2.2版新增),GEO(3.2版新增);
String 就是经常用到的基本的数据结构,格式一般都是"名字+:"。哈希是一个键值对集合,适合存储对象的属性方面用户进行快速检索。List 是一个有序的字符串集合,底层实现为双向链表,用于简单任务调度、消息传递等场景,通过 LPUSH 和 PROP 操作实现生产者消费者模型。Set 是无序且不重复的字符串集合,支持哈希表实现,支持快速查找和去重操作。Sorted Set 有序集合类似于集合,但每个元素都有一个分数(score),用于排序。底层使用跳表实现,支持快速的范围查询。
GEO 是 Redis 提供的一种用于存储地理位置信息的数据结构,可以存储经纬度信息并支持空间查询,例如计算距离和获取范围内的坐标。
BitMap 是一种以单位存储数据的高效方式,适合用来表示布尔值(如存在性、状态等),每个 bit 可以表示一个状态(0或1),使用空间少且操作快速。例如要统计每天用户的在线状态,可以用 BitMap 记录每个用户是否在线。

Redis String 类型的底层实现是什么
SET key value GET key EXISTS key DEL key STRLEN key SETNX key MSET key1 value1 key2 value2 MGET key1 key2 EXPIRE key SETEX key [TTL] VALUE
它的底层结构实现主要基于 SDS(Simple Dynamic String) 结构,并结合 int、embstr、raw 等不同的编码方式进行优化存储。

int 编码:用于存储可以解析为整数的字符串,内存消耗最小,适合数字值。
embstr 编码:用于存储较短的字符串,将元数据和内容存储在同一块内存中,适合读多写少的场景。
raw 编码:用于存储较长的字符串,元数据和内容分开存储,适合需要频繁操作的大字符串。
Redis 中的 hash 的底层实现是什么
# 设置 hash 中指定字段的值 HSET user:1001 name "mianshiya" # 获取 hash 中指定字段的值 HGET user:1001 name #一次设置多个字段的值 HMSET user:1001 name "mianshiya" age "18" city "shanghai" # 一次获取多个字段的值 HMGET user:1001 name age # 删除 hash 中的一个或多个字段 HDEL user:1001 age # 为 hash 中的字段加上一个整数值 HINCRBY user:1001 age 1
Hash 底层实现解析
Redis 6 及之前,Hash 的底层是压缩列表加上哈希表的数据结构(ziplist + hashtable)
Redis 7 之后,Hash 的底层是紧凑列表(Listpack),加上哈希表的数据结构(Listpack + hashtable)
ziplist 和 listpack 查找 key 的效率是类似的,时间复杂度都是 O(n), 主要的区别就在于 listpack 解决了 ziplist 的 级联更新 问题。
Redis 内有两个值分别是 hash-max-ziplist-entries 和 hash-max-ziplist-value 。当 hash 小于 这两个值的时候会使用 listpack 或者 ziplist 进行存储,当大于 这两个值的时候会使用 hashtable 进行存储。这两个值可以根据合适的情况进行适当的调整,并且在使用 hashtable 结构之后,就不会再退化成 ziplist 或 listpack 之后都是使用 hashtable 进行存储。
聊聊 Redis 中的 Hashtable ?
Hashtable 就是哈希表实现,查询时间复杂度为 O(1),效率非常快。

table:哈希表实现存储元素的结构,可以看成是哈希节点(dictEntry) 组成的数组。
size: 表示哈希表的大小
sizemask: 这个是指哈希表大小的掩码,它的值永远等于 size - 1,这个属性和哈希值一起约定了哈希节点所处的哈希表的位置,索引的值 index = hash(哈希值) & sizemask;
user: 表示已经使用的节点数量。
渐进式 rehash
负载因子 = 哈希表已保存节点的数量 / 哈希表的大小
当负载因子大于等于 5 时,说明哈希冲突非常严重了,这个时候无论有没有进行 AOF 重写或者 RDB 快照,都会强制执行 rehash 操作
负载因子大于等于 1,如果这个时候服务器 没有执行 RDB 快照或者 AOF 重写这两个持久化机制的时候,就会进行 rehash 操作
负载因子小于 0.1 的时候,就会进行缩容操作,如果服务器没有执行 RDB 快照 和 AOF 重写的时候就会进行扩容反之不会进行。
Redis Zset 的实现原理是什么
ZSet 的实现由两个核心数据结构组成:跳表(Skip List)用于存储数据的排序和快速查找,哈希表用于存储成员与其分数的映射,提供快速查找。
当 ZSet 元素数量较少时,Redis 会使用压缩列表(Zip List) 来节省内存。
元素的个数 <= zset-max-ziplist-entries(默认 128)
元素成员名和分值的长度 <= zset-max-ziplist-value(默认 64 字节)
如果任何一个条件不满足,Zset 将使用 跳表 + 哈希表 作为底层实现。

为什么 Redis Zset 用跳表实现而不是红黑树,B+ 树
为什么不用红黑树
范围查询跳表可以通过 O(logn) 的时间复杂度定位起点,然后在原始的链表中往后遍历即可,红黑树从结构上不支持范围查询。
跳表基于多层链表实现,通过概率算法动态生成索引层级,没有左旋右旋等操作,逻辑上理解更为简单。而红黑树需要复杂的平衡操作(旋转)来维护结构,代码实现复杂度高,理解门槛更高。
跳表的层数和节点结构是动态的,可以基于概率分布调整层数,灵活的适应不同的数据量(数据量大层级可以多一些,小的话层级少一点)。
为什么不用 B+ 树
B+ 树节点更新比较复杂,涉及页合并和分裂,会导致额外的计算。
B+ 树节点理论上占用内也比跳表节点大,因为控制层级的情况下,大部分跳表节点仅需要维护自己的值和一个指针(可能还有一个回退指针,redis 的实现有回退指针),而 B+ 树是多叉树,一个节点需要多指针,且节点内部还有若干指针。每个元素在叶子节点有一份完整的数据,在非叶子节点还需要存储键的数据,所以内存开销相比跳表大。
B+ 树更适合磁盘存储,特别是大规模存储数据。因为 B+ 树完整数据都存储在叶子节点中,而非叶子节点只起到索引作用,这样内存中就能存放更多的索引,便于海量数据的快速检索。
跳表的实现原理是什么
一句话概括:就是一个多层索引的链表,每一层索引的元素在最底层的链表中可以找到的元素。红色的为跳表的回退指针。


在定位到对应节点之后,具体是在当前节点创建数据还是增加一个层级这个是随机的。
typedef struct zskiplistNode {// Zset 对象的元素值sds ele;// 元素的权重值double score;// 后退指针struct askiplistNode *backward;// 节点的 level 数组,保存每层上的向前指针和跨度struct zskiplistLevel1 {struct zskiplistNode *forward;unsigned long span;} level[];
} zskiplistNode;ele: 这里用到了 Redis 字符串底层的一个实现 sds,其主要作用是用来存储数据。
score: 节点的分数,double 即浮点数型数据。
backward:我们可以看到其是 zskiplistNode 结构体指针类型,即代表指向前一个跳表节点。
level: 这个就是 zskiplistNode 的结构体数组了,数组的索引代表层级索引,这里注意与 hashtable 中的结构进行区分,那个使用的是联合体,一个是 forward,其代表下一个跳转节点,注意一个点,其实跳转到同一层 span 主要作用代表距离下一个节点的步数。
为什么 Redis 跳表实现多了个回退指针(前驱指针)
回退指针主要是为了提高跳表的操作效率和灵活性 ,尤其是在频繁插入和删除的场景中,回退指针减少了节点之间指针的更新复杂度,提升性能。
例如删除节点时,通过前驱指针可以在一次遍历中找到并记录所有关联的前驱节点,无需在变更指针时再次查找前驱节点,这种设计避免了重复查找过程,简化了操作逻辑,大幅提高了删除的执行效率。
优点是:简单实现,内存高效,并且由于数据紧凑存储,可以更快地进行读取和写入操作。
为什么引入了 ListPack
之所以设计它来替换 ziplist 就是因为 ziplist 连锁更新的问题,因为 ziplist 的每个 entry 会记录之前的 entry 长度。如果前面的 entry 长度变大,那么当前 entry 记录前面 entry 的字段所需要的空间也需要扩大,而当前面的变大了,可能后面的 entry 也得变大,这就是所谓的连锁更新,比较影响性能。
ListPack 的每个元素,仅记录自己的长度,这样一来修改会新增不会影响后面的长度变大,也避免了连锁更新的问题。


head: 整个 listpack 的元素据,包括总长度和总元素个数。
elements:实际存储的元素,每个元素包括长度和数据部分。
end: 标识 listpack 结束的特殊字节。
encoding-type:元素的编码类型。
element-data:实际存放的数据。
element-tot-len:encoding-type + element-data 的总长度,不包含自己的长度。
Redis 中的 Ziplist 和 Quicklist 数据结构的特点是什么
Ziplist: 简单、紧凑、连续存储,适用于小数据量场景例如短列表和小哈希表等,它的内存紧凑,但对大数据量或频繁的修改操作不太友好。
Quicklist: 通过将链表和 Ziplist 结合,即实现了链表的灵活操作又能节省内存。它是为了替代纯链表而设计的,适用于需要频繁对列表进行插入、删除、查找等操作的场景。
Ziplist: 在 List 和 Hash 中,大小少于一定阈值(默认是 512)且每个元素都小于 64 字节时,会使用 Ziplist

zlbytes: 记录整个 ziplist 所占用的字节数
zltail: 记录 ziplist 中最后一个节点距离 ziplist 起始地址的偏移量
zllen: 记录 ziplist 中节点的个数
entry: 各个节点的数据
zlend: 特殊值 0xFF,用于标记 ziplist 的结束

可能会导致连锁更新的情况,影响性能。由于元素是按顺序存储的,顺序访问性能比较好,但随机访问性能较差。
Quicklist
它结合了 Ziplist 和双端链表的优点,每个 Quicklist 节点都是一个 Ziplist,它限制了单个 Ziplist 的大小降低级联更新产生的影响。

特点:结合 Ziplist 的内存紧凑性和双端链表的快速插入、删除操作。能降低级联更新产生的影响。
Redis 的 ListPack 数据结构是什么
它是 Redis 内部的一种数据结构,用于高效存储短小的字符串或者整数集合,是一种紧凑型的序列化数据结构,直接以字节序列的形式存储数据,目的是减少内存占用和提升性能。
Redis 的 Lua 脚本功能是什么,该如何使用呢
Lua 脚本的所有命令在执行过程中是原子的,避免了并发修改带来的问题同时通过在服务端执行脚本,减少了客户端和服务器之间的网络往返次数,提高了相关的性能,并且可以对 Redis 进行复杂操作。
lua 本身是不具备原子性的,但是由于 Redi 的命令是单线程执行的,它会把整个 lua 脚本作为一个命令执行,会阻塞其间接受到的其他命令,这就保证了 lua 脚本的原子性。并且多条命令,一条执行失败,前面的命令也不会进行回滚。
Redis 中的缓冲击穿、缓冲穿透和缓冲雪崩是什么
雪崩:多个缓存数据在同一时间过期,导致大量请求同时访问数据库,从而造成数据库瞬间负载激增。解决办法是:采用随机过期时间策略,过滤掉不存在的请求,避免直接访问数据库。或者使用双缓冲策略,将数据同时存储在两层缓冲中,减少数据库直接请求。或者使用缓存预热让系统启动时提前加载缓存数据。
穿透:查询一个不存在的数据,每次请求都会去数据库查询,造成数据库负担加重。解决办法有两个:使用布隆过滤器过滤掉不存在的请求或者对查询结果进行缓存,即使是不存在的数据,也可以缓存一个标识。
击穿:某个热点数据在缓存中失效,导致大量请求直接访问数据库,可能会因为瞬间的高并发导致数据库崩溃。解决办法是:热点数据永远不过期,或是使用互斥锁,确保同一时期只有一个请求可以去数据库查询并且更新缓存。
互斥锁实现示例(Java)
public class CacheService {private Jedis jedis = new Jedis("localhost");private Lock lock = new ReentrantLock();public String getData(String key) {// 尝试从缓存获取数据String value = jedis.get(key);// 如果缓存不存在if (value == null) {// 加锁以防止并发请求lock.lock();try {// 再次检查缓存,避免重复查询value = jedis.get(key);if (value == null) {// 查询数据库value = queryDatabase(key);// 将结果放入缓存jedis.set(key, value);}} finally {// 释放锁lock.unlock();}}return value;}
}首先尝试从 Redis 中获取数据
如果缓存中没有数据,使用
ReentrantLock加锁,确保只有一个线程可以查询数据库。在加锁后再次检查缓存,避免重复查询。
数据库查询:如果缓存仍然没有数据,查询数据库并将结果放入缓存。
确保锁再查询结果后被释放,以防止死锁。
Redis 的持久化机制有哪些,它们的优缺点和应用场景是什么
它的持久化机制主要分为 RDB 和 AOF 两种。
RDB 持久化机制
RDB 持久化机制是指将 Redis 在内存中的数据以快照的形式写入磁盘中,可以手动或自动执行快照操作,将数据集的状态保存到一个 RDB 文件中。
它的优点是:适合在数据集比较大时进行备份操作,因为它可以生成一个非常紧凑、经过压缩的数据文件,对于 备份、恢复、迁移数据 都很方便。缺点就是: RDB 机制可能会出现数据丢失,因为数据是周期性地进行备份,一旦 Redis 出现问题并且上一次备份之后还没有进行过数据变更,那么这部分数据将会丢失。还有就是 RDB 机制会造成一定的 IO 压力,当数据集比较大的时候,进行备份操作可能会阻塞 Redis 服务器进程。
AOF 持久化机制
AOF 持久化机制是指将 Redis 在内存中的操作命令以追加的形式写入到磁盘中的 AOF 文件,只要在 Redis 重启后重新执行 AOF 文件中的操作命令即可将数据恢复到内存中。
它的优点在于:AOF 机制比 RDB 机制更加可靠,因为 AOF 文件记录了 Redis 执行的所有操作命令,可以确保数据不丢失并且 AOF 机制在恢复大数据集时更加稳健。它的缺点就是:生成的AOF文件比 RDB 文件更大,当数据集比较大时,AOF 文件会比 RDB 文件占用更多的磁盘空间。AOF 机制对于数据恢复的时间比 RDB 机制更加耗时,因为要重新执行 AOF 文件中的所有操作命令。
RDB 适用于数据集合大、备份、恢复数据和迁移数据 等操作,AOF 适用于 可靠性高、数据恢复稳健 等场景。
Redis 在生成 RDB 文件时如何处理请求呢
在 Redis 生成 RDB 文件时是异步的(使用 bgsave) 命令,采用了 fork 子进程的方式来进行快照操作,生成 RDB 文件的过程由子进程执行,主进程继续处理客户端请求,所以可以保证 Redis 在生成快照的过程中依然对外提供服务,不会影响正常请求。
生成 RDB 文件的时候,数据可以修改吗
可以修改。流程如下
当主进程 fork 出一个子进程后,主进程和子进程共享相同的内存页面。当主进程收到写命令时,主进程会将对应数据所在的页复制一份,对复制的副本进行修改。此时子进程指向的还是老的页,因此数据没有变化,符合快照的概念,并且在写的时候才触发内存复制,可以提高性能。

避免高峰期生成 RDB
因为写时复制的原因,如果共享的每一页数据都被修改,那么最大内存可以膨胀两倍,需要防止内存过载。
RDB 会产生大量的磁盘 I/O,要注意磁盘性能导致的影响,还需要注意 CPU 负载,毕竟有大量的数据需要写入。
Redis 支持事务码?是如何实现的
Redis 支持事务,但它的事务与 MySQL 中的事务不同,它主要保证的是多个命令行的原子性,并且不支持回滚。
开始事务:使用
mulit命令开始一个事务,之后的所有命令都会被派对。添加命令:在事务中添加命令,这些命令不会立即执行,而是存储在队列中。
执行事务:使用
EXEC命令执行队列中的所有命令,确保原子性。取消事务:使用
DISCARD命令可以放弃事务,清空命令队列。监视键:使用
watch命令可以监视一个或多个键值,如果在事务执行前这些键被修改,则EXEC将不会执行,确保数据一致性。
从 Redis 2.6.5 开始,服务器会在累计命令的过程中检测到错误,支持会拒绝执行 EXEC 事务并且返回一个错误,同时丢弃该事务。如果事务执行过程中发生了错误,Redis 会继续执行剩下的命令,而不会对事务进行回滚,且 Redis 是单线程的,只能是串行隔离级别。
只是一个噱头,不是我们平时理解的事务
Redis 快速实现布隆过滤器
布隆过滤器是一种高效的概率数据结构,常用于检测一个元素是否在一个集合中,可以有效减少数据库的查询次数,解决缓存穿透等问题并且它的判断可以失误。
可以通过 位图(Bitmap) 或使用 Redis 模块 RedisBloom。
使用 RedisBloom 模块
Redis 提供了一个官方模式 RedisBloom,封装了哈希函数、位图大小等操作,可以直接用于创建和管理布隆过滤器。
使用 BF.ADD 来向布隆过滤器添加元素,使用 BF.EXISTS 来检查某个元素是否可能存在。
BF.RESERVE myBloomFilter 0.01 1000000 BF.ADD myBloomFilter "item1" BF.EXISTS myBloomFilter "item1" # 返回 1(可能存在) BF.EXISTS myBloomFilter "item2" # 返回 0(一定不存在)
RedisBloom 可以自动调整底层数据结构大小以适应不断增加的数据量,用户可以指定误判率。
布隆过滤器的原理
它是由一个 位数组 和 k个独立的哈希函数 组成。 添加元素时,通过 k 个哈希函数将元素映射到位数组的 k 个位置上,将这些位置设置为 1。检查元素是否存在时,同样计算 k 个位置,如果所有位置都是 1,则说明元素可能存在,只要有一个位置为 0,就可以确定元素不一定存在。

布隆过滤器优缺点
优点是,插入和查询操作都非常高效且相比于直接存储所有元素,布隆过滤器大幅度减少了内存使用。可扩展性高:可以根据需要调整位数组的大小和哈希函数的数量来平衡时间和空间效率。
缺点是:需要多个哈希函数:选择合适的哈希函数并保证它们独立性并不容易。一旦插入元素不能删除。有一定的误判率会误认为不存在的元素在集合中。
使用 Redis 位图(bitmap) 实现布隆过滤器
import redis.clients.jedis.Jedis;
import java.nio.charset.StandardCharsets;
import java.util.BitSet;
import java.util.List;
import java.util.ArrayList;
public class RedisBloomFilter {
private static final String BLOOM_FILTER_KEY = "bloom_filter";private static final int BITMAP_SIZE = 1000000; // 位图大小private static final int[] HASH_SEEDS = {3, 5, 7, 11, 13, 17}; // 多个哈希函数的种子
private Jedis jedis;private List<SimpleHash> hashFunctions;
public RedisBloomFilter() {this.jedis = new Jedis("localhost", 6379);this.hashFunctions = new ArrayList<>();for (int seed : HASH_SEEDS) {hashFunctions.add(new SimpleHash(BITMAP_SIZE, seed));}}
// 添加元素到布隆过滤器public void add(String value) {for (SimpleHash hashFunction : hashFunctions) {jedis.setbit(BLOOM_FILTER_KEY, hashFunction.hash(value), true);}}
// 检查元素是否可能存在于布隆过滤器中public boolean mightContain(String value) {for (SimpleHash hashFunction : hashFunctions) {if (!jedis.getbit(BLOOM_FILTER_KEY, hashFunction.hash(value))) {return false;}}return true;}
// 关闭连接public void close() {jedis.close();}
// 简单哈希函数public static class SimpleHash {private int cap;private int seed;
public SimpleHash(int cap, int seed) {this.cap = cap;this.seed = seed;}
public int hash(String value) {int result = 0;byte[] bytes = value.getBytes(StandardCharsets.UTF_8);for (byte b : bytes) {result = seed * result + b;}return (cap - 1) & result;}}
public static void main(String[] args) {RedisBloomFilter bloomFilter = new RedisBloomFilter();
// 添加元素到布隆过滤器bloomFilter.add("user1");bloomFilter.add("user2");bloomFilter.add("user3");
// 检查元素是否可能存在System.out.println("Does user1 exist? " + bloomFilter.mightContain("user1")); // 输出: trueSystem.out.println("Does user4 exist? " + bloomFilter.mightContain("user4")); // 输出: false
// 关闭连接bloomFilter.close();}
}使用位图实现布隆过滤器
使用 Redis 的位图结构
SETBIT和GETBIT操作来实现布隆过滤器,位图本质上是一个比特数组,用于标识元素是否存在。对于给定的数据,通过多个 哈希函数 计算位置索引,将位图中的相应位置设置为 1,表示该元素可能存在。
布隆过滤器适用场景
推荐系统:用于判断用户是否已经看过某个推荐内容,避免重复推荐。分布式系统:用于判断数据是否在某个节点上,减少网络请求,提高性能。黑名单:例如反垃圾邮件,用于判断一个邮件地址是否在黑名单中,提高垃圾邮件过滤的效率(可能会误杀)。
Redis 集群的实现原理是什么
Redis 集群(Redis cluster) 是通过多个 Redis 实例组成的,每个实例存储部分的数据(即每个实例之间的数据是不重复的)。具体是采用 Hash Slot 来分配数据,将整个键空间划分为 16384 个槽(slots),每个 Redis 实例负责一定范围的哈希槽,数据的 key 经过哈希函数计算后对 16384 取余即可定位到对应节点。
客户端在发送请求时,会通过集群的任意节点进行连接,如果该节点存储了对应的数据则直接返回,反之该节点会根据请求的键值计算哈希槽并路由到正确的节点。
Redis 集群分片原理图示
Redis 集群会将数据分散到 16384 (2^14) 个哈希槽中,集群中的每个节点负责一定范围的哈希槽,在 Redis 集群中,使用 CRC16 哈希算法计算键的哈希槽。


还有一点需要注意的是:redis 客户端可以访问集群中任意一台实例,正常情况下这个实例包含这个数据,但是如果槽被转移了,客户端还未来的及更新槽的信息,当前实例没有这个数据,则返回 MOVED 响应给客户端,将其重新定向对应的实例(因 Gossip 集群内每个节点都会保持集群的完整拓扑信息)
为什么 Redis 哈希槽节点的数目是 16384 呢
正常的心跳包需要带上节点完整配置数据,心跳还是比较频繁的,所以需要考虑数据包的大小,如果使用 16384 数据包只要 2k,如果用了 65535 则需要 8k,那这个 ping 的消息头就太大了,浪费带宽。

[
集群规模的考虑:集群不太可能会扩展超过 1000 个节点,16384 够用且使得每个分片下的槽又不会太少。
Redis 集群中节点之间的信息如何同步?
Redis 集群内每个节点都会保存集群的完整拓扑信息,包括每个节点的 ID、IP 地址、端口、负责的哈希槽范围等。
节点之间使用 Gossip 协议进行状态转换,以保持集群的一致性和故障检测。每个节点会周期性地发送 PING 和 PONG 消息,交换集群消息,使得集群信息得以同步。
Gossip 的优点:快速收敛:Gossip 协议能够快速传播信息,确保集群状态的迅速更新。降低网络负担:由于信息是以随机节点间的对话方式传播,避免了集中式的状态查询,从而降低了网络流量。
Gossip 协议
分布式信息传播:每个节点定期向其他节点发送其状态信息,确保所有节点对集群的状态有一致的视图。低延迟和高效率:Gossip 协议设计为轻量级的通信方式,能够快速传播信息,减少单点故障带来的风险。去中心化:没有中心节点,所有节点平等地参与信息传播,提高了系统地鲁棒性。

Redis 集群中存储 key 示例和请求 key 示例(客户端直接连接地并不是对应 key 的节点)
假设我们有一个 Redis 集群,包含三个主节点(Node1、Node2、Node3),它们分别负责以下哈希槽:
Node1: 哈希槽 0-5460
Node2: 哈希槽 5461-10922
node3: 哈希槽 10923-16383
现在要存储一个键为 user:1001 的数据。先使用 CRC16 哈希算法计算 user:1001 的 CRC16 值,然后假设计算结果为 12345,然后计算该值对应的哈希槽,最后确定目标节点对目标进行存储。
客户端先使用 CRC16 算法计算 user:1001 的哈希值,因为客户端连接的是集群中的 node1,所以客户端发送查询命令 GET user:1001 到 Node1,Node1 检测到请求的键 user:1001 属于 Node3,返回一个 MOVED 错误,指示客户端请求的键在另一个节点上。MOVED 错误会中返回目标节点的信息(如 IP 端口)。最后客户端根据返回的目标节点信息,建立 Node 的连接。然后客户端再次发送查询请求,最后在 Node3 获取结果。
当客户端收到
MOVED响应时,表示 key 所在的哈希槽已经被移动到另一个节点,客户端需要更新哈希槽并且重试操作。当客户端收到
ASK响应时,表明 Redis 集群进行伸缩(扩容/缩容)
ASK 重定向的工作原理
客户端发送一个命令来访问某个 key,如果该 key 所在的哈希槽正在从源节点迁移到目标节点,源节点会返回一个 ASK 重定向指令,客户端收到 ASK 重定向后,首先发送一个 ASKING 命令到目标节点,随后重新发送原始命令到目标节点。
ASKING 命令其实是一个临时授权,告诉目标节点即使该节点还没有正式拥有该哈希槽,也要暂时处理这个请求。如果没有先发送 ASKING 命令,目标节点可能会因为还没有正式接管哈希槽而拒绝处理请求。
哈希标签(Hash Tag) 机制
为了让多个键可以存储在同一个槽位中,Redis 提供了哈希标签功能。
如果键包含 {},则只对括号内的部分进行哈希计算,通过这种方式,可以确保具有相同标签的键都落在同一个节点上,从而简化分布式场景下的操作,例如执行多键事务或管道操作。
Redis 主从复制的实现原理是什么呢
先说一下它的复制流程吧。
首先呢从节点通过向主节点发送 PSYNC 命令发起同步,如果是第一次连接或之前的连接失败,从节点会请求全量复制,主节点将当前数据快照(RDB文件)发送给从节点,全量复制完毕后,主从之间会保持一个长连接,主节点会通过这个连接将后续的写操作传递给从节点执行,来保证数据的一致。
主从架构

整个主从集群仅仅主节点可以写入,其他从节点都通过复制来同步数据,这样就能保证数据的一致性。并且对读请求分散到多个节点,提高了 Redis 的吞吐量,从一定程度上也提高了 Redis 的可用性。
主从复制原理详解
Redis 之间主从复制主要有两种数据同步方式,分别是全量同步和增量同步。
全量同步
runid 指的是主服务器的 run ID,从节点第一次同步不知道主节点 ID,于是传递 "?"
offset 为复制进度,第一次同步值为 -1

从节点发送 psync ? -1 触发同步,主节点收到从节点的 psync 命令之后,发现 runid 没有值,判断是全量同步,返回 fullresync 并带上主服务器的 runid 和当前复制进度,从服务器会存储这两个值。主节点执行 bfsave 生成 RDB 文件,在 RDB 文件生成过程中,主节点新接收到的写入数据的命令会存储到 replication buffer 中。RDB 文件生成完毕后,主节点将其发送给从节点,从节点清空旧数据,加载 RDB 的数据。等到从节点中 RDB 文件加载完成之后,主节点将 replication buffer 缓存的数据发送给从节点,从节点执行命令,保证数据的一致性。
增量同步
repl_backlog_buffer 是一个环形缓冲区,默认大小为 1m,主节点会将写入命令存到这个缓冲区中,但是大小有限,当待写入的命令超过 1m 后,会覆盖之前的数据,会环形写入。
增量同步也是 psync 命令,如果主节点判断从节点传递的 runid 和主节点一致,且根据 offset 判断数据还在 repl_backlog_buffer中,则说明可以进行增量同步。于是去 repl_backlog_buffer 查找对应 offset 之后的命令数据,写入到 replication buffer 中,最终将其发送给 slave 节点,slave 节点收到指令之后执行对应的命令,一次增量同步的过程就完成了。

如果根据 offset 判断数已经被覆盖了,此时只能触发全量同步!因此可以调整 repl_backlog_buffer 大小,尽量避免出现全量同步。
replication buffer 和 repl_backlog_buffer 的区别
repl_backlog_buffer 在主节点上只有一个,存储最近的命令,用于从服务器重新连接时进行部分重同步。
不同从节点同步速度不一样,主节点会为每个从节点都创建一个 replication buffer,它用于实时传输写命令,且大小是动态的,因为对于同步速度较慢的从服务器,需要更多的内存来缓存数据。
client-output-buffer-limit slave 256mb 64mb 60 。如果从服务器的输出缓冲区大小超过 256 MB或者超过 64MB 的时间达到 60s,Redis 将断开与从服务器的连接。
Redis 主从复制的常见拓扑结构有哪些
一主多从
这是最基本的拓扑结构,包含一个节点和多个从节点。所有写操作都在主节点上执行,而读操作可以在从节点上进行,以提高读取速度和负载均衡。
树状结构(级联)
从节点也可以作为其他从节点的主节点,这样形成了一个层次结构,主节点负责写操作而从节点负责读操作,并将数据再次复制到更下一级的从节点。
Redis 的哨兵机制是什么
是一种高可用性解决方案,用于监控 Redis 主从集群,自动完成主从切换,以实现故障自动恢复和通知。
主要功能有:哨兵不断监控 Redis 主节点和从节点的运行状态,定期发送 PING 请求检查节点是否正常。哨兵可以向系统管理员或其他服务发送通知,以便快速处理 Redis 实例的状态变化。当主节点发生故障时,哨兵会选举一个从节点提升为新的主节点,并通知客户端更新主节点的地址,从而实现高可用。
哨兵机制的由来
如果采用读写分离的模式,假设这个时候主节点宕机了,没有新的主节点顶替上来的话,就会出现很长一段时间写请求没响应的情况。
针对这个情况,便出现了哨兵这个机制,它主要进行监控作用,如果主节点挂了,将从节点切换成主节点,从而最大限度地减少停机时间和数据丢失。

主观下线和客观下线
Sentinel 每隔 1s 会发送 ping 命令给所有节点,如果 Sentinel 超过一段时间(可通过 down-after-milliseconds 还未收到对应节点地 pong 回复,就会认为这个节点主观下线。
只有主节点才有客观下线,从节点没有。如果因为网络抖动导致了一台哨兵的误判,这个时候哨兵需要问问它的队友,来确定这个主节点是不是真的出了问题。因此它会向其他哨兵发起投票,其他哨兵会判断主节点的状态进行投票。如果认为下线的总票数大于 quorum(一般为集群总数/2 + 1,假设哨兵集群有 3 台实例,那么 3/2 + 1 = 2),则能判定主节点客观下线,此时需要进行主从切换。只有哨兵的 leader 才能操作主从切换。
Redis Cluster 和 Sentinel 模式的区别是什么
Redis Cluster 是 Redis 集群,提供自动分片功能,将数据自动分布在多个节点上,支持自动故障转移。如果一个节点失败,集群会自动重新配置和平衡,不需要外部介入,因为它内置了哨兵逻辑。
Sentinel 是哨兵,主要用于管理多个 Redis 服务器实例来提高数据的高可用性。当主节点宕机,哨兵会将从节点提升为主节点,它并不提供数据分片功能。
如果需要处理大量数据并进行数据分片,应选择 Redis Cluster,它支持水平扩展,适用于大规模数据、高吞吐量场景。
如果只是为了提高 Redis 实例的可用性,并不需要数据分片,应选择主从 + Sentinel,它主要关注故障转移和实例高可用,适用于高可用性、读写分离场景。
Sentinel leader 是如何选举出来的
判断主节点主观下线的 sentinel 就是候选者,此时它像成为 leader,如果同时有两个 sentinel 判断主观下线,那么它们都是候选人,一起竞争为 leader。
候选者会先投自己一票,然后向其他 sentinel 发送命令让它们给自己投票。每个哨兵手里只有一票,投了一个之后就不能投别人了。
最后,如果某个候选者拿到哨兵集群半数及以上的赞成票,就会成为 leader,这里有一个注意的点,为了保证 sentinel 选举的时候尽量避免出现平票的情况,sentinel 的节点个数一般都会是奇数。比如 3,5,7 这样。
Redis 主节点选举
选出哨兵 leader 之后,需要选出 Redis 主从集群中的新 master 节点。把一些已经下线的节点全部剔除,然后从正常的从节点中选择主节点。
首先根据从节点的优先级进行选择,优先选择优先级比较小的节点(值越小,优先级越高,可以通过 slave-priority 配置)。如果节点的优先级相同,则查看进行主从复制的 offset 的值,即复制的偏移量,偏移量越大则表示其同步的数据越多,优先级越高。如果 offset 也相同了,那只能比较 ID 号,选择 DI 号比较小的那个作为主节点。
选好主节点之后,哨兵 leader 会让其他节点全部成为新 master 节点的 slave 节点。最后利用 redis 的 发布/订阅机制,把新主节点的 IP 和端口信息推送给客户端,此时主从切换就结束了。如果旧的主节点上线了,哨兵集群会向它发送 slaveof 命令,让它成为新主节点的从节点。
Redis 集群会出现脑裂问题吗?
会出现脑裂问题,特别是网络分区 的情况下,可能会导致同一集群内出现多个主节点,导致数据不一致。
什么是脑裂
脑裂是指在分布式系统中,由于网络分区或其他问题导致系统中的多个节点(特别是主节点)误认为自己是唯一的主节点,这种情况会导致多个主节点同时提供写入服务,从而引起数据不一致问题。
Redis 中如何避免脑裂问题的发生呢
min-slaves-to-write:设置主节点在至少有指定数量的从节点确认写操作的情况下才执行写操作min-saves-max-lag:设置从节点的最大延迟(以秒为单位),如果从节点的延迟超过这个值,则该从节点不会被计入min-slaves-to-write的计数中。
举个例子:当 min-slaves-to-write 设置为2,min-slaves-max-lag 设置为 10 秒时,主节点只有在至少有 2 个从节点延迟不超过 10 秒的情况下才会接受写操作。这两个参数就使得发生脑裂的时候,如果某个主节点跟随的从节点数量不够或延迟较大,就无法被写入,这样就能避免脑裂导致的数据不一致。
建议集群部署奇数个节点,例如集群数为 5,那么可以设置 min-slaves-to-write 为 3,min-slaves-max-log 为 5-10 秒。
脑裂问题能完全避免吗
不能。在选举进行的时候,主节点恢复了,此时它还是跟着很多从节点,假设 min-slaves-max-log 配置了 10s,可能此时从节点和主节点延迟才 6s,因此此时主节点还是可以被写入。而等选举完毕了,选出新的主节点,旧的主节点被哨兵操作需要 salveof 新主,此时选举时间内写入的数据会被覆盖,因此就导致了数据不一致的问题。
扫盲八股
Memcached 与 Redis 的区别有了解过吗?
我们可以从四个方面来进行对比。首先就从功能上来说吧:Redis 支持发布订阅、Lua 脚本等特性,但是呢 Memcached 它的特性就比较少。然后是它的分布式架构:Redis 内置支持主从复制和集群分片(Redis Cluster),能在分布式环境中提供高可用性和扩展性,Memcached 它需要在客户端代码中自定义实现分布式逻辑。接着是它的持久化机制: Redis 支持持久化功能,可以将数据保存在磁盘上,通过 RDB 和 AOF 两种方式实现数据的持久化,它不支持数据持久化,适合存储临时的数据。最后就是内置的数据结构了,Redis 支持多种数据结构,适合存储复杂的数据类型,Memcached 仅仅支持简单的键值对存储,数据结构较为单一。
Redis 中内存淘汰策略有了解过吗
它一共有 8 中,8 种里面可以细分为两大类,即开启数据淘汰和不开启数据淘汰两大类,然后开启数据的那一类又可以进行细分,分为基于过期时间的淘汰策略 以及 全部数据的淘汰策略。
不淘汰数据(默认):
noeviction: 当运行内存超过最大设置内存的时候,不会淘汰数据,而是直接返回报错禁止写入。适用于所有数据都被持久化或避免丢失任何数据的场景。
设置了过期时间的数据淘汰
volattile-random: 随机淘汰掉设置了过期时间的 key。适用于对数据没有严格的优先级需求。
volatile-ttl: 优先淘汰掉较早过期的 key。适用于对实时性较敏感的场景,比如存储用户会话等。
volatile-lru(redis 3.0之前默认策略):淘汰掉所有设置了过期时间的,然后最久未使用的 key。只关心设置了过期时间的键。
volatile-lfu(reids4.0后新增): 与上面类似,不过是淘汰掉最少使用的 key。只关心设置了过期时间的键。
所有数据的数据淘汰
volatile-random: 随机淘汰掉设置了过期时间的 key
volatile-ttl: 优先淘汰较早过期的 key
allkeys-lfu(redis 4.0 后新增): 淘汰缓存中最少使用的 key

Redis 的虚拟内存(VM) 机制是什么 2.0 以后被废弃
通过这种方式,Redis 可以处理比实际物理内存更多的数据,但是数据存到磁盘在获取会显著的使得性能下降,满足不了高并发得场景,因此 Redis 在 2.0 版本之后放弃了 VM 机制,转而推荐使用更加高效得内存淘汰策略来管理内存。
工作原理
分页:Redis 将数据分为多个页面,每个页面的大小固定(默认为 4 KB)
交换数据:当 Redis 需要更多内存时,它会将冷数据页面写入磁盘,并在内存中释放这些页面。当需要访问这些数据时,Redis 会从磁盘读取相应页面并加载到内存中
冷热数据区分:Redis 会维护一个 LRU 列表,用于跟踪数据的访问频率。冷数据会被移动到磁盘,热数据会被保留在内存中。
看过 Redis 对象的源码吗?你认为它优秀的点在哪里呢
数据持久化:AOF 重写机制
Redis 会将所有写操作以追加的方式记录到 AOF 文件中,以保证数据的安全性,随着时间推移,AOF 文件可能会变得非常大,影响恢复速度。 Redis 引入了 AOF 重写(AOF Rewrite) 机制。
增加重写:在重写期间,Redis 仍然可以将新的写操作继续追加到旧的 AOF 文件中,确保了高并发写入时的持久化性能。
空间优化:AOF 重写过程中,它会根据当前的数据状态,生成最精简的命令集,即使一个键值经过多次修改,最终的 AOF 文件中只会保留一条最有效的命令。
异步执行:AOF 重写是通过子进程异步执行的,主进程不会受到影响,这保证了在执行重写的同时,Redis 性能不会受到影响。
共享内存池
Redis 中有许多常用的小整数,例如 0 到 9999,这些整数频繁地出现在各种命令和数据操作中。为了减少内存分配和释放地开销,Redis 使用了 共享内存池(Shared Object Pool) 实现了 对象复用和内存节省。
线程模型
Redis 使用单线程模型 来处理所有的客户端请求,并且通过多路复用(epoll、select等) + 事件驱动 机制,Redis 仍然能够处理大量地并发连接。这种设计减少了上下文切换和锁的开销,避免了多线程编程中的复杂性。
过期设计
Redis 支持为键设置过期时间,当键过期后,它应该被自动删除。它采用了一种 惰性删除与定期删除相结合的策略 来处理过期键值。
惰性删除:当客户端访问一个键时,Redis 会检查该键是否已经过期,如果过期则立即删除且只在访问时才检查键值的状态。
定期删除:Redis 每隔一段时间会随机抽取一部分键进行过期检查,并删除其中已过期的键值,通过这种方式,Redis 可以避免系统中大量存在过期键而无法及时清理的过程。
惰性删除确保访问性能,定期删除避免内存泄漏,实现了过期键的高效管理。
数据结构
Redis 对很多数据结构进行了优化,例如 sds、ziplist,还有哈希表的扩容机制。
SDS 结构中会额外存储字符串的当前长度,在O(1)时间内可以获取字符串长度。它通过维护分配空间的总长度和已用长度,能有效防止缓冲区溢出问题。当 SDS 中的字符串长度缩短时,并不会立即缩减内存空间,而是会保留部分缩短后的空间,作为内存优化。它支持自动扩展,当字符串长度增加时,SDS 会自动分配更大的内存空间,并且通常会额外预留一部分空间,动态扩展与空间预分配。
ziplist(压缩列表) 是 Redis 用于实现列表和哈希表的底层数据结构之一。当列表或哈希表中的数据量较小,且元素长度较短时,Redis 会选择使用压缩列表来存储数据。
压缩列表将所有元素紧凑地存储在一段连续地内存空间中,每个元素前都有一个向前遍历和向后遍历,避免了链表中指针地开销的额外开销。
压缩列表根据存储的元素长度动态调整编码方式,能够极大地节省内存。
性能与空间的平衡:当列表或哈希表变得很大时,Redis 会自动切换到更高效的链表或者哈希表结构,这种按需调整的数据结构设计,确保了在不同场景下的性能最优。
渐进式 rehash,哈希表在扩容或者缩容时不会一次性进行全量 rehash, 它采用了渐进式 rehash 的方式, 将 rehash 操作分摊到后续的增删改查操作中,从而避免了集中的性能抖动。
Redisson 看门狗机制了解过吗
主要用来避免 Redis 中的锁在超时后业务逻辑还未执行完毕,锁却被自动释放的情况,它通过定期刷新锁的过期时间来实现自动续期。
如果获取锁的客户端挂了怎么办?
续期是通过定时任务执行的,如果当前客户端宕机,那么定时任务就没了,等 30s 时间一到,集群中的其他客户就可以获取锁了。如果觉得 30s 的时间过长了。
可以直接在 redis 中删除对应的 key, 这样对应的锁就释放了
可以通过
lockwatchdogTimeout参数来修改看门狗机制的超时时间
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setConnectionPoolSize(10);
// 设置 Redisson 锁的看门狗超时时间为 15 秒
config.setLockWatchdogTimeout(15000);
RedissonClient redisson = Redisson.create(config);锁未设置过期时间才会续期
并不是 redisson 分布式锁都会有看门狗续期机制,不设置 leaseTime 即超时时间,才会有自动续期。

核心流程是:1. 客户端在获取锁时,启动一个定时任务,定期刷新锁的过期时间。2. 定时任务会在过期时间到期之前执行,通过异步方法续期锁的过期时间。3. 如果续期成功,定时任务会重新调度自己,如果失败,则取消续期操作。
Redisson 分布式锁的原理
锁的获取
Redisson 使用 Lua 脚本,利用 exists + hexists + hincrby 命令来保证只有一个线程能成功设置键值,同时 Redisson 会通过 pexpire 命令为锁设置过期时间,防止因宕机等原因导致锁无法释放产生死锁问题。
锁的续期
为了防止锁在持有过程中过期导致其他线程抢占锁,Redisson 实现了锁自动续期的功能,持有锁的线程会定期续期,即更新锁的过期时间,确保任务没有完成时锁不会失效。
锁的释放
锁释放时,Redission 也是通过 Lua 脚本保证释放操作的原子性,利用 hexists + del 确保只有持有锁的线程才能释放锁,防止误释放锁的情况。
可重入锁
Redisson 支持可重入锁,持有锁的新城可以多次获取同一把锁而不会被阻塞。利用了 Hash,key 为线程 ID,如果重入则 value + 1,如果释放则 value - 1,减到 0 说明锁被释放了,则 del 锁。
分布式锁可能会遇见的问题
时钟漂移
因为 Redis 分布式锁依赖于实例的时间来判断是否过期,如果时钟出现漂移,很可能导致锁直接失效。解决办法就是让所有节点的系统时钟通过 NTP 服务进行同步,减少时钟漂移的影响。
网络分区
在网络不稳定的情况下,客户端与 Redis 之间的连接可能会中断,如果未设置锁的过期时间,可能会导致锁无法正常释放,如果有多个锁,还可能会引发死锁的问题。
单点故障
如果 Redis 单机部署,当实例宕机或不可用,整个分布式锁服务将无法正常工作,阻塞业务的正常执行。
主从问题
如果线上 Redis 是主从 + 哨兵部署的,则分布式锁可能会有问题。
因为 Redis 的主从复制过程是异步实现的,如果 Redis 主节点获取到锁之后,还没同步到其他的从节点,此时 Redis 主节点发送宕机了,这个时候新的主节点上没锁的数据,会导致多个应用服务器同时获取锁。
业务未执行完,锁已经到期
为了避免持有锁的客户端崩溃或因网络问题断开连接时,锁无法被正常释放,需要给锁设置过期时间。
可以设置一种续约机制(Redisson 中的看门狗机制),线程 a 在执行的时候,设置一个超时时间,并且启动一个守护线程,守护线程每隔一段时间就去判断线程 a的执行情况,如果 a 还没有执行完毕并且 a的时间快过期了,就重新设置一下超时时间,即继续续约。
Redis 的 Red Lock 是什么,有了解过吗?
所以 Redis 推出了红锁,避免这种状况产生。它主要解决问题就是当部分节点发生故障也不会影响锁的使用和数据问题的产生。
红锁的实现原理
要想使用 Red lock,我们需要集群部署 redis,官方推荐至少 5 个实例,不需要部署从库和哨兵,仅仅需要主库。
这 5 个实例之间没有任何关系(不同于 redis cluster),它们之间不需要任何信息交互。客户端会对这 5 个实例依次申请锁,如果最终申请成功的数量超过半数(>=3),则表明红锁申请成功,反之失败。
具体的加锁流程如下:
客户端获取当前时间(t1)。
客户端按照顺序依次对 N 个 Redis 节点利用 set 命令进行加锁操作,对每个节点加锁都会设置超时时间(远远小于锁的总过期时间),如果当前节点请求超时,立马向下一个节点申请锁。
当客户端成功从半数的 Redis 节点获取到了锁,这个时候获取一下当前时间 t2,然后计算加锁过程的总耗时 t(t2 - t1),如果 t < 锁的过期时间,这个时候就可以判断加锁成功,反之加锁失败。
加锁成功则执行业务逻辑,加锁失败则依次向全部节点发起释放锁的流程。
业务上
从上面的内容大家也可以知道红锁的实现成本其实不低,需要至少 5 个实例,而且因为要依次加锁,所以性能来说也比不上单实例的 Redis 加锁,且极端环境下还是有问题。所以业务上还是使用主从+哨兵来实现相关的分布式锁
Redis 中的 Big Key 问题是什么,该如何解决呢?
大 key 就是指一个内存空间占用比较大的键值。它的缺点如下:
操作 Key 时耗比较长,可能会导致客户端等待超时。
大 Key 对资源的占用巨大,在你进行网络 I/O 传输时候, 导致你获取过程中产生的网络流量较大,从而产生网络传输时间延长甚至网络传输发现阻塞的现象。
由于 Redis 单线程执行命令,操作大 Key 时耗时较长,从而导致 Redis 出现其他命令阻塞的问题
内存分布不均,在集群模式下,不同 slot 分配到不同示例中,如果大 key 都映射到一个实例,则分布不均,查询效率也会受到影响。
如何解决 big key 问题
数据分布方面
采用 Redis 集群方式进行 Redis 的部署,然后将大 key 拆分散落到不同的服务器上面,加快响应速度。
业务方面
第一:可以根据实际情况,调整存储策略,只存一些必要的数据。第二:可以优化业务逻辑,消除数据冗余。
开发方面
第一:对要存储的数据进行压缩之后再进行存储。第二:选择合适的数据结构。第三:将一个大 key 拆分成若干个小 key,降低单个 key 的内存大小。

如何解决 Redis 中的热点 key 问题

如果一个 key 的访问频率占比过大,或带宽占比过大,都属于热点 key。
由于 Redis 的读写是单线程执行的,所以热点 key 可能会影响 Redis 的整体效率,消耗大量的 CPU 资源,从而降低 Redis 的整体吞吐量,集群环境下会使得流量不均衡,从而导致读写热点倾斜问题的发生。
限流和降级;在热点 Key 访问过高时,应用限流策略,减少对 Redis 的请求,或者在必要时返回降级的数据或空值。
多级缓存:在 Redis 前增加其他缓存层(如 CDN、本地缓存),以分担 Redis 的访问压力。
读写分离:通过 Redis 主从复制,将读请求分发到多个从节点,从而减轻单节点压力。
热点 key 拆分:将热点数据分散到多个 Key 中,例如通过引入随机前缀,使不同用户请求分散到多个 Key, 多个 key 分布在多实例中,避免集中访问单一 key。
如何发现热点 Key 呢?
根据业务经验进行分析
这个主要就是依据业务场景进行分析,比如某明星的花边新闻、秒杀活动、演唱会门票等。优点是简单直接,基本没有什么成本。缺点就是,对于业务能力有一定的要求且有些突发事情是无法预测的。
redis 集群监控
我们只需要查看集群中哪个 Redis 出现 QPS 倾斜,而出现 QPS 倾斜的实例有极大的概率存在热点 Key。缺点 不一定所有 QPS 倾斜都是热 Key 导致的,优点在普遍是 Redis 集群部署下使用起来非常简单。
使用 hotkey 监控
这个是 Redis 4.0 版本后引入的一个新指令,只需要在命令行执行 redis-cli 时加上即可,它是通过 scan + object freq 实现的。优点是 Redis 自带命令使用起来简单快捷,缺点是需要扫描整个 keyspace,如果 Redis 中的 key 数量比较多的话,可能导致执行时间非常长并且实时性也不太好。
代理层收集
在代理层进行统一的收集,有些服务在请求 Redis 之前都会请求一个代理服务,可以使用在代理层收集 Redis 热 Key 数据和在客户端收集比较类似。缺点:需要给 redis 定制一个代理层,进行转发等操作,构建代理成本不低且转发有性能损耗。
应用程序中的多级缓存
一级缓存:一般指的是应用程序本地缓存(如 JVM 内存中的缓存)。二级缓存:则为 Redis 缓存,当数据不在一级缓存中时,才会请求二级缓存。
通过多级缓存架构,可以有效减少 Redis 的访问次数,从而避免单 Key 的热点问题。
热点 key 的拆分
全量拷贝:即将 resume这个 key 复制成 resume_1、resume_2、resume_3,它们之间的数据是一致的,这样不同用户都访问到全量的数据。
部分拷贝:直接进行 key 的拆分,resume_1、resume_2、resume_3 各存一部分的数据,不同用户仅需要访问不同数据即可。不同用户可以进行 hash,将用户 id 哈希之后取余得到后缀,拼上 resume_ 即可组成一个 key。
Redis 的 Pipeline 功能是什么
作用就是允许在一次网络连接中,批量的将多条命令发送给服务端执行。好处:1. 减少网络往返时间(多条命令一次发送过去) 2. 减少服务端上下文切换(服务端读写数据,涉及到了内核态和用户态的转换,多条命令一起发送,只会有一次切换)。
注意点:命令不能太多,因为客户端是等待命令的返回的,太多命令的话,会造成客户端长时间等待;而且每条命令执行的结果,服务端会放在内存中,太多命令的话,也会消耗过多的内存,不能保证原子性,要保证原子性可以使用 lua 脚本或者事务。
