本文积累了我在(准备)面试的关于 Redis
的问题,当然我不喜欢背八股文,所以就会强迫自己去系统性学习, Redis
之前我开了一个 tag
的,但是没更完就要准备面试了,Java 后端岗位太卷了!!
大部分参考小林 Coding 以及敖丙的 JavaFamily
# 通用篇
Redis
为什么这么快?
有几个方面的原因:
- 单线程执行,在 6.0 版本以前处理网络请求和数据操作都是单线程,减少了上下文切换,性能对于一些中小项目是完全足够的。网络请求处理使用的 IO 多路复用,数据处理因为是在内存中操作,CPU 资源并不会限制
Redis
性能。6.0 版本对网络处理使用了多线程,保留数据操作使用单线程,同时页保证不会出现并发问题。 - 基于内存操作,不必多说,比
MySQL
要请求磁盘快多了。 - 高效的数据结构,
Redis
底层使用了很多高效的数据结构,比如SDS
、压缩列表、跳表、哈希表等。 - 自定义协议:使用了高性能的自定义
Redis
协议 RESP 和协议分析器。
这些因素加起来使得 Redis
能够达到一秒十万级别的处理。
参考链接:
- https://mp.weixin.qq.com/s/KtzvawDnQQwhfjnCoXpcMQ
- https://mp.weixin.qq.com/s/mscKInWNAuhCbg183Um9_g
# 数据类型篇
String、List、Hash、Set、Zset、Stream、Hyperloglog、GEO、BitMap、BloomFilter
redisObject
是什么,为什么需要这个对象?
redis
是键值数据库,这意味着会对键有大量的操作,一些命令只适用于特定的数据类型,如 zadd
只适用于 zset
而不适合 string
,但是又有一些命令适用于所有 key
,如 ttl
、 del
等,所以这些命令要正确执行, key
就需要带有类型信息,使得程序可以检查 key
类型,选择合适的处理方式。
为此 redis
构建了自己的类型系统,包括显示多态,类型判断,对象分配销毁和共享。 redisObject
的属性有 type、encoding、lru、refcount、ptr
typedef struct redisObject { | |
// 类型,判断数据类型 | |
unsigned type:4; | |
// 编码方式,判断数据结构 | |
unsigned encoding:4; | |
// LRU - 24 位,记录最末一次访问时间(相对于 lru_clock); 或者 LFU(最少使用的数据:8 位频率,16 位访问时间) | |
unsigned lru:LRU_BITS; // LRU_BITS: 24 | |
// 引用计数 | |
int refcount; | |
// 指向底层数据结构实例 | |
void *ptr; | |
} robj; |
当执行一个命令时,假设是 zadd
,就会先将 key
从字典中找到对应的 redisObject
,如果为 null
就说明 key
不存在,然后继续检查 type
是否正确,最后根据 encoding
判断底层的数据结构到底是什么来调用多态函数。
参考链接:
https://pdai.tech/md/db/nosql-redis/db-redis-x-redis-object.html
https://www.modb.pro/db/72947
Redis 的
SDS
是什么?
脑图:回忆一下当初学 C 语言时字符串的缺陷, SDS
就是为了克服这些缺陷。
SDS
是 Redis
自定义的简单动态字符串,也是 Redis
最基本的数据结构之一。设计 SDS
是因为 c 语言的字符串问题太多,性能太低了。主要问题是:
- 获取长度时间复杂度为
O(N)
,SDS
内部维护了当前字符串长度len
,O(1)
复杂度 - 操作不方便,容易溢出,类似
strcat
这种函数,拼接两个字符串,都会默认前一个字符串剩余空间足够,所以很不方便。SDS
维护了当前分配空间大小alloc
,可以检测剩余空间。 - 以
\0
结束,需要指定编码格式。这种性质使得字符串只能存储文本文件,SDS
使用了字节数组,使得可以存储任何可转为字节的数据。
SDS
还可以动态扩容,并且会还会多分配一些未使用空间,减少分配次数。具体是,当操作触发扩容机制,会先算出需要扩容到多少才够,这个值保存在 newLen
中,然后真正扩容还会多分配一些空间:
- 如果
newLen < 1MB
,那么newLen *= 2
再进行扩容 - 如果
newLen >= 1MB
,那么newLen += 1MB
再进行扩容
SDS
设计了不同类型的结构体,区别在于 len
和 alloc
的大小不同,通过为不同大小字符串灵活分配,可以节省内存。
struct __attribute__ ((__packed__)) sdshdr16 { | |
uint16_t len; | |
uint16_t alloc; | |
unsigned char flags; // SDS 类型 | |
char buf[]; | |
}; |
最后, SDS
还使用了编译优化 __attribute__ ((__packed__))
,告诉编译器取消结构在编译中的对齐优化,而是实际使用多少就分配多少。比如一个结构体有 1 个 int
和 1 个 char
,正常的优化对齐会使 char
对齐 int
,也就是 char
也会占 3 个字节。其实这 3 个字节就浪费了。 SDS
的编译优化就可以使 char
只分配一个字节。
SDS
与 redisObject
的关系根据字符串存储的值的不同而有所不同:
字符串对象保存的整数值,并且该整数可以用
long
表示,那么就会把redisObject.ptr
从void*
改为long
,并设置encoding=int
字符串对象保存的字符串,则使用
SDS
保存字符串,ptr
指向SDS
地址,实际数据放在SDS
中的buf
中。如果字符串字节长度<=X
,则encoding=embstr
反之则encoding=raw
。(x 在个版本中定义不同,2.+ 是 32,3.0-4.0 是 39,5.0 是 44)
embstr
会一次性分配 redisObject
和 SDS
的空间,有利于内存连续更好利用 CPU 缓存,减少内存分配次数,内存释放次数。而 raw
就需要分配两次。 embstr
是只读的,如果要对 embstr
操作,就会先升级为 raw
再执行修改命令。
参考链接:https://mp.weixin.qq.com/s/qptE172slg_6Tl1yuzdbfw
redis
的BitMap
是什么?
位图,本质就是比特数组,用于存储一些只有两个状态的数据,占用空间小,实际应用:打卡,判断用户登录态(5000 万用户只需要 6MB 空间)
刚才你提到了
HyperLoglog
,知道什么原理吗?
HyperLoglog
是一个基于基数统计(集合中不重复元素的个数)的数据结构(12kB 就可以统计 2^64 的数据量),其实底层算法很早之前就有人提出了,但是 Redis
第一次使用数据结构将其实现。说到应用,我们可以假设一个场景,有个业务需求需要每个网页都统计访问量,同一 IP 多次访问只算做一次访问。如果是在业务端实现,最先想到的就是对每一个网页加一个 Set
,最后需要统计量时直接获取集合大小即可,如果访问量很大,上百万、千万什么的,就非常消耗内存,不可能为了这么小的业务需求付出这么大的内存,并且这种业务是可以容忍一定误差的,所以就可以使用 Redis
里的 HyperLoglog
。
至于原理,涉及到统计概率中的伯努利实验,以及后来者引入的桶和加权平均等修正,我还没来得及深入了解。仅仅只知道这确实可以统计去重元素个数,但是存在一点误差,如果可以容忍误差,那么性能是很高的。
GEO
是什么,有什么用?
GEO
并没有设计新的数据结构,而是使用了 Sorted Set
,使用 GeoHash
编码方法实现了经纬度到元素权重分数的转换,关键就是【对二维地图做区间划分】和【对区间进行编码】,经纬度落到某个区间,就用这个区间的编码值表示 Sorted Set
元素的权重分数。
实际应用如滴滴叫车:主要使用 GEOADD
和 GEORADIUS
两个命令。使用 GEO
集合保存所有车辆的经纬度,当用户想寻找自己附近的车,LBS(Location Based Services)应用就可以以用户的经纬度为中心指定公里内的车辆信息找到并返回。
Redis
的压缩列表了解过吗?
压缩列表是 Redis
的基础数据结构,如果 list
或者 hash
的节点较少,且保存的项都是一些小整数或者短字符串,通常会使用压缩列表来作为列表键的底层实现。
压缩列表的本质是数组,不用链表是因为链表的节点之间内存不连续,无法高效利用 CPU
缓存,命中率很低。而压缩列表是内存连续的,命中率高。
压缩列表前面几个字段是列表的一些信息,比如列表占用字节数,列表尾部节点的偏移量,节点数量,压缩列表结束点。而每个节点的构成为:前一个节点的长度,自身数据类型和节点长度,数据。
为了尽可能节省内存,和 MySQL
记录中 varchar
一样,使用了不同字节来记录数据长度。前一个节点长度在 256 之内,使用 1 个字节,反之使用 5 个字节。但是这种机制会导致连锁更新问题,比如首节点插入长度大于 256 的数据,而下一个节点之前记录长度使用的 1 字节,此时就需要扩容,扩容后可能自身也超过了 256 字节,它的下一个节点也要扩容,如此往复,直到最后一个节点扩容完成。
参考链接:https://mp.weixin.qq.com/s/qptE172slg_6Tl1yuzdbfw
Redis
的哈希表了解过吗?
哈希表是 Redis
的基础数据结构,数据类型 hash
如果节点很多或者项是大的整数、长字符串,就会使用哈希表。哈希表底层实现使用的是数组,链式增长解决哈希冲突。当负载因子 >= 1
时,如果没有执行 rdb
或 aof
就会 rehash
。当负载因子 >=5
时,不论有没有 rdb\aof
都会 rehash
。
rehash
使用的是渐进式 rehash
,假设原哈希表 1 扩容后为哈希表 2,那么在 rehash
的过程中,每次有请求增删改查哈希表 1,就会把当前索引的节点转移到哈希表 2,使得整个 rehash
过程分配到各个请求上,避免一次性 rehash
的耗时操作。
如果有一个查询请求,在哈希表 1 查不到,就会去哈希表 2 查询。在渐进式 rehash
进行期间,哈希元素的操作都是在两个哈希表进行的。
参考链接:https://mp.weixin.qq.com/s/qptE172slg_6Tl1yuzdbfw
Redis
的整数集合是什么?
整数集合是实现 set
的数据结构之一,底层其实就是整数数组
typedef struct intset { | |
// 编码方式 | |
uint32_t encoding; | |
// 集合包含的元素数量 | |
uint32_t length; | |
// 保存元素的数组 | |
int8_t contents[]; | |
} intset; |
但是数组的元素类型由 encoding
控制,从 8bit
、 16bit
、 32bit
、 64bit
增长。不一来就用 64bit
也是想尽量节省空间。在插入新元素时,会维护有序性和唯一性。如果插入的整数所占用的字节超过了数组 1 个元素的字节,就要先升级再插入
Redis
的跳表了解过吗?
跳表算是一种很优雅的实现,相较于普通链表,查询效率提升,相较于二叉树,省去了平衡、树退化的问题。比如一个链表节点为: 1 2 3 4 5
那么从中挑出一半 1 3 5
形成新的链表,并且将新的链表接到原来的链表上面,如此直到最上面的节点只有 1 个。这种数据结构使得查询效率为 O(logn)
。但是当插入新节点时,需要调整上面的节点,严重时时间复杂度还是 O(n)
。
所以 Redis
也是优化了跳表,在每次插入节点时就通过随机数决定其层数(随机数 r<=0.25 就加一,继续生成,>0.25 就停止),然后提前加入到对应的层数。这样虽然不是严格的 log2N
,也许要存储的节点会变多,也可能变小,但总的效率依然维持在一个很高的水平。
在 Redis
常用的数据类型中, zset
就是通过跳表实现的。
至于为什么 zset
使用了跳表而不是平衡树,原因:
- 平衡树实现复杂,还要考虑插入删除后树的平衡调整。
- 在范围查找时,平衡树比较难实现,而跳表只需要找到最小值然后遍历即可。
参考链接:https://mp.weixin.qq.com/s/NOsXdrMrWwq4NTm180a6vw
redis
的quicklist
了解过吗?
3.0
版本之前,列表都是使用 list
和压缩列表( ziplist
)实现的, 3.2
之后就是只使用 quicklist
实现了。 quicklist
其实就是 list+ziplist
。
redis
的listpack
了解过吗?
listpack
也是一种压缩列表的实现,之前提到的 quicklist
并没有解决连锁更新的问题,就是因为节点记录了前一个节点的长度,为了能后从后向前遍历,所以 listpack
不再记录前一个节点的长度而是记录当前节点的长度。其实这也能从后向前遍历,是由 lpDecodeBacklen
函数实现,它会从当前列表项起始位置的指针开始,向左逐个字节解析,得到前一项的 entry-len
值。
# 持久化篇
Redis
如何保证数据持久化?
Redis
为了保证数据高可用,引入了持久化机制, 在早期版本,还有 VM,后来版本不推荐了。现在一般都是使用 RDB
、 AOF
或者混合持久化。
RDB
通过对内存数据拍摄快照来持久化数据,触发机制是在一定时间内发生一定次数的修改操作。当然也可以使用save/bgsave
主动拍摄快照,前者会阻塞线程,后者才会fork
一个子线程进行快照拍摄。因为采用了压缩算法,实际占用空间很小。异步存储为了保证数据一致性,借助了操作系统的Copy on Write
机制,主线程修改哪个页,就会先将这个页复制出来,复制页是用于 rdb,主进程还是在原地址修改。AOF
通过存储执行的命令到磁盘中保证数据的持久性,可以配置多种存储方式,比如执行一条命令就存储一条,或者每秒存储一次,或者看系统心情,什么时候有空什么时候就将缓冲区的命令存进去。AOF
机制执行久了,就会导致文件保存了很多无效的命令,所以需要重写AOF
文件 ——bgrewriteaof
,过程为:子线程遍历Redis
内存生成一系列指令,然后将这些指令序列化到临时文件中,过程中的增量命令会同时写到 aof 缓冲区和 aof 重写缓冲区,会追加到临时文件中,最后替换AOF
文件。这里需要重点说一下,我们将数据写入到文件中时,其实是先写入到内核缓冲区,再到磁盘缓冲区,最后到磁盘,最后一个阶段我们是无法介入的,但是可以调用fsync()
强制将数据刷新到磁盘缓冲区。redis
默认是每秒调用一次。(有参数控制何时重写,比如文件大小超过多少,增量达到多少)混合持久化
:4.0 版本后还出现了混合持久化(混合持久化工作在 AOF 日志重写过程。),该机制必须打开AOF
,(就是重写 aof 文件)隔一段时间拍摄快照,生成rdb
数据,两次快照之间的记录使用AOF
日志来记录,并追加到rdb
数据后面。恢复数据时,先回复rbd
数据,再执行AOF
日志。这种机制既解决了rdb
快照摄时突然断电导致整个快照丢失(因为还在临时文件中),也解决了AOF
文件太大,不断重写的性能消耗。
参考链接:
- Redis 进阶 - 持久化
- https://mp.weixin.qq.com/s/O_qDco6-Dasu3RomWIK_Ig
Redis
的大key
对持久化有什么影响?
首先大 key
不是指 key
的长度很大或者字面量很大,而是指对应的 value
占用内存很大。
大 key
在 aof
机制写入命令时,如果是 Always
机制,那么调用 fsync()
函数,将数据从内核缓冲区写入磁盘时,必须等待写完函数才会返回,如果是大 key
,数据量很大,自然就会导致长时间阻塞。如果是 Everysec
机制,因为是创建异步任务调用 fsync
,所以不会影响主线程。
同时,如果 redis
存储了很多大 key
,一方面会使 aof
文件频繁重写,另一方面会导致主线程对应的页表越大, rdb
异步快照和 aof
重写都会 fork
一个子线程,就需要为子线程复制一份页表,页表越大,复制过程就越长,主线程阻塞在 fork
调用就越久。
# 高可用篇
Redis
如何保证数据一致性?
首先,很难保证缓存和数据库 100% 数据一致,因为我们引入 Redis
本身是为了性能,花很大代价完全保证数据一致性,有时性能反而还会下降,只能说尽量吧。
考虑到并发,面对更新请求,我了解到的解决方案就是:先更新数据库再删除缓存(也有问题,但是相对概率很小)。如果是更新数据库 + 更新缓存,并发问题很大(先更新的会覆盖后更新的缓存),哪怕是先删除缓存再更新数据库(A 先读,B 修改删除了缓存,再更新数据库,A 将读到的旧数据再写入缓存),也存在并发问题,因为从数据库拿数据到缓存中是两步:从数据库读取,写到缓存中。只要不是原子操作,在并发环境就可能导致数据不一致。
再考虑到删除缓存操作可能会失败,现在的解决方案一般是使用消息队列或者订阅数据库变更日志再操作缓存(canal)。
参考链接:https://mp.weixin.qq.com/s/D4Ik6lTA_ySBOyD3waNj1w
Redis
内存淘汰是怎么一回事?
首先 Redis
对于过期了的 key
采用两种策略:惰性删除和定期删除。所以当内存耗尽时, Redis
存在过期 / 没过期两种键,所以删除策略也有不同,有 8 种:直接返回错误 / 删除 LRU、LFU 最早的过期(所有)key / 随机删除过期(所有)key
传统的 LRU
存在存储、误删的缺点,所以 Redis
配置文件定义了一个属性,默认为 5,会取出 5 个的 key
,按照 LRU
算法删除对应 key
。
至于 LFU
, Redis
也是采用了一些随机算法的策略,因为在 RedisObject
中有个 lru
属性,前 24bit
用于记录 LRU
,后 8bit
记录 LFU
的访问热度。 8bit
最多表示 255,所以不能单纯的访问一次就自增,而是通过比较两个参数:
- 从 0~1 随机生成一个随机数
R
- 配置中有一个
factory
参数,用于计算P = 1 / (差值*factory+1)
。这里的差表示当前热度减去初始值。 - 如果
P>R
,热度 + 1,反之 + 0。
可以看出,热度越高,那么上升的概率越小。
参考链接:https://mp.weixin.qq.com/s/-caMTrOXQu-o0O44e6I9dQ
Redis
主从复制原理是什么?
主节点和从节点的数据交互方式分为全量复制和增量复制。
- 全量复制:在从节点与主节点建立连接(tcp 长连接)时,从节点先发送
sync
,主节点会执行 bgsave 生成 rdb 文件再发送,使得从节点加载并初始化数据。在生成rdb
时新来的写命令请求会放在一个缓冲区,等rdb
传输完了就会传输这个缓冲区数据到salve
中。这个缓冲区就是replication buffer
。 - 增量复制:如果从节点(
client
)和主节点不断开连接,那么就可以一直通过replication buffer
(如果满了就会断开连接,删除缓存,重连)传输数据,但是如果连接不小心断开了,replication buffer
就会被释放。此时就需要增量复制。在建立主从连接时,双方会维护一个offset
,在主节点将写操作记录到replication buffer
时,还会记录到repl_backlog_buffer
环形缓冲区,从节点维护的offset
就是同步数据的偏移量。主节点维护的master_repl_offset
就是环形缓冲区的当前数据的偏移,当从节点重新连接,发现master
的offset
没有覆盖自己维护的offset
,就可以进行增量复制,如果覆盖了,就走全量复制。(abs(m_offset-s_offset) < len
)
参考链接:https://juejin.cn/post/6981744631000072205
Redis
集群中如何判断某个节点是否正常工作?
一般是一半以上的主节点 ping
一个节点都超时时,就会认为该节点挂了。
- 主节点会默认每
10s
(可以通过参数repl-ping-slave-period
控制)向从节点发送ping
命令,从而判断从节点存活性和连接状态。 - 从节点每 1 秒向主节点发送
replconf ack
命令,给主节点报告自己的复制偏移量。一方面检测网络状态,另一方面检测数据复制情况。
Redis
的脑裂现象了解过吗?
在 Redis
集群中,如果主节点 A 因为网络问题和集群失联了,但是客户端还在不断往主节点 A 写入数据。哨兵发现后选举出新的主节点 B,然后 A 网络恢复后降级为从节点 A,此时需要主从同步,从节点 A 需要先清空数据再获取主节点 B 的 rdb
文件。这就导致之前客户端写的数据没了,这其实也是一种数据不一致问题。
解决方案为:如果主节点的连接的从节点少于 N 个( min-slaves-to-write
)或者主从同步的延迟高于 x( min-slaves-max-lag
),就禁止客户端写入数据。
Redis
的哨兵机制了解过吗?
哨兵机制可以检测集群中主节点存活情况,如果主节点挂了,可以选举一个从节点为新的主节点,并把新的主节点的信息通知给客户端和其他从节点。所以哨兵主要负责:监控、选主、通知。
监控:哨兵每隔 1 秒就会向节点发送
ping
命令,如果节点回复超时(down-after-milliseconds
配置选项),就判定该节点主观下线。对于主节点而言,还有客观下线,就是主节点没有挂,因为服务器压力或者网络问题导致回复超时,如果哨兵集群超过一半都回复超时就会判定为客观下线。如果主节点并没有挂,那么超过一半的哨兵都回复超时的概率也比较低,这都是为了减少误判的概率。选主:当哨兵发现主节点挂了,就会问其他哨兵是否觉得主节点挂了,超过一半那么该节点就认为其客观下线,然后该哨兵就会成为候选者,向其他哨兵请求投票,如果票数超过一半 (票数超过
quorum = n/2+1
) ,就会作为leader
进行主从故障转移,选举出新的主节点。选举主节点的过程:- 从节点中选一个升级为主节点(从节点的网络状态,优先级,复制进度,ID 选出最合适的)
- 让其他从节点修改复制目标,复制新的主节点
- 将新的主节点的 IP 和信息通过发布者 / 订阅者机制通知给客户端
- 继续监控旧主节点,一旦恢复上线,将其降为从节点
通知:客户端通过发布者 / 订阅者机制知道新选举出来的主节点。主从节点切换完后会在
+switch-master
频道发布新主节点的 IP 地址和信息。
哨兵集群之间是如何通信的?
在搭建主从集群的哨兵集群时,每个哨兵只需要指定主节点名字、 IP
、端口以及 quorum
,哨兵之间就是通过 Redis
的发布者 / 订阅者机制通信的。主节点有一个 _sentinel_:hello
,各个哨兵把自己的 IP 和信息发布到 _sentinel_:hello
频道,其他哨兵就可以获取信息进行通信。
redis 集群中,为什么是每个主节点分配一系列个槽而不是每个主节点分配一个槽?
这个问题涉及到 redis 的设计问题,大家可以开放性回答,但一定要言之有理,因为面试官往往会深问下去,如果你的想法很片面,往往无法回答面试官下一个问题。我谈谈我的理解,不一定全面,也不一定完全正确,大家要有自己的理解。
首先,我们搭建 redis 集群往往是为了服务的高可用,比如一个 redis 一般是一秒 10 万的处理级别,但是当一瞬间请求特别高(比如双十一秒杀),一个 redis 也扛不住。所以需要搭建 redis 集群。这就涉及到数据该放到哪一个主节点中,于是就有了分槽的机制。redis 并没有把分槽机制写死,默认的分槽机制就是:每个主节点的槽数 =(总槽数 / 主节点个数)。然后一个请求过来,先看它的 key,将 key 做 CRC 冗余检验并取模看看这个 key 属于哪个槽从而判断这个 key(请求)属于哪个主节点。那么看到这里,其实每个主节点只分一个槽,然后主节点总数作为槽的总数其实也是没问题的。
美团二面问我这个,我当时回答的是:一个主节点分配多个槽,可能是为了让请求分布更均匀。这个回答就很浅,所以他就继续问:你能说说这个过程吗,为什么一个节点有多个槽,他就分配均匀?我直接懵逼。
// 以下内容付费可见,如果面试被问到了这个题,记得请我吃饭
我下来自己思考的思路如下:一个节点多个槽比一个节点一个槽,redis 多出来的机制是什么?很显然,槽越多,越好分配。我们可以自己分配每个节点的槽有哪些,这个很重要,如果每个主节点只有一个槽,那么是不可能有自定义分配机制的。那么为什么要自定义分配机制呢?比如我有很多个热点数据 key,我一个 redis 可能可以抗住三四个热点数据的请求,那么如果我所有的热点数据都落在了一个 redis 怎么办?那么我搭建集群不就没有意义了吗?所以我就可以分槽,这些热点数据可能数据槽 A,槽 B,槽 C,我就可以把这些槽分配到不同的 redis 节点中,这样就使得请求均匀到不同的节点中。现在我们假设每个主节点只能分配一个槽,如果这些热点数据取模后都落在了一个主节点上,我们是没有太多手段来解决这个问题的(当然可以增加主节点个数,使得取模的总数不相同,但是一方面解决方法不如自定义槽分配优雅,另一个方面就是集群中节点个数不可能太大,那么热点 key% 总数落到同一个节点的概率也很大)。
这就是我个人的理解,回顾我的美团二面,第一个问题是 MySQL 集群中有多个主节点,从节点应该听谁的,或者说多个主节点如何保持数据一致,一开始 hr 的问题描述就在带偏我,正确的问法也不是这么问的,但是我太在意这次面试了,根本没有好好冷静思考,导致我越说越混乱,第二个问题就是 redis 的槽分配,以前我也注意到槽这个问题的,但是从来没有深究过,我确实没想到会问的这么细,第三个问题就是从全国身份证号查询一个给定身份证号,有什么好的思路,这个也是意难平,因为听到这个问题,我就想到了 redis 的一个最基本的设计机制:我传入 key,redis 是如何快速在内存中找到的?答案是 redis 存储 key 在最底层用了字典,可是我老早就听到了字典这个概念,一直没想着去了解它 (md 就是一棵树),但是计算机的知识就是这样,很多你一听就知道,但是没听过就根本无法想象还有这样一种思路。最后面试官给了我一个困难题算法,我没写出来,最可气的就是当天晚上我在力扣上搜到这道题,冷静下来后一会就 AC 了。这次面试真的是意难平,我老跟同学、家人说那个 hr 面的太难了,问的都是一些业务、设计类题目,不去实习,不去真正做一下很难知道这些思路,但是现在反思一下,我太在意这次的面试了,如果我冷静一点,就不会像现在这样,叹气说一声,苍天悠悠,何薄于我。