Redis面试知识点
2022-11-11 21:40:14 24 举报
AI智能生成
登录查看完整内容
Redis为什么这么快? Redis的数据结构 Redis的数据类型 Redis对象共享 Redis内存回收机制 Redis对象的空转时长 redis的线程模型 Redis 是单线程吗? 为啥Redis单线程模型也能效率这么高? 客户端与redis通信流程 Redis 6.0 之后为什么引入了多线程? Redis 是如何实现数据不丢失的-持久化 Redis 是如何实现服务高可用的-数据复制 redis内存淘汰策略
作者其他创作
大纲/内容
1)纯内存操作Redis 将所有数据放在内存中,内存的响应时长大约为 100 纳秒,这是 redis 的 QPS 过万的重要基础。
2)核心是基于非阻塞的IO多路复用机制 就是select/epoll 机制。有了非阻塞 IO 意味着线程在读写 IO 时可以不必再阻塞了,读写可以瞬间完成然后线程可以继续干别的事了。redis 需要处理多个 IO 请求,同时把每个请求的结果返回给客户端。由于 redis 是单线程模型,同一时间只能处理一个 IO 事件,于是 redis 需要在合适的时间暂停对某个 IO 事件的处理,转而去处理另一个 IO 事件,这就需要用到IO多路复用技术了, 就好比一个管理者,能够管理个socket的IO事件,当选择了哪个socket,就处理哪个socket上的 IO 事件,其他 IO 事件就暂停处理了
3)单线程反而避免了多线程的频繁上下文切换带来的性能问题第一,单线程可以简化数据结构和算法的实现第二,单线程避免了线程切换和竞态产生的消耗,对于服务端开发来说,锁和线程切换通常是性能杀手。单线程的问题:对于每个命令的执行时间是有要求的。如果某个命令执行过长,会造成其他命令的阻塞,所以 redis 适用于那些需要快速执行的场景。
CPU 并不是制约 Redis 性能表现的瓶颈所在 ,更多情况下是受到内存大小和网络I/O的限制,所以 Redis 核心网络模型使用单线程并没有什么问题,如果你想要使用服务的多核CPU,可以在一台服务器上启动多个节点或者采用分片集群的方式。
为啥Redis单线程模型也能效率这么高?
在 Redis 启动初始化的时候,Redis 会将连接应答处理器跟 AE_READABLE 事件关联起来,接着如果一个客户端跟Redis发起连接,此时会产生一个 AE_READABLE 事件,然后由连接应答处理器来处理跟客户端建立连接,创建客户端对应的 Socket,同时将这个 Socket 的 AE_READABLE 事件跟命令请求处理器关联起来。当客户端向Redis发起请求的时候(不管是读请求还是写请求,都一样),首先就会在 Socket 产生一个 AE_READABLE 事件,然后由对应的命令请求处理器来处理。这个命令请求处理器就会从Socket中读取请求相关数据,然后进行执行和处理。接着Redis这边准备好了给客户端的响应数据之后,就会将Socket的AE_WRITABLE事件跟命令回复处理器关联起来,当客户端这边准备好读取响应数据时,就会在 Socket 上产生一个 AE_WRITABLE 事件,会由对应的命令回复处理器来处理,就是将准备好的响应数据写入 Socket,供客户端来读取。命令回复处理器写完之后,就会删除这个 Socket 的 AE_WRITABLE 事件和命令回复处理器的关联关系
客户端与redis通信流程
虽然 Redis 的主要工作(网络 I/O 和执行命令)一直是单线程模型,但是 在 Redis 6.0 版本之后,也采用了多个 I/O 线程来处理网络请求 , 这是因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上 。所以为了提高网络请求处理的并行度,Redis 6.0 对于网络请求采用多线程来处理。 但是对于读写命令,Redis 仍然使用单线程来处理, 所以大家 不要误解 Redis 有多线程同时执行命令。Redis 官方表示, Redis 6.0 版本引入的多线程 I/O 特性对性能提升至少是一倍以上 。Redis 6.0 版本支持的 I/O 多线程特性,默认是 I/O 多线程只处理写操作(write client socket),并不会以多线程的方式处理读操作(read client socket)。要想开启多线程处理客户端读请求,就需要把 Redis.conf 配置文件中的 io-threads-do-reads 配置项设为 yes同时, Redis.conf 配置文件中提供了 IO 多线程个数的配置项 io-threads N,表示启用 N-1 个 I/O 多线程(主线程也算一个 I/O 线程)
关于线程数的设置,官方的建议是如果为 4 核的 CPU,建议线程数设置为 2 或 3,如果为 8 核 CPU 建议线程数设置为 6,线程数一定要小于机器核数,线程数并不是越大越好。 因此, Redis 6.0 版本之后, Redis 在启动的时候,默认情况下会有 6 个线程:Redis-server : Redis的主线程,主要负责执行命令;bio_close_file、bio_aof_fsync、bio_lazy_free:三个后台线程,分别异步处理关闭文件任务、AOF刷盘任务、释放内存任务;io_thd_1、io_thd_2、io_thd_3:三个 I/O 线程,io-threads 默认是 4 ,所以会启动 3(4-1)个 I/O 多线程,用来分担 Redis 网络 I/O 的压力
Redis 6.0 之后为什么引入了多线程?
AOF 日志(Append Only File,文件追加方式):记录所有的操作命令,并以文本的形式追加到文件中。RDB 快照(Redis DataBase):将某一个时刻的内存数据,以二进制的方式写入磁盘。混合持久化方式:Redis 4.0 新增了混合持久化的方式,集成了 RDB 和 AOF 的优点
因为 ,Redis 在写入日志之前,不对命令进行语法检查;所以,只记录执行成功的命令,避免了出现记录错误命令的情况;并且,在命令执行完之后再记录,不会阻塞当前的写操作。当然,这样做也会带来风险数据可能会丢失:如果 Redis 刚执行完命令,此时发生故障宕机,会导致这条命令存在丢失的风险。可能阻塞其他操作:虽然 AOF 是写后日志,避免阻塞当前命令的执行,但因为 AOF 日志也是在主线程中执行,所以当 Redis 把日志文件写入磁盘的时候,还是会阻塞后续的操作无法执行。
Reids 为什么先执行命令,再把数据写入日志呢?AOF
因为 AOF 日志记录的是操作命令,不是实际的数据,所以用 AOF 方法做故障恢复时,需要全量把日志都执行一遍,一旦日志非常多,势必会造成 Redis 的恢复操作缓慢。为了解决这个问题,Redis 增加了 RDB 内存快照(所谓内存快照,就是将内存中的某一时刻状态以数据的形式记录在磁盘中)的操作,它即可以保证可靠性,又能在宕机时实现快速恢复。和 AOF 不同的是,RDB 记录 Redis 某一时刻的数据,而不是操作,所以在做数据恢复时候,只需要直接把 RDB 文件读入内存,完成快速恢复
RDB 快照是如何实现的呢?
因为 Redis 的单线程模型决定了它所有操作都要尽量避免阻塞主线程,所以对于 RDB 快照也不例外,这关系到是否会降低 Redis 的性能。为了解决这个问题,Redis 提供了两个命令来生成 RDB 快照文件,分别是 save 和 bgsave。save 命令在主线程中执行,会导致阻塞。而 bgsave 命令则会创建一个子进程,用于写入 RDB 文件的操作,避免了对主线程的阻塞,这也是 Redis RDB 的默认配置
RDB 做快照时会阻塞线程吗?
RDB 做快照的时候数据能修改吗?
Redis 对 RDB 的执行频率非常重要,因为这会影响快照数据的完整性以及 Redis 的稳定性,所以在 Redis 4.0 后,增加了 AOF 和 RDB 混合的数据持久化机制:把数据以 RDB 的方式写入文件,再将后续的操作命令以 AOF 的格式存入文件,既保证了 Redis 重启速度,又降低数据丢失风险
Redis 是如何实现数据不丢失的-持久化
主从同步 (主从复制)
哨兵模式做到了可以监控主从服务器,并且提供自动容灾恢复的功能
Redis Sentinel(哨兵模式)
Redis Cluster 是一种分布式去中心化的运行模式,是在 Redis 3.0 版本中推出的 Redis 集群方案,它将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高 Redis 服务的读写性能。
Redis Cluster 方案采用哈希槽(Hash Slot),来处理数据和实例之间的映射关系。在 Redis Cluster 方案中,一个切片集群共有 16384 个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中,具体执行过程分为两大步。根据键值对的 key,按照 CRC16 算法计算一个 16 bit 的值。再用 16bit 值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽。剩下的一个问题就是,这些哈希槽怎么被映射到具体的 Redis 实例上的呢?有两种方案。平均分配:在使用 cluster create 命令创建 Redis 集群时,Redis 会自动把所有哈希槽平均分布到集群实例上。比如集群中有 9 个实例,则每个实例上槽的个数为 16384/9 个。手动分配:可以使用 cluster meet 命令手动建立实例间的连接,组成集群,再使用 cluster addslots 命令,指定每个实例上的哈希槽个数
Redis Cluster(集群)
Redis 是如何实现服务高可用的-数据复制
a)针对设置了过期时间的key1. volatle-ttl:在筛选时,会针对设置了过期时间的键值对,根据过期时间的先后进行删除,越早过期的越先被删除.2. volatile-random:就像它的名称一样,在设置了过期时间的键值对中,进行随机删除。3. volatile-Iru:会使用LRU算法筛选设置了过期时间的键值对删除.4.volatile-lfu:会使用LFU 算法筛选设置了过期时间的值对删除。b)针对所有的key做处理:5.allkeys-random:从所有键值对中随机选择并删除数据。6. allkeys-Iru:使用LRU算法在所有数据中进行筛选制除。7.allkeys-lfu:使用LFU算法在所有数据中进行筛选删除。c)不处理:8.noeviction:不会剔除任何数据。拒绝所有写入操作并返回客户端错误信息\"(error) OOM command not allowed when used memory”。此时Redis只响应读操作。
LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的大部分情况适用
LFU(least frequently used (LFU) page-replacement algorithm)。即最不经常使用页置换算法,要求在页置换时置换引用计数最小的页,因为经常使用的页应该有一个较大的引用次数。但是有些页在开始时使用次数很多,但以后就不再使用,这类页将会长时间留在内存中,因此可以将引用计数寄存器定时右移一位,形成指数衰减的平均使用次数。大量热点缓存适用
redis内存淘汰策略
基于内存存储实现内存读写是比在磁盘快很多的,Redis基于内存存储实现的数据库,相对于数据存在磁盘的MySQL数据库,省去磁盘I/O的消耗。
高效的数据结构Mysql索引为了提高效率,选择了B+树的数据结构。其实合理的数据结构,就是可以让你的应用/程序更快。
合理的线程模型I/O 多路复用 多路I/O复用技术可以让单个线程高效的处理多个连接请求
单线程模型Redis是单线程模型的,而单线程避免了CPU不必要的上下文切换和竞争锁的消耗。
虚拟内存机制Redis直接自己构建了VM机制 ,不会像一般的系统会调用系统函数处理,会浪费一定的时间去移动和请求。Redis的虚拟内存机制是啥呢?虚拟内存机制就是暂时把不经常访问的数据(冷数据)从内存交换到磁盘中,从而腾出宝贵的内存空间用于其它需要访问的数据(热数据)。通过VM功能可以实现冷热数据分离,使热数据仍在内存中、冷数据保存到磁盘。这样就可以避免因为内存不足而造成访问速度下降的问题。
Redis为什么这么快?
简单动态字符串(simple dynamic string,简称SDS)
提前分配
惰性释放
二进制安全
兼容C字符函数
由于Redis数据库的特性,会频繁的增删查改,保存一些二进制数据,而原来的C字符串并不能高性能的完成这些事,所以Redis才自己封装了SDS
字符串SDS
链表 list
哈希表
rehashidx它记录了目前rehash的进度,为-1时则说明不进行rehash
字典结构
如果数据量比较多时,一次性移动我们的hash表,那么时间会比较久,就有可能造成redis服务停止。所以执行rehash时,并不是一次完成的,而是渐进式完成的
为字典的ht[1]分配空间,大小取决于ht[0]和所执行的扩展或者收缩操作。
将ht[0]的所有键值对rehash到ht[1]上(rehash指的是重新计算哈希值和索引,重新散列到ht[1]这个哈希表中)
移动完成后,释放ht[0]的空间,将ht[1]改为ht[0],并为ht[1]重新创建一个空的哈希表,为下一次rehash准备
rehash(重点)
1.为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
2.在字典中维持一个rehashidx,也就是上面的字典结构的属性,将其值设置为0,表示rehash工作开始
3.在rehash期间,程序除了执行指定的操作外,还会将索引为rehashidx的数据移动到ht[1]相当于将ht[0]里的数据删除,在ht[1]里面增加,当rehashidx这个索引的数据全部移动完成时,则将rehashidx值加1,直到全部完成
4.完成后,将rehashidx的值表示为-1,并将ht[1]设置为ht[0].
在渐进式rehash期间,字典进行的删除,更新,查找会在两张哈希表上进行。比如查找,redis会先在ht[0]查找,找不到才会到ht[1]上面查找而字典进行的插入操作,则只会在ht[1]表里执行。这样的话,ht[0]表里的数量只减 不增,也减少了重复插入的操作
渐进式rehash 4步
ht[x] 可以是ht[0] 或者ht[1] index = hash & dict->ht[x].sizemask;
hash = dict->type->hashFunction(key);
采用MurmurHash2算法来计算键的哈希值,这种算法最大的优点就是当输入有规律的数时,也能平均散列到数组中
哈希算法
字典 dict
一个跳跃表有多个跳跃表节点。通过zskiplist来持有这些节点。
跳跃表 zskiplist
整数集合是Redis用来保存整数值的集合,保证集合中不会出现重复的元素
当对数组中添加新元素时,如果新添加的元素类型大于原来的数组的类型,则需要对数组进行升级。
1. 更加方便C语言中要保存两种不同类型的元素就必须使用两个类型数组来保存而Redis则使用数组升级来避免了使用两个数组,更加方便,且不用担心类型错误
2. 节约内存要让一个数组保存不同的类型,最简单就是直接定义一个最大的数组类型,但是这样会占用不必要的空间而Redis则只是在必要的时候才升级数组,尽量节约了内存
数组升级的好处
数组升级(一旦升级就不会降级)
整数集合 intset
压缩列表 ziplist
quicklist由多个快速列表节点构成的双向链表,每一个快速列表节点都保存一个ziplist压缩列表
在快速列表中,两端节点的数据被访问的可能性比较高,中间访问的可能性比较低。如果符合这种场景的话,就可以把中间节点的数据用LZF算法进行压缩,进一步节省空间。
压缩配置参数list-compress-depth 默认0不压缩
快速列表 quicklist
Redis数据结构
其中:embstr和raw都是由SDS动态字符串构成的。唯一区别是:raw是分配内存的时候,redisobject和 sds 各分配一块内存,而embstr是redisobject和raw在一块儿内存中。
字符串 string
底层quicklist快速列表
对于 list 数据类型,可以实现简单的消息队列,另外可以利用lrange命令,做基于redis的分页功能Stack(栈)= LPUSH + LPOPQueue(队列)= LPUSH + RPOPBlocking MQ(阻塞队列)= LPUSH + BRPOP
列表 list
当键值对的长度都小于64字节,且键值对数量小于512个时,使用ziplist,否则使用hashtable
哈希 hash
集合对象底层可以是intset或者hashtable,如果集合对象全为整数,且数量小于等于512个,则使用intset;否则使用hashtable
对于 set 数据类型,由于底层是字典实现的,查找元素特别快,另外set 数据类型不允许重复,利用这两个特性我们可以进行全局去重,比如在用户注册模块,判断用户名是否注册;另外就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能
集合 set
有序集合对象使用ziplist或者skiplist来实现,当集合数量小于128且字符长度小于等于64字节时使用ziplist;否则使用skiplist
为什么要同时使用跳跃表和字典呢??
为什么要使用跳跃表,而不是用平衡树?跳跃表更加简单,使用范围查找比其他的平衡树效率要高;容易实现容易调试的;跳表插入和删除只要维护节点指针即可,不需要调整树。
当底层使用skiplist实现:这种有序集合对象使用zset结构作为底层实现,
对于 zset 数据类型,有序的集合,可以做范围查找,排行榜应用,取 TOP N 操作等
一个zset结构包含一个字典和一个跳跃表
有序集合 zset
bitmap布隆过滤器
常用于大数据量的统计,比如页面访问量统计或者用户访问量统计。①需求:要统计一个页面的访问量(PV)①方案:直接用redis计数器或者直接存数据库都可以②需求:要统计一个页面的用户访问量(UV),即:一个用户一天内如果访问多次的话,也只能算一次②方案:可能会想到用SET集合来做,因为SET集合是有去重功能的,key存储页面对应的关键字,value存储对应userId③需求:假如有几千万访问量,为了统计一个访问量,要频繁创建SET集合对象。③方案:针对大访问量需要进行统计的问题,redis实现了一种HyperLogLog算法。
Streams 内存版的kafka
Redis数据类型
由于C语言并没有自动的内存回收机制,在redisObject结构中有一个refcount引用计数属性,当该值为0,也就是该对象不再被其他所引用时,就会释放内存。
Redis内存回收机制
Redis对象共享
redisObject还有一个属性lru:LRU_BITS:记录了该对象最后一次被访问的时间。使用 object idletime命令便可以打印对象的空转时长:当前时间减去对象的lru属性值
Redis对象的空转时长
单reactor单线程模型
基于Reactor模式开发了网络事件处理器
Redis基于Reactor模式开发了网络事件处理器,这个处理器叫做文件事件处理器 file event handler。这个文件事件处理器,它是单线程的,所以 Redis 才叫做单线程的模型,它采用IO多路复用机制来同时监听多个Socket,根据Socket上的事件类型来选择对应的事件处理器来处理这个事件。如果被监听的 Socket 准备好执行accept、read、write、close等操作的时候,跟操作对应的文件事件就会产生,这个时候文件事件处理器就会调用之前关联好的事件处理器来处理这个事件。文件事件处理器是单线程模式运行的,但是通过IO多路复用机制监听多个Socket,可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了 Redis 内部的线程模型的简单性。文件事件处理器的结构包含4个部分:多个Socket、IO多路复用程序、文件事件分派器以及事件处理器(连接应答处理器(建立连接)、命令请求处理器(写)、命令回复处理器(读))。多个 Socket 可能并发的产生不同的操作,每个操作对应不同的文件事件,但是IO多路复用程序会监听多个 Socket,会将 Socket 放入一个队列中排队,每次从队列中取出一个 Socket 给事件分派器,事件分派器把 Socket 给对应的事件处理器。然后一个 Socket 的事件处理完之后,IO多路复用程序才会将队列中的下一个 Socket 给事件分派器。文件事件分派器会根据每个 Socket 当前产生的事件,来选择对应的事件处理器来处理。
1)文件事件处理器
当 Socket 变得可读时(比如客户端对redis执行write操作,或者close操作),或者有新的可以应答的 Sccket 出现时(客户端对redis执行connect操作),Socket就会产生一个AE_READABLE事件。当 Socket 变得可写的时候(客户端对redis执行read操作),Socket 会产生一个AE_WRITABLE事件。IO 多路复用程序可以同时监听 AE_REABLE 和 AE_WRITABLE 两种事件,如果一个Socket同时产生了这两种事件,那么文件事件分派器优先处理 AE_READABLE 事件,然后才是 AE_WRITABLE 事件
2)文件事件
redis的线程模型
Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发生数据给客户端」这个过程是由一个线程(主线程)来完成的,这也是我们常说 Redis 是单线程的原因。
但是, Redis 程序并不是单线程的 ,Redis 在启动的时候,是会 启动后台线程(BIO) 的:Redis 在 2.6 版本 ,会启动 2 个后台线程,分别处理关闭文件、AOF 刷盘这两个任务;Redis 在 4.0 版本之后 ,新增了一个新的后台线程,用来异步释放 Redis 内存,也就是 lazyfree 线程。例如执行 unlink key / flushdb async / flushall async 等命令,会把这些删除操作交给后台线程来执行,好处是不会导致 Redis 主线程卡顿。因此,当我们要删除一个大 key 的时候,不要使用 del 命令删除,因为 del 是在主线程处理的,这样会导致 Redis 主线程卡顿,因此我们应该使用 unlink 命令来异步删除大key。
之所以 Redis 为「关闭文件、AOF 刷盘、释放内存」这些任务创建单独的线程来处理,是因为这些任务的操作都是很耗时的,如果把这些任务都放在主线程来处理,那么 Redis 主线程就很容易发生阻塞,这样就无法处理后续的请求了。后台线程相当于一个消费者,生产者把耗时任务丢到任务队列中,消费者(BIO)不停轮询这个队列,拿出任务就去执行对应的方法即可
Redis 是单线程吗?
Redis
收藏
收藏
0 条评论
回复 删除
下一页