Redis面试指南
Redis
单线程
o多线程编程模式面临的共享资源的并发访问控制问题
oRedis 却能使用单线程模型达到每秒数十万级别的处理能力
o原因
Redis 的大部分操作在内存上完成,再加上它采用了高效的数据结构,例如哈希表和跳表,这是它实现高性能的一个重要原因
Redis 采用了多路复用机制,使其在网络 IO 操作中能并发处理大量的客户端请求,实现高吞吐率
在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字
o为了在请求到达时能通知到 Redis 线程,select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数
oRedis 无需一直轮询是否有请求实际发生,这就可以避免造成 CPU 资源浪费。同时,Redis 在对事件队列中的事件进行处理时,会调用相应的处理函数,这就实现了基于事件的回调。因为 Redis 一直在对事件队列进行处理,所以能及时响应客户端请求,提升 Redis 的响应性能。
持久化
oAOF(Append Only File)日志
AOF 日志正好相反,它是写后日志,“写后”的意思是 Redis 是先执行命令,把数据写入内存,然后才记录日志
三种写回策略
Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘
Everysec,每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘
No,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
AOF 重写机制就是在重写时,Redis 根据数据库的现状创建一个新的 AOF 文件,也就是说,读取数据库中的所有键值对,然后对每一个键值对用一条命令记录它的写入
重写过程是由后台子进程 bgrewriteaof 来完成的,这也是为了避免阻塞主线程,导致数据库性能下降
oRDB 快照
命令
save:在主线程中执行,会导致阻塞
bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的默认配置
数据同步
o高可靠性
数据尽量少丢失
AOF 和 RDB 保证了前者
服务尽量少中断
增加副本冗余量
主从库模式
以保证数据副本的一致
主从库之间采用的是读写分离的方式
读操作
o主库、从库都可以接收
写操作
o首先到主库执行,然后,主库将写操作同步给从库
从库和主库建立起连接,并告诉主库即将进行同步,主库确认回复后,主从库间就可以开始同步了
哨兵机制
监控是指哨兵进程在运行时,周期性地给所有的主从库发送 PING 命令,检测它们是否仍然在线运行。如果从库没有在规定时间内响应哨兵的 PING 命令,哨兵就会把它标记为“下线状态”;同样,如果主库也没有在规定时间内响应哨兵的 PING 命令,哨兵就会判定主库下线,然后开始自动切换主库的流程
选主。主库挂了以后,哨兵就需要从很多个从库里,按照一定的规则选择一个从库实例,把它作为新的主库。这一步完成后,现在的集群里就有了新主库。
通知。在执行通知任务时,哨兵会把新主库的连接信息发给其他从库,让它们执行 replicaof 命令,和新主库建立连接,并进行数据复制。
数据类型
ostring
简单动态字符串(Simple Dynamic String,SDS)
buf:字节数组,保存实际数据。为了表示字节数组的结束,Redis 会自动在数组最后加一个“\0”,这就会额外占用 1 个字节的开销
len:占 4 个字节,表示 buf 的已用长度
alloc:也占个 4 字节,表示 buf 的实际分配长度,一般大于 len
String 类型并不是适用于所有场合的,它有一个明显的短板,就是它保存数据时所消耗的内存空间较多
oset
集合类型有非常节省内存空间的底层实现结构
oSorted Set
同时采用跳表和哈希表两个索引结构
跳表高效支持范围查询
哈希表高效支持单点查询
oStream
为了节省内存空间,在 Stream 数据类型的底层数据结构中,采用了 Radix Tree 和 listpack 两种数据结构来保存消息
Radix Tree
这个数据结构的最大特点是适合保存具有相同前缀的数据
Radix Tree 是属于前缀树的一种类型。前缀树也称为 Trie Tree,它的特点是,保存在树上的每个 key 会被拆分成单字符,然后逐一保存在树上的节点中。前缀树的根节点不保存任何字符,而除了根节点以外的其他节点,每个节点只保存一个字符。当我们把从根节点到当前节点的路径上的字符拼接在一起时,就可以得到相应 key 的值了
相比哈希表的保存方式,前缀树能够很好地节省内存空间,这对于 Redis 来说是非常重要的
在前缀树中,如果一系列单字符节点之间的分支连接是唯一的,那么这些单字符节点就可以合并成一个节点,而这种结构的树,就正是 Radix Tree,也被称为基数树
数据结构
o第一类节点是非压缩节点
这类节点会包含多个指向不同子节点的指针,以及多个子节点所对应的字符
o第二类节点是压缩节点
类节点会包含一个指向子节点的指针,以及子节点所代表的合并的字符串
底层数据结构
o简单动态字符串(Simple Dynamic String,SDS)
提升字符串的操作效率,并且可以用来保存二进制数据
o压缩列表(ziplist)
表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量,以及列表中的 entry 个数。压缩列表尾还有一个 zlend,表示列表结束。
entry
prev_len,表示前一个 entry 的长度。prev_len 有两种取值情况:1 字节或 5 字节。取值 1 字节时,表示上一个 entry 的长度小于 254 字节。虽然 1 字节的值能表示的数值范围是 0 到 255,但是压缩列表中 zlend 的取值默认是 255,因此,就默认用 255 表示整个压缩列表的结束,其他表示长度的地方就不能再用 255 这个值了。所以,当上一个 entry 长度小于 254 字节时,prev_len 取值为 1 字节,否则,就取值为 5 字节。
len:表示自身长度,4 字节
encoding:表示编码方式,1 字节
content:保存实际数据
实现
list
hash
set
特点
ziplist 的最大特点,就是它被设计成一种内存紧凑型的数据结构,占用一块连续的内存空间,以达到节省内存的目的。
设计代价
不能保存过多的元素,否则访问性能会降低
不能保存过大的元素,否则容易导致内存重新分配,甚至可能引发连锁更新的问题
oquicklist
一个 quicklist 就是一个链表,而链表中的每个元素又是一个 ziplist
typedef struct quicklistNode { struct quicklistNode *prev; //前一个quicklistNode struct quicklistNode *next; //后一个quicklistNode unsigned char *zl; //quicklistNode指向的ziplist unsigned int sz; //ziplist的字节大小 unsigned int count : 16; //ziplist中的元素个数 } quicklistNode;
o紧凑列表(listpack)
用一块连续的内存空间来紧凑地保存数据
listpack 元素会对不同长度的整数和字符串进行编码
缓存异常
o缓存雪崩
指大量的应用请求无法在 Redis 缓存中进行处理,紧接着,应用将大量请求发送到数据库层,导致数据库层的压力激增
原因
缓存中有大量数据同时过期,导致大量请求无法得到处理
o设置随机过期时间
o服务降级
当业务应用访问的是非核心数据(例如电商商品属性)时,暂时停止从缓存中查询这些数据,而是直接返回预定义信息、空值或是错误信息
当业务应用访问的是核心数据(例如电商商品库存)时,仍然允许查询缓存,如果缓存缺失,也可以继续通过数据库读取
宕机
o在业务系统中实现服务熔断或请求限流机制
o事前预防
通过主从节点的方式构建 Redis 缓存高可靠集群
o缓存击穿。
针对某个访问非常频繁的热点数据的请求,无法在缓存中进行处理,紧接着,访问该数据的大量请求,一下子都发送到了后端数据库,导致了数据库压力激增,会影响数据库处理其他请求
原因
热点数据过期失效
o对于访问特别频繁的热点数据,我们就不设置过期时间了
o缓存穿透
缓存穿透是指要访问的数据既不在 Redis 缓存中,也不在数据库中,导致请求在访问缓存时,发生缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据
原因
业务层误操作:缓存中的数据和数据库中的数据被误删除了,所以缓存和数据库中都没有数据
恶意攻击:专门访问数据库中没有的数据
解决方案
缓存空值或缺省值
o一旦发生缓存穿透,我们就可以针对查询的数据,在 Redis 中缓存一个空值或是和业务层协商确定的缺省值
布隆过滤器
o使用布隆过滤器快速判断数据是否存在,避免从数据库中查询数据是否存在,减轻数据库压力
在请求入口的前端进行请求检测
事务
oMULTI 和 EXEC 命令可以保证执行这些命令时的原子性
oMULTI 命令:表示一系列原子性操作的开始。收到这个命令后,Redis 就知道,接下来再收到的命令需要放到一个内部队列中,后续一起执行,保证原子性
oEXEC 命令:表示一系列原子性操作的结束。一旦 Redis 收到了这个命令,就表示所有要保证原子性的命令操作都已经发送完成了。此时,Redis 开始执行刚才放到内部队列中的所有命令操作
6.0 新特性
o面向网络处理的多 IO 线程
采用多个 IO 线程来处理网络请求,提高网络请求处理的并行度
Redis 的多 IO 线程只是用来处理网络请求的,对于读写命令,Redis 仍然使用单线程来处理
Redis 处理请求时,网络处理经常是瓶颈,通过多个 IO 线程并行处理网络操作,可以提升实例的整体处理性能
而继续使用单线程执行命令操作,就不用为了保证 Lua 脚本、事务的原子性,额外开发多线程互斥机制了
主线程和多 IO 线程的协作分成四个阶段
阶段一:服务端和客户端建立 Socket 连接,并分配处理线程
阶段二:IO 线程读取并解析请求
阶段三:主线程执行请求操作
阶段四:IO 线程回写 Socket 和主线程清空全局队列
设置
设置 io-thread-do-reads 配置项为 yes,表示启用多线程
设置线程个数
o客户端缓存
普通模式
实例会在服务端记录客户端读取过的 key,并监测 key 是否有修改
广播模式
服务端会给客户端广播所有 key 的失效情况
在实际应用时,我们会让客户端注册希望跟踪的 key 的前缀,当带有注册前缀的 key 被修改时,服务端会把失效消息广播给所有注册的客户端
和普通模式不同,在广播模式下,即使客户端还没有读取过 key,但只要它注册了要跟踪的 key,服务端都会把 key 失效消息通知给这个客户端
o细粒度的权限控制
支持创建不同用户
支持以用户为粒度设置命令操作的访问权限
oRESP 3 协议的使用
Redis 6.0 实现了 RESP 3 通信协议,而之前都是使用的 RESP 2。在 RESP 2 中,客户端和服务器端的通信内容都是以字节数组形式进行编码的,客户端需要根据操作的命令或是数据类型自行对传输的数据进行解码,增加了客户端开发复杂度。
RESP 3 直接支持多种数据类型的区分编码,包括空值、浮点数、布尔值、有序的字典集合、无序的集合等。
o特性作用
提升 Redis 吞吐量
加速业务应用访问
支持多用户以不同权限访问 Redis
高效支持不同数据类型使用,支持客户端缓存
禁用部分命令
oKEYS
oFLUSHALL
oFLUSHDB
- 点赞
- 收藏
- 关注作者
评论(0)