Redis面试知识点
2022-11-11 21:40:14 29 举报
AI智能生成
Redis为什么这么快? Redis的数据结构 Redis的数据类型 Redis对象共享 Redis内存回收机制 Redis对象的空转时长 redis的线程模型 Redis 是单线程吗? 为啥Redis单线程模型也能效率这么高? 客户端与redis通信流程 Redis 6.0 之后为什么引入了多线程? Redis 是如何实现数据不丢失的-持久化 Redis 是如何实现服务高可用的-数据复制 redis内存淘汰策略
作者其他创作
大纲/内容
为啥Redis单线程模型也能效率这么高?
1)纯内存操作
Redis 将所有数据放在内存中,内存的响应时长大约为 100 纳秒,这是 redis 的 QPS 过万的重要基础。
Redis 将所有数据放在内存中,内存的响应时长大约为 100 纳秒,这是 redis 的 QPS 过万的重要基础。
2)核心是基于非阻塞的IO多路复用机制 就是select/epoll 机制。
有了非阻塞 IO 意味着线程在读写 IO 时可以不必再阻塞了,读写可以瞬间完成然后线程可以继续干别的事了。
redis 需要处理多个 IO 请求,同时把每个请求的结果返回给客户端。由于 redis 是单线程模型,同一时间只能处理一个 IO 事件,于是 redis 需要在合适的时间暂停对某个 IO 事件的处理,转而去处理另一个 IO 事件,这就需要用到IO多路复用技术了, 就好比一个管理者,能够管理个socket的IO事件,当选择了哪个socket,就处理哪个socket上的 IO 事件,其他 IO 事件就暂停处理了
有了非阻塞 IO 意味着线程在读写 IO 时可以不必再阻塞了,读写可以瞬间完成然后线程可以继续干别的事了。
redis 需要处理多个 IO 请求,同时把每个请求的结果返回给客户端。由于 redis 是单线程模型,同一时间只能处理一个 IO 事件,于是 redis 需要在合适的时间暂停对某个 IO 事件的处理,转而去处理另一个 IO 事件,这就需要用到IO多路复用技术了, 就好比一个管理者,能够管理个socket的IO事件,当选择了哪个socket,就处理哪个socket上的 IO 事件,其他 IO 事件就暂停处理了
3)单线程反而避免了多线程的频繁上下文切换带来的性能问题
第一,单线程可以简化数据结构和算法的实现
第二,单线程避免了线程切换和竞态产生的消耗,对于服务端开发来说,锁和线程切换通常是性能杀手。
单线程的问题:对于每个命令的执行时间是有要求的。如果某个命令执行过长,会造成其他命令的阻塞,所以 redis 适用于那些需要快速执行的场景。
第一,单线程可以简化数据结构和算法的实现
第二,单线程避免了线程切换和竞态产生的消耗,对于服务端开发来说,锁和线程切换通常是性能杀手。
单线程的问题:对于每个命令的执行时间是有要求的。如果某个命令执行过长,会造成其他命令的阻塞,所以 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发起请求的时候(不管是读请求还是写请求,都一样),首先就会在 Socket 产生一个 AE_READABLE 事件,然后由对应的命令请求处理器来处理。这个命令请求处理器就会从Socket中读取请求相关数据,然后进行执行和处理。
接着Redis这边准备好了给客户端的响应数据之后,就会将Socket的AE_WRITABLE事件跟命令回复处理器关联起来,当客户端这边准备好读取响应数据时,就会在 Socket 上产生一个 AE_WRITABLE 事件,会由对应的命令回复处理器来处理,就是将准备好的响应数据写入 Socket,供客户端来读取。
命令回复处理器写完之后,就会删除这个 Socket 的 AE_WRITABLE 事件和命令回复处理器的关联关系
Redis 6.0 之后为什么引入了多线程?
虽然 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 线程)
所以为了提高网络请求处理的并行度,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-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 是如何实现数据不丢失的-持久化
AOF 日志(Append Only File,文件追加方式):记录所有的操作命令,并以文本的形式追加到文件中。
RDB 快照(Redis DataBase):将某一个时刻的内存数据,以二进制的方式写入磁盘。
混合持久化方式:Redis 4.0 新增了混合持久化的方式,集成了 RDB 和 AOF 的优点
RDB 快照(Redis DataBase):将某一个时刻的内存数据,以二进制的方式写入磁盘。
混合持久化方式:Redis 4.0 新增了混合持久化的方式,集成了 RDB 和 AOF 的优点
Reids 为什么先执行命令,再把数据写入日志呢?AOF
因为 ,Redis 在写入日志之前,不对命令进行语法检查;
所以,只记录执行成功的命令,避免了出现记录错误命令的情况;
并且,在命令执行完之后再记录,不会阻塞当前的写操作。
当然,这样做也会带来风险
数据可能会丢失:如果 Redis 刚执行完命令,此时发生故障宕机,会导致这条命令存在丢失的风险。
可能阻塞其他操作:虽然 AOF 是写后日志,避免阻塞当前命令的执行,但因为 AOF 日志也是在主线程中执行,所以当 Redis 把日志文件写入磁盘的时候,还是会阻塞后续的操作无法执行。
所以,只记录执行成功的命令,避免了出现记录错误命令的情况;
并且,在命令执行完之后再记录,不会阻塞当前的写操作。
当然,这样做也会带来风险
数据可能会丢失:如果 Redis 刚执行完命令,此时发生故障宕机,会导致这条命令存在丢失的风险。
可能阻塞其他操作:虽然 AOF 是写后日志,避免阻塞当前命令的执行,但因为 AOF 日志也是在主线程中执行,所以当 Redis 把日志文件写入磁盘的时候,还是会阻塞后续的操作无法执行。
RDB 快照是如何实现的呢?
因为 AOF 日志记录的是操作命令,不是实际的数据,所以用 AOF 方法做故障恢复时,需要全量把日志都执行一遍,一旦日志非常多,势必会造成 Redis 的恢复操作缓慢。
为了解决这个问题,Redis 增加了 RDB 内存快照(所谓内存快照,就是将内存中的某一时刻状态以数据的形式记录在磁盘中)的操作,它即可以保证可靠性,又能在宕机时实现快速恢复。
和 AOF 不同的是,RDB 记录 Redis 某一时刻的数据,而不是操作,所以在做数据恢复时候,只需要直接把 RDB 文件读入内存,完成快速恢复
为了解决这个问题,Redis 增加了 RDB 内存快照(所谓内存快照,就是将内存中的某一时刻状态以数据的形式记录在磁盘中)的操作,它即可以保证可靠性,又能在宕机时实现快速恢复。
和 AOF 不同的是,RDB 记录 Redis 某一时刻的数据,而不是操作,所以在做数据恢复时候,只需要直接把 RDB 文件读入内存,完成快速恢复
RDB 做快照时会阻塞线程吗?
因为 Redis 的单线程模型决定了它所有操作都要尽量避免阻塞主线程,所以对于 RDB 快照也不例外,这关系到是否会降低 Redis 的性能。
为了解决这个问题,Redis 提供了两个命令来生成 RDB 快照文件,分别是 save 和 bgsave。save 命令在主线程中执行,会导致阻塞。而 bgsave 命令则会创建一个子进程,用于写入 RDB 文件的操作,避免了对主线程的阻塞,这也是 Redis RDB 的默认配置
为了解决这个问题,Redis 提供了两个命令来生成 RDB 快照文件,分别是 save 和 bgsave。save 命令在主线程中执行,会导致阻塞。而 bgsave 命令则会创建一个子进程,用于写入 RDB 文件的操作,避免了对主线程的阻塞,这也是 Redis RDB 的默认配置
RDB 做快照的时候数据能修改吗?
原始数据是可以修改的,利用 bgsave 子进程
如果主线程执行读操作,则主线程和 bgsave 子进程互相不影响;
如果主线程执行写操作,则被修改的数据会复制一份副本(即副本数据也被修改了),然后 bgsave 子进程会把该副本数据写入 RDB 文件,在这个过程中,主线程仍然可以直接修改原来的数据。
如果主线程执行读操作,则主线程和 bgsave 子进程互相不影响;
如果主线程执行写操作,则被修改的数据会复制一份副本(即副本数据也被修改了),然后 bgsave 子进程会把该副本数据写入 RDB 文件,在这个过程中,主线程仍然可以直接修改原来的数据。
Redis 对 RDB 的执行频率非常重要,因为这会影响快照数据的完整性以及 Redis 的稳定性,所以在 Redis 4.0 后,增加了 AOF 和 RDB 混合的数据持久化机制:把数据以 RDB 的方式写入文件,再将后续的操作命令以 AOF 的格式存入文件,既保证了 Redis 重启速度,又降低数据丢失风险
Redis 是如何实现服务高可用的-数据复制
主从同步 (主从复制)
实现方案就是将从前的一台 Redis 服务器,同步数据到多台从 Redis 服务器上,即一主多从的模式,这样我们就可以对 Redis 做读写分离了,来承载更多的并发操作,因为多节点只是master的备份所以这种方案会受限于master节点,所以一般不建议用
Redis Sentinel(哨兵模式)
哨兵模式做到了可以监控主从服务器,并且提供自动容灾恢复的功能
Redis Cluster(集群)
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 命令,指定每个实例上的哈希槽个数
根据键值对的 key,按照 CRC16 算法计算一个 16 bit 的值。
再用 16bit 值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽。
剩下的一个问题就是,这些哈希槽怎么被映射到具体的 Redis 实例上的呢?有两种方案。
平均分配:在使用 cluster create 命令创建 Redis 集群时,Redis 会自动把所有哈希槽平均分布到集群实例上。比如集群中有 9 个实例,则每个实例上槽的个数为 16384/9 个。
手动分配:可以使用 cluster meet 命令手动建立实例间的连接,组成集群,再使用 cluster addslots 命令,指定每个实例上的哈希槽个数
redis内存淘汰策略
当redis内存超过maxmemory限定时,触发主动清除策略,
redis4.0之前有6种策略,后面版本增加到8种
redis4.0之前有6种策略,后面版本增加到8种
a)针对设置了过期时间的key
1. 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只响应读操作。
1. 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为什么这么快?
高效的数据结构
Mysql索引为了提高效率,选择了B+树的数据结构。其实合理的数据结构,就是可以让你的应用/程序更快。
合理的线程模型
I/O 多路复用
多路I/O复用技术可以让单个线程高效的处理多个连接请求
单线程模型
Redis是单线程模型的,而单线程避免了CPU不必要的上下文切换和竞争锁的消耗。
虚拟内存机制
Redis直接自己构建了VM机制 ,不会像一般的系统会调用系统函数处理,会浪费一定的时间去移动和请求。
Redis的虚拟内存机制是啥呢?
虚拟内存机制就是暂时把不经常访问的数据(冷数据)从内存交换到磁盘中,从而腾出宝贵的内存空间用于其它需要访问的数据(热数据)。通过VM功能可以实现冷热数据分离,使热数据仍在内存中、冷数据保存到磁盘。这样就可以避免因为内存不足而造成访问速度下降的问题。
基于内存存储实现
内存读写是比在磁盘快很多的,Redis基于内存存储实现的数据库,相对于数据存在磁盘的MySQL数据库,省去磁盘I/O的消耗。
内存读写是比在磁盘快很多的,Redis基于内存存储实现的数据库,相对于数据存在磁盘的MySQL数据库,省去磁盘I/O的消耗。
高效的数据结构
Mysql索引为了提高效率,选择了B+树的数据结构。其实合理的数据结构,就是可以让你的应用/程序更快。
合理的数据编码
Redis 支持多种数据数据类型,每种基本类型,可能对多种数据结构。什么时候,使用什么样数据结构,使用什么样编码,是redis设计者总结优化的结果。
Redis 支持多种数据数据类型,每种基本类型,可能对多种数据结构。什么时候,使用什么样数据结构,使用什么样编码,是redis设计者总结优化的结果。
合理的线程模型
I/O 多路复用
多路I/O复用技术可以让单个线程高效的处理多个连接请求
单线程模型
Redis是单线程模型的,而单线程避免了CPU不必要的上下文切换和竞争锁的消耗。
虚拟内存机制
Redis直接自己构建了VM机制 ,不会像一般的系统会调用系统函数处理,会浪费一定的时间去移动和请求。
Redis的虚拟内存机制是啥呢?
虚拟内存机制就是暂时把不经常访问的数据(冷数据)从内存交换到磁盘中,从而腾出宝贵的内存空间用于其它需要访问的数据(热数据)。通过VM功能可以实现冷热数据分离,使热数据仍在内存中、冷数据保存到磁盘。这样就可以避免因为内存不足而造成访问速度下降的问题。
Redis数据结构
字符串SDS
简单动态字符串(simple dynamic string,简称SDS)
提前分配
惰性释放
二进制安全
兼容C字符函数
由于Redis数据库的特性,会频繁的增删查改,保存一些二进制数据,而原来的C字符串并不能高性能的完成这些事,所以Redis才自己封装了SDS
listNode 基础双向链表
pre,next,val
pre,next,val
链表 list
虽然使用多个listnode便可以组成链表的话,但Redis使用list结构来持有链表,操作更加方便
head,tail,dup,free,match,len
head,tail,dup,free,match,len
字典 dict
哈希表
字典结构
type,privdata,ht[2],rehashidx
ht[2]两个哈希表是为了rehash而设计的,
一般只使用ht[0]这个哈希表,ht[1]只会在对ht[0]进行rehash的时候使用
ht[2]两个哈希表是为了rehash而设计的,
一般只使用ht[0]这个哈希表,ht[1]只会在对ht[0]进行rehash的时候使用
rehashidx它记录了目前rehash的进度,为-1时则说明不进行rehash
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 4步
1.为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
2.在字典中维持一个rehashidx,也就是上面的字典结构的属性,将其值设置为0,表示rehash工作开始
3.在rehash期间,程序除了执行指定的操作外,还会将索引为rehashidx的数据移动到ht[1]相当于将ht[0]里的数据删除,在ht[1]里面增加,
当rehashidx这个索引的数据全部移动完成时,则将rehashidx值加1,直到全部完成
当rehashidx这个索引的数据全部移动完成时,则将rehashidx值加1,直到全部完成
4.完成后,将rehashidx的值表示为-1,并将ht[1]设置为ht[0].
在渐进式rehash期间,字典进行的删除,更新,查找会在两张哈希表上进行。比如查找,redis会先在ht[0]查找,找不到才会到ht[1]上面查找
而字典进行的插入操作,则只会在ht[1]表里执行。这样的话,ht[0]表里的数量只减 不增,也减少了重复插入的操作
而字典进行的插入操作,则只会在ht[1]表里执行。这样的话,ht[0]表里的数量只减 不增,也减少了重复插入的操作
哈希算法
ht[x] 可以是ht[0] 或者ht[1]
index = hash & dict->ht[x].sizemask;
index = hash & dict->ht[x].sizemask;
hash = dict->type->hashFunction(key);
采用MurmurHash2算法来计算键的哈希值,这种算法最大的优点就是当输入有规律的数时,也能平均散列到数组中
跳跃表 zskiplist
header, tail,len,level
一个跳跃表有多个跳跃表节点。通过zskiplist来持有这些节点。
整数集合 intset
整数集合是Redis用来保存整数值的集合,保证集合中不会出现重复的元素
数组升级(一旦升级就不会降级)
当对数组中添加新元素时,如果新添加的元素类型大于原来的数组的类型,则需要对数组进行升级。
数组升级的好处
1. 更加方便
C语言中要保存两种不同类型的元素就必须使用两个类型数组来保存
而Redis则使用数组升级来避免了使用两个数组,更加方便,且不用担心类型错误
2. 节约内存
要让一个数组保存不同的类型,最简单就是直接定义一个最大的数组类型,但是这样会占用不必要的空间
而Redis则只是在必要的时候才升级数组,尽量节约了内存
压缩列表 ziplist
zlbytes,zltail,zllen,entry1,..,entryN,zlend
entry{previous entry_length,encoding,content}
entry{previous entry_length,encoding,content}
previous_entry_length记录了前一个节点的长度,低于254=1字节,>=254=5字节,
这有连锁更新问题
这有连锁更新问题
快速列表 quicklist
quicklist由多个快速列表节点构成的双向链表,每一个快速列表节点都保存一个ziplist压缩列表
在快速列表中,两端节点的数据被访问的可能性比较高,中间访问的可能性比较低。如果符合这种场景的话,就可以把中间节点的数据用LZF算法进行压缩,进一步节省空间。
压缩配置参数list-compress-depth 默认0不压缩
Redis数据类型
字符串 string
int:存储8个字节的长整型(long,2的63次方-1),那么字符串对象会将整数值保存在字符串里的ptr属性里面,将void*转换为long,并将字符串编码设置为int
embstr:代表embstr格式的SDS,用来存储小于等于44字节的字符串;
raw:存储大于44字节的字符串
Redis浮点数也是用字符串值来表示的,需要用时先把它转为浮点数,计算完后又转为字符串值保存
embstr:代表embstr格式的SDS,用来存储小于等于44字节的字符串;
raw:存储大于44字节的字符串
Redis浮点数也是用字符串值来表示的,需要用时先把它转为浮点数,计算完后又转为字符串值保存
其中:embstr和raw都是由SDS动态字符串构成的。唯一区别是:raw是分配内存的时候,redisobject和 sds 各分配一块内存,而embstr是redisobject和raw在一块儿内存中。
对于string 数据类型,因为string 类型是二进制安全的,可以用来存放图片,视频等内容,另外由于Redis的高性能读写功能,而string类型的value也可以是数字,可以用作计数器(INCR,DECR),比如分布式环境中统计系统的在线人数,秒杀等。
列表 list
底层quicklist快速列表
对于 list 数据类型,可以实现简单的消息队列,另外可以利用lrange命令,做基于redis的分页功能
Stack(栈)= LPUSH + LPOP
Queue(队列)= LPUSH + RPOP
Blocking MQ(阻塞队列)= LPUSH + BRPOP
Stack(栈)= LPUSH + LPOP
Queue(队列)= LPUSH + RPOP
Blocking MQ(阻塞队列)= LPUSH + BRPOP
哈希 hash
当键值对的长度都小于64字节,且键值对数量小于512个时,使用ziplist,否则使用hashtable
对于 hash 数据类型,value 存放的是键值对,比如可以做单点登录存放用户信息,购物车
集合 set
集合对象底层可以是intset或者hashtable,如果集合对象全为整数,且数量小于等于512个,则使用intset;否则使用hashtable
对于 set 数据类型,由于底层是字典实现的,查找元素特别快,另外set 数据类型不允许重复,利用这两个特性我们可以进行全局去重,比如在用户注册模块,判断用户名是否注册;另外就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能
有序集合 zset
有序集合对象使用ziplist或者skiplist来实现,当集合数量小于128且字符长度小于等于64字节时使用ziplist;否则使用skiplist
当底层使用skiplist实现:这种有序集合对象使用zset结构作为底层实现,
为什么要同时使用跳跃表和字典呢??
为什么要使用跳跃表,而不是用平衡树?
跳跃表更加简单,使用范围查找比其他的平衡树效率要高;
容易实现容易调试的;跳表插入和删除只要维护节点指针即可,不需要调整树。
跳跃表更加简单,使用范围查找比其他的平衡树效率要高;
容易实现容易调试的;跳表插入和删除只要维护节点指针即可,不需要调整树。
对于 zset 数据类型,有序的集合,可以做范围查找,排行榜应用,取 TOP N 操作等
一个zset结构包含一个字典和一个跳跃表
bitmap布隆过滤器
优点
相比于其它的数据结构,布隆过滤器在空间和时间方面都有巨大的优势。布隆过滤器存储空间和插入/查询时间都是常数(即hash函数的个数)
Hash 函数相互之间没有关系,方便由硬件并行实现
布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势
布隆过滤器可以表示全集,其它任何数据结构都不能。
缺点
但是布隆过滤器的缺点和优点一样明显:
误算率(False Positive)是其中之一。随着存入的元素数量增加,误算率随之增加(误判补救方法是:再建立一个小的白名单,存储那些可能被误判的信息)。但是如果元素数量太少,则使用散列表足矣。
一般情况下不能从布隆过滤器中删除元素。我们很容易想到把位列阵变成整数数组,每插入一个元素相应的计数器加 1, 这样删除元素时将计数器减掉就可以了。然而要保证安全的删除元素并非如此简单。首先我们必须保证删除的元素的确在布隆过滤器里面. 这一点单凭这个过滤器是无法保证的。另外计数器回绕也会造成问题。
相比于其它的数据结构,布隆过滤器在空间和时间方面都有巨大的优势。布隆过滤器存储空间和插入/查询时间都是常数(即hash函数的个数)
Hash 函数相互之间没有关系,方便由硬件并行实现
布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势
布隆过滤器可以表示全集,其它任何数据结构都不能。
缺点
但是布隆过滤器的缺点和优点一样明显:
误算率(False Positive)是其中之一。随着存入的元素数量增加,误算率随之增加(误判补救方法是:再建立一个小的白名单,存储那些可能被误判的信息)。但是如果元素数量太少,则使用散列表足矣。
一般情况下不能从布隆过滤器中删除元素。我们很容易想到把位列阵变成整数数组,每插入一个元素相应的计数器加 1, 这样删除元素时将计数器减掉就可以了。然而要保证安全的删除元素并非如此简单。首先我们必须保证删除的元素的确在布隆过滤器里面. 这一点单凭这个过滤器是无法保证的。另外计数器回绕也会造成问题。
GeoHash坐标,借助Sorted Set实现,通过zset的score进行排序就可得到坐标附近的其他元素,通过将score还原成坐标值就可以得到元素的原始坐标
HyperLogLog统计不重复数据,用于人数据基数统计
常用于大数据量的统计,比如页面访问量统计或者用户访问量统计。
①需求:要统计一个页面的访问量(PV)
①方案:直接用redis计数器或者直接存数据库都可以
②需求:要统计一个页面的用户访问量(UV),即:一个用户一天内如果访问多次的话,也只能算一次
②方案:可能会想到用SET集合来做,因为SET集合是有去重功能的,key存储页面对应的关键字,value存储对应userId
③需求:假如有几千万访问量,为了统计一个访问量,要频繁创建SET集合对象。
③方案:针对大访问量需要进行统计的问题,redis实现了一种HyperLogLog算法。
①需求:要统计一个页面的访问量(PV)
①方案:直接用redis计数器或者直接存数据库都可以
②需求:要统计一个页面的用户访问量(UV),即:一个用户一天内如果访问多次的话,也只能算一次
②方案:可能会想到用SET集合来做,因为SET集合是有去重功能的,key存储页面对应的关键字,value存储对应userId
③需求:假如有几千万访问量,为了统计一个访问量,要频繁创建SET集合对象。
③方案:针对大访问量需要进行统计的问题,redis实现了一种HyperLogLog算法。
Streams 内存版的kafka
Redis内存回收机制
由于C语言并没有自动的内存回收机制,在redisObject结构中有一个refcount引用计数属性,当该值为0,也就是该对象不再被其他所引用时,就会释放内存。
Redis对象共享
可以让多个键共用一个值对象,只需要将指针指向它,并且将该值对象的引用计数+1便可以。
注意这里只对整数值的字符串对象进行共享。如果对包含字符串值的对象进行共享,那么需要把两个字符串遍历一遍,时间复杂度为O(N),如果包含多个值的列表或者对象,那么时间复杂度为O(N^2).而整数值只需要转换完比较。所以为了时间效率,Redis只共享整数值的字符串
注意这里只对整数值的字符串对象进行共享。如果对包含字符串值的对象进行共享,那么需要把两个字符串遍历一遍,时间复杂度为O(N),如果包含多个值的列表或者对象,那么时间复杂度为O(N^2).而整数值只需要转换完比较。所以为了时间效率,Redis只共享整数值的字符串
Redis对象的空转时长
redisObject还有一个属性lru:LRU_BITS:记录了该对象最后一次被访问的时间。
使用 object idletime命令便可以打印对象的空转时长:当前时间减去对象的lru属性值
redis的线程模型
基于Reactor模式开发了网络事件处理器
单reactor单线程模型
1)文件事件处理器
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 当前产生的事件,来选择对应的事件处理器来处理。
如果被监听的 Socket 准备好执行accept、read、write、close等操作的时候,跟操作对应的文件事件就会产生,这个时候文件事件处理器就会调用之前关联好的事件处理器来处理这个事件。
文件事件处理器是单线程模式运行的,但是通过IO多路复用机制监听多个Socket,可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了 Redis 内部的线程模型的简单性。
文件事件处理器的结构包含4个部分:多个Socket、IO多路复用程序、文件事件分派器以及事件处理器(连接应答处理器(建立连接)、命令请求处理器(写)、命令回复处理器(读))。
多个 Socket 可能并发的产生不同的操作,每个操作对应不同的文件事件,但是IO多路复用程序会监听多个 Socket,会将 Socket 放入一个队列中排队,每次从队列中取出一个 Socket 给事件分派器,事件分派器把 Socket 给对应的事件处理器。
然后一个 Socket 的事件处理完之后,IO多路复用程序才会将队列中的下一个 Socket 给事件分派器。文件事件分派器会根据每个 Socket 当前产生的事件,来选择对应的事件处理器来处理。
2)文件事件
当 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 事件
当 Socket 变得可写的时候(客户端对redis执行read操作),Socket 会产生一个AE_WRITABLE事件。
IO 多路复用程序可以同时监听 AE_REABLE 和 AE_WRITABLE 两种事件,如果一个Socket同时产生了这两种事件,那么文件事件分派器优先处理 AE_READABLE 事件,然后才是 AE_WRITABLE 事件
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 在 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)不停轮询这个队列,拿出任务就去执行对应的方法即可
后台线程相当于一个消费者,生产者把耗时任务丢到任务队列中,消费者(BIO)不停轮询这个队列,拿出任务就去执行对应的方法即可
收藏
收藏
0 条评论
下一页