文章目录
前言
redis八股文总结,后面有待补充
提示:以下是本篇文章正文内容,下面案例可供参考
1、什么是redis
redis是一种基于内存
的数据库,对数据的读写操作都是在内存中完成的,因此读写速度非常快
,常用于缓存、消息队列、分布式锁等场景
。
redis提供了多种数据类型来支持不同的业务场景,比如string(字符串)、hash(哈希)、List(列表)、set(集合)、Zset(有序集合)、Bitmaps(位图)、HyperLogLog(基数统计)、GEO(地理信息)、Stream(流),并且对数据类型的操作都是原子性
的,因为redis执行命令是单线程,不存在并发竞争的问题
为什么用redis作为mysql的缓存
主要是因为redis具备高性能和高并发两种特性
- redis具备高性能
用户第一次访问mysql中的某些数据,因为是从硬盘上读取的,所以过程会比较慢。将该用户访问的数据缓存在redis中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了,操作redis就是直接操作内存
,不用访问硬盘,所以速度相当快。
由于mysql数据改变后,需要同步改变redis缓存中的数据,不过这里会有redis和mysql双写一致性的问题。 - redis具备高并发
单台设备的redis的QPS(每秒钟处理完请求的次数)是mysql的十倍,所以redis能够承受的请求是远远大于mysql的,因此可以考虑把数据库中的部分数据转移到缓存中去。
2、redis数据结构
2.1 五种类型是什么及使用场景
redis提供了丰富的数据类型,常见的有五种:String(字符串)、Hash(哈希)、List(列表)、Set(集合)、Zset(有序集合)
redis五种数据类型的应用场景:
- String类型的应用场景:缓存对象、常规计数、分布式锁、共享session信息等。
- List类型的应用场景:消息队列(2 problems:
生产者需要自行实现全局唯一ID
不能以消费组形式消费数据
- Hash类型:缓存对象、购物车
- set类型:集合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
- Zset类型:排序场景,比如排行榜、电话和姓名排序等 。
redis后续又支持四种数据类型:
- BitMap:二值状态统计的场景,比如签到、判断用户登录状态、连续签到用户总数等。
- HyperLogLog:海量数据基数统计的场景,比如百万级网页UV计数等。
- GEO:存储地理位置信息的场景,比如滴滴叫车
- Stream:消息队列,相比于基于List类型实现的消息队列,可以自动生成全局唯一消息ID,支持以消费组形式消费数据。
2.2 五种常见数据类型怎么实现
String类型内部实现
string类型底层数据结构使用简单动态字符串(SDS),SDS相比于C的原生字符串:
- SDS不仅可以保存文本数据,还可以保存二进制数据(即能保存图片、音频、视频、压缩文件)
- SDS获取字符串长度的时间复杂度是O(1),用len属性记录了字符串长度,复杂度为O(1)
- Redis的SDS API 是安全的,拼接字符串不会造成缓冲区溢出。
String类型的使用场景:
- 常规计数,redis执行命令是单线程的,所以执行命令的过程是原子的。因此String类型适合用于计算访问次数、点赞、转发、库存数量等。
- 分布式锁,setNx,如果key不存在才插入,即
- 如果key不存在,则显示插入成功,可以用来表示加锁成功
- 如果key存在,会显示插入失败,即可用来表示加锁失败
一般而言,还会对分布式锁设置过期时间,防止某一线程崩溃,锁一直无法释放
- 共享session信息
在分布式系统中,借助redis对session信息进行统一的存储和管理,这样无论请求发送到哪台服务器,服务器都会去同一个redis获取相关的session信息,这就解决了分布式系统下session存储的问题。
List类型内部实现
双向链表或压缩列表
实现,简单的字符串列表,按照插入顺序排序
元素较少,使用压缩列表(连锁更新情况保存前一个entry的长度,比较耗性能);元素较多使用双向链表;后期改为quickList,其实就是双向链表+压缩链表。
quickList
应用场景:消息队列
消息队列有三大需求:消息保序(list先进先出保证
)、处理重复的消息(每个消息都会有个全局的id,消费者记录已处理过的消息id,来判断当前收到的消息有没有经过处理,如果处理过,消费者程序就不再进行处理
)和保证消息可靠性(BRPOPLPUSH
命令,让消费者程序从一个list读取消息,同时,redis将这个消息插入到另一个list(备份list)中留存
)
List作为消息队列,有缺陷:
list不支持多个消费者消费同一条消息,即不支持消费组的实现
Hash内部实现
压缩列表或者哈希表
实现,redis7后压缩表列表舍弃,交由listpack数据结构实现(不存储上一个节点的长度,不出现连锁更新问题)。元素个数较少,使用压缩列表(listpack),较多使用哈希表
应用场景
- 缓存对象
- 购物车
set内部实现
用哈希表或整数集合
来实现
元素个数较少,使用整数集合;较多使用哈希表。
set集合的几个特性:无序、不可重复、支持并交差
应用场景:
- 点赞 (一篇文章有多少用户点赞,去重)
- 共同关注
- 抽奖活动(去重,保证一个用户不会中奖两次)
zset内部实现
小:压缩列表(listpack),多:跳表
有序集合
使用场景
- 排行榜
- 电话、姓名排序
Bitmap
位图,可以通过偏移量(offset)定位元素,即可以将最小的单位bit来进行0/1的设置,表示某个元素的值或者状态,特别适合一些数据量大且使用二值统计的场景
,用String类型作为底层数据结构
应用场景
- 签到统计
- 判断用户登录状态
- 连续签到用户总数
hyperLogLog
统计基数的数据集合类型,提供不精确的去重计数
,用很小的内存空间,计算出很多元素的基数
使用场景
百万级网页UV计数
GEO
存储地理位置信息,并对存储的信息进行操作,使用数据类型为zset,sorted set
应用场景
滴滴叫车
Stream
消息队列,在list基础上增加了消费组的实现。
2.3 redis线程模型
2.3.1 redis是单线程嘛
redis单线程是指[接受客户端请求 -> 解析请求 -> 进行数据读写操作 -> 发送数据给客户端]这个过程是由一个线程来完成的
redis程序并不是单线程的
,redis在启动的时候,会启动后台线程
的。
redis会为很耗时的任务创建单独的线程来处理,如关闭文件、AOF刷盘、释放内存
注意redis的单线程只局限于网络IO和执行命令
2.3.2 redis采用单线程为什么还这么快
有如下几个原因:
- redis的大部分操作
都在内存中完成
,并且采用了高效的数据结构,因此redis的瓶颈可能是机器的内存或者网络带宽,而不是CPU。 - redis采用单线程模型可以
避免多线程之间的竞争
- redis采用了
I/O多路复用机制
处理大量的客户端socket请求,该机制允许内核中存在多个监听socket和已连接socket。内核会一直监听socket上的连接请求或数据请求,一旦请求到达,就会交给redis线程处理,即实现一个redis线程处理多个IO流的效果。
2.3.3redis6.0之前为什么使用单线程
核心的原因就是CPU不是制约redis性能表现的瓶颈所在
,如果想使用服务的多核CPU,可以在一台服务器上启动多个节点或采用分片集群的方式。(即启动多个redis节点,每个都是单线程,这样也形成多线程)
多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加系统复杂度,同时可能存在线程切换、甚至加锁解锁、死锁造成的内存损耗
2.3.4redis6.0之后为什么引入了多线程
虽然redis的网络IO和执行命令一直是单线程模型,但是在redis 6.0之后,也采用了多个IO线程来处理网络请求,这是因为随着网络硬件的性能提升,redis的性能瓶颈有时会出现在网络IO处理上
因此为了提高网络I/O的并行度,redis 6.0对网络IO采用多线程处理,对于命令的执行,仍然采用单线程来处理
,redis 6.0版本引入的多线程I/O特性对性能提升至少一倍以上
2.4 redis持久化
2.4.1redis如何实现数据不丢失
redis的读写操作都是在内存中,所以redis性能才会高,但是当redis重启后,内存中的数据就会丢失,那为了保证内存中的数据不会丢失,redis实现了数据持久化的机制,这个机制会把数据存储到磁盘,这样redis重启就能够从磁盘中恢复原有的数据。
redis共有三种数据持久化的方式:
- AOF日志:每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里
- RDB快照:将某一时刻的内存数据,以二进制的方式写入磁盘
- 混合持久化方式 :redis4.0新增的方式,集成了AOF和RDB的优点
2.4.2 AOF日志是如何实现的
redis在执行完一条写操作命令后,就会把该命令以追加
的方式写入到一个文件里,然后redis重启后,会读取该文件记录的命令,然后逐一执行命令
的方式进行数据恢复。
为什么先执行命令,再把数据写入日志呢
避免额外的检查开销
,确保记录进AOF的命令是正确的不会阻塞当前写操作命令的执行
。
也会有风险:
数据可能丢失
,写操作命令和记录日志是两个过程,当redis还没来得及将命令写入硬盘时,服务器宕机了,这个数据就有丢失的风险。可能阻塞其他操作
,因为AOF也是在主线程中执行,所以当redis把日志文件写入磁盘时,会阻塞后续的操作无法执行。
2.4.3 RDB快照如何实现的呢?
因为AOF日志记录的是操作命令,不是实际的数据,Redis增加了RDB快照,RDB快照就是记录某一个瞬间的内存数据,记录的是实际数据,AOF文件记录的是命令操作的日志,而不是实际的数据。
RDB做快照时会阻塞线程吗
Redis提供了两个命令来生成RDB文件,分别是save和bgsave,他们的区别就在于是否在主线程
里执行:
- 执行了save命令,就会在主线程生成RDB文件,由于和执行操作命令在同一个线程,所以如果写入RDB文件的时间太长,
会阻塞主线程
; - 执行了bgsave命令,会创建一个子进程来生成RDB文件,这样可以
避免主线程的阻塞
。
redis还可以通过配置文件的选项来实现每隔一段时间自动执行一次bgsave命令,默认会提供以下配置:
2.4.4 为什么会有混合持久化
RDB优点是数据恢复速度快,但是快照的频率不好把握。频率太低,丢失的数据就会比较多,频率太高,就会影响性能。
AOF优点是丢失数据少,但是数据恢复不快。
redis 4.0提出了混合使用AOF日志和内存快照
,即简单的来说就是AOF文件的前半部分是RDB格式的全量数据,后半部分是AOF格式的增量数据
2.5 redis集群
2.5.1 redis如何实现服务高可用
主从复制
主从服用是redis高可用服务的最基础的保证,实现方案就是将从前的一台redis服务器,同步数据到多台redis服务器上,即一主多从的模式,且主从服务器之间采用的是读写分离
的方式。
主服务器可以进行读写操作对客户端来说
,但发生写操作时自动将写操作同步给从服务器,而从服务器一般是只读对客户端来说
,接受主服务器同步过来写操作命令,然后执行这条命令。
主从服务器之间的命令复制是异步
执行的,具体来说,在主从服务器命令传播阶段,主服务器收到新的命令后,会发送给从服务器。但是主服务器不会等到从服务器实际执行完命令后,再把结果返回给客户端,而是主服务器自己在本地执行完命令后,就会向客户端返回结果。如果从服务器没有执行主服务器同步过来的命令,主从服务器之间就不一致了。
哨兵模式
使用redis主从服务的时候,当redis的主从服务器出现故障宕机时,需要手动进行恢复。为了解决这个问题,redis增加了哨兵模式
,因为哨兵模式做到了可以监控主从服务器,并且提供主从节点故障转移的功能
切片集群模式
redis缓存数据量
大到一台服务器无法缓存时,需要使用redis切片集群
,它将数据分布在不同的服务器上,以此来降低系统对但主节点的依赖,从而提高redis服务的读写性能。
redis集群采用哈希槽,一个切片集群共有16384个哈希槽
,每个键值对都会根据它的key,被映射到一个哈希槽中。
然后哈希槽会映射到具体的redis节点,有两种方案:
平均分配
手动分配
2.5.2 集群脑裂导致数据丢失怎么办?
脑裂
即由于网络问题,集群节点间失去联系。主从数据不同步,重新平衡选举,产生两个主服务。等网络恢复,旧主节点就会降级为从节点,再与新主节点进行同步复制时,由于从节点会清空自己的缓冲区,导致之前客户端写入的数据丢失了。
解决方案
- min-slaves-to-write x,主节点必须要有至少x个从节点连接,如果少于这个数,主节点就会禁止写数据
- min-slaves-max-lag x,主从数据复制和同步的延迟不能超过x秒,如果超过,主节点会禁止写数据
2.6 redis过期删除和内存淘汰
2.6.1 redis使用的过期删除策略是什么
每当我们对一个key设置了过期时间,redis就会把该key带上过期时间存储到一个过期字典
,也就是过期字典
保存了数据库所有key的过期时间。
当我们查询一个key时,redis首先检查该key是否存在于过期字典中:
- 如果不在,则正常读取键值
- 如果存在,则会获取该key的过期时间,然后与当前系统时间进行比对,如果比系统时间大,那就没有过期,否则判定该key已过期。
redis使用的过期策略是惰性删除+定期删除
惰性删除策略
不主动删除过期键,每次从数据库访问key时,检测key是否过期,如果过期就删除该key
优点:每次访问,才会检查key是否过期,因此惰性删除策略对CPU时间最友好
缺点:如果一个key已经过期,但一直未被访问,则该key会一直保存在数据库中,会造成内存浪费,因此对内存不友好。
定期删除策略
每隔一段时间,随机从数据库中取出一定数量的key进行检查,并删除其中的过期key
,过期key超过一定比例,会重复执行定期删除,否则,直接结束,还会限制每次定期删除的时间上限。
优点:通过限制删除操作执行的时长和频率,减少删除操作对CPU的影响,同时也能删除一部分过期数据,减少过期键对空间的无效占用。
缺点:难以确定删除操作执行的时长和频率在CPU和内存之间找到妥协
2.6.2 redis持久化时,对过期键会如何处理
redis持久化两种方式,RDB和AOF
RDB分为文件生成阶段和加载阶段:
-
RDB文件生成阶段:过期的键不会被保存到新的RDB文件中。
-
RDB加载阶段:
- 如果redis是主服务器运行模式的话,载入RDB文件时,程序会对文件中保存的键进行检查,过期键不会被载入到数据库中。
- 如果redis是从服务器运行模式,载入RDB文件时,不论键是否过期都会被载入到数据库中。(但主从服务器在进行数据同步时,
从服务器数据会被清空
,所以过期键对载入RDB文件的从服务器也不会造成影响)
AOF文件分为两个阶段,AOF文件写入阶段和AOF重写阶段
AOF文件写入阶段:
如果数据库某个过期键还未被删除,那么AOF文件会保留此过期键,当此过期键被删除后,redis会向AOF文件追加一条DEL命令来显示地删除该键值。AOF重写阶段
:AOF重写时,会对redis的键值对进行检查,已过期的键不会被保存到重写后的AOF文件中
。
2.6.3 redis主从模式中,对过期键如何处理
redis运行在主从模式下时,从库不会进行过期扫描,从库对过期的处理是被动的,从库的过期键处理依靠主服务器控制,主库在key到期时,会在AOF文件里增加一条del指令,同步到所有的从库
,从库通过执行这条del指令来删除过期的key。
2.6.4 redis内存满了,会发生什么
在redis的运行内存达到了某个阈值,就会触发内存淘汰机制
,这个阈值就是设置的最大运行内存即配置项maxmemory。
2.6.5 redis内存淘汰策略有哪些
- 不进行数据淘汰的策略
noeviction:当运行内存超过最大设置内存时,不淘汰任何数据,而是不提供服务,直接返回错误 - 进行数据淘汰的策略
可进一步细分为在设置了过期时间的数据中进行淘汰
和在所有数据范围内进行淘汰
。
2.6.6 LRU算法和LFU算法的区别
LRU算法
LRU算法全称是least recently used翻译为最近最少使用
,会选择淘汰最近最少使用的数据。传统的LRU算法实现是基于链表
结构,链表中的元素按照操作顺序从前往后排列,最新操作的键会被移动到表头,当需要内存淘汰时,只需要删除链表尾部的元素即可,因为链表尾部的元素代表最久未被使用的元素。
传统的LRU算法存在两个问题:
- 需要用链表管理所有的缓存数据,这会带来额外的空间开销
- 当有数据被访问时,需要在链表上把该数据移动到头端,如果有大量数据被访问,就会带来很多链表移动操作,会很耗时,进而降低redis缓存性能。
redis是如何实现LRU算法的
redis实现的是一种近似LRU算法
,它的实现方式是在redis的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间。
当redis进行内存淘汰时,会使用随机采样的方式来淘汰数据
,它会随机取n个指,然后淘汰最久没有使用的那个
redis实现的LRU算法优点:
- 不用为所有的数据维护一个大链表,节省了空间占用
- 不用在每次数据访问时都移动链表项,提升缓存性能
但是LRU无法解决缓存污染问题
,比如应用一次读取了大量的数据,而这些数据只会被读取这一次,那么这些数据会留存在redis缓存中很长一段时间,造成缓存污染。
什么是LFU
LFU(least frequently used),最近最不常用的,LRU根据数据访问次数来淘汰数据的,核心思想是如果数据过去被访问多次,那么将来被访问的频率也更高
2.7 redis缓存设计
2.7.1 如何避免缓存雪崩、缓存击穿、缓存穿透
如何避免缓存雪崩
通常我们维克保证缓存中的数据与数据库中的数据一致性,会给redis里的数据设置过期时间,当缓存数据过期后,用户访问的数据如果不在缓存里,业务系统需要重新生成缓存,因此就会访问数据库,并将数据更新到redis里,这样后续请求都可以直接命中缓存。
但是当大量缓存数据在同一时间过期(失效)时,如果此时有大量的用户请求,都无法在redis中处理,那么全部请求就都直接访问数据库,从而导致数据库的压力骤增,严重的会使数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩
缓存雪崩问题采用两种方式解决
将缓存失效时间随机打散
设置缓存不过期
:后台服务来更新缓存数据
如何避免缓存击穿
业务中通常会有几个数据被频繁的访问,比如秒杀活动,这类被频繁访问的数据称为热点数据。
如果缓存中的某个热点数据过期了
,此时大量的请求访问了该热点数据,就无法从缓存中读取,从而直接访问数据库,数据库很容易被高并发的请求冲垮,这就是缓存击穿
问题。
解决方案:
- 互斥锁方案,保证同一时间只有一个业务线程请求缓存。
- 不给热点数据设置过期时间,即后台异步更新缓存重新设置过期时间。
如何避免缓存穿透
当发生缓存雪崩或者击穿时,数据库中还是保存了应用要访问的数据,一旦缓存恢复相对应的数据,就可以减轻数据库的压力,而缓存穿透是不一样的。
当用户访问的数据,既不在缓存中,也不在数据库中
,即请求缓存和数据库都找不到要访问的数,没办法构建缓存数据来服务后续的请求,当有大量的请求到来时,数据库压力骤增,即出现缓存穿透
缓存穿透一般发生在如下两种清空:
- 业务误操作
- 黑客恶意攻击
解决方式:
非法请求的限制
:需要在API入口处判断请求参数是否合理,是否含有非法值,如果判断出是恶意请求,则直接返回错误,避免进一步访问缓存和数据库设置空值或默认值
:针对查询的数据,在缓存中设置一个空值或默认值,返回给应用,从而不会继续查询数据库。使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在
:在写入数据库数据时,使用布隆过滤器来做个标记,可以在业务线程确认缓存失效后,通过查询布隆过滤器来快速判断数据是否存在,如果不存在,不用通过查询数据库来判断数据是否存在。即让请求只查询redis和布隆过滤器,不会查询数据库。
2.7.2 如何设计一个缓存策略,可以动态缓存热点数据
通过数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据
2.7.3常见的缓存更新策略
常见的共有三种:
- cache aside(旁路缓存)策略
- read/write through (读穿/写穿)策略
- write back (写回)策略
实际开发中,redis和mysql的更新操作使用的是cache aside,另外两种策略应用不了。
cache aside(旁路缓存)策略
cache aside策略是最常用的,应用程序直接与数据库、缓存
交互。并负责对缓存维护,该策略又可以细分为读策略
和写策略
写策略的步骤
:
- 先更新数据库中的数据,再
删除
缓存中的数据
读策略的步骤
:
- 如果读取的数据命中了缓存,则直接返回数据
- 如果读取的数据没有命中缓存,则从数据库中读取数据,然后将数据写入缓存,并且返回给用户
3、数据类型篇
3.1 SDS
为什么不用c字符串
c字符串的缺点:
- 获取字符串长度的时间复杂度是O(N)
即需要遍历到'\0'终止字符才可以得到长度
- 字符串的结尾是以’\0’字符标识,字符串里面不能含有“\0"字符,因此不能保存二进制数据
- 字符串操作函数不高效且不安全,比如有缓冲区溢出的风险,可能会造成程序运行终止
SDS结构设计
结构中的每个成员变量如下:
- len:记录了字符串长度(获取字符串长度时间复杂度为O(1))
- alloc:分配给字符数组的空间长度。因此可以通过
alloc - len
计算出剩余的空间大小,可以用来判断空间是否满足修改需求,如果不满足,则会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作即SDS会自动扩展空间
- flags:用来表示不同类型的SDS
- buf[]:字符数组,用来保存实际数据
3.2 链表
list结构为链表提供了链表头指针head,链表尾指针tail、链表节点数量len、以及可自定义实现的dup、free、match函数。
链表的优势和缺点
链表的优势
- listNode链表节点的结构里带有prev和next指针,
获取某个节点的前置节点或后置节点的时间复杂度只需O(1),且这两个指针都可以指向null,所以链表是无环链表
- list结构因为提供了表头指针head和表尾指针tail,所以
获取链表的表头节点和表尾节点的时间复杂度只需O(1)
- list节点因为提供了链表节点数量len,所以
获取链表节点数量的时间复杂度为O(1)
链表的缺陷
- 链表每个节点之间的内存都是不连续的,意味着
无法很好利用CPU缓存
- 保存一个链表节点的值都需要一个链表节点结构头的分配,
内存开销较大
3.3 压缩列表
redis中的list、hash和zset对象中包含的元素数量较少或者元素值不大时才会使用压缩列表作为底层数据结构
压缩列表是由连续内存块组成的顺序型数据结构
,压缩列表表头有三个字段:
zlbytes
:记录整个压缩链表占用对内存字节数zltail
:记录压缩列表尾部节点距离起始地址多少字节,也就是列表尾的偏移量zllen
,记录压缩列表包含的字节数量
尾部有字段zlen
,标记压缩列表的结束点,固定值0xFF
压缩列表中,查找定位第一个元素和最后一个元素,可以通过表头三个字段来直接定位,复杂度为O(1),查找其他元素时,就没有那么高效,只能逐个查找,此时的复杂度是O(N),因此压缩列表不适合保存过多的元素
压缩列表中每一个节点entry包含三部分内容:
prevlen
:记录前一个节点的长度encoding
:记录了当前节点实际数据
的类型(字符串和数组)和长度data
:记录当前节点的实际数据,类型和长度都由encoding
决定
prevlen属性记录了前一个节点的长度
- 如果
前一个节点的长度小于254字节
,那么prevlen需要用1字节的空间
来保存这个长度值 - 如果
前一个节点的长度大于等于254字节
,那么prevlen属性需要用5字节的空间
来保存这个长度值
压缩列表新增某个元素或修改某个元素时,如果空间不够,压缩列表占用的内存空间需要重新分配,而当新插入的元素较大时,可能导致后续元素的prevlen占用空间都发生变化,从而引起连锁更新问题,导致每个元素的空间都要重新分配,造成访问压缩列表性能的下降
假设有一种极端情况,有n个节点的长度都是253个字节,将一个长度大于等于254个字节的节点插入表头,则此时,后续的n个节点prevlen都得从1字节扩展为5字节,这种在特殊情况下产生的连续多次空间扩展操作就叫做连锁更新
因此压缩列表只会用于保存节点数量不多的场景
3.4 哈希表
redis采用链式哈希来解决哈希冲突
,当有两个以上数量的key被分配到了哈希表中同一个哈希桶上时,称这些key发生了冲突
,被分配到了同一个哈希桶上的多个节点可以用单向链表连接起来
rehash操作
rehash操作即是对哈希表的大小进行扩展,redis实际上使用了两个hash表,交替使用,进行rehash操作
在正常服务请求阶段,插入的数据都会被写入到哈希表1,此时哈希表2没有被分配空间
随着数据增多,触发了rehash操作,这个过程分为三步:
- 给哈希表2分配空间,一般会比哈希表1大两倍
- 将哈希表1的数据迁移到哈希表2中
- 迁移完成后,哈希表1的空间会释放,并把哈希表2设置为哈希表1,然后在哈希表2新创建一个空白的哈希表,为下一次rehash做准备。
第二步会存在问题,如果哈希表1的数据量非常大,那么在迁移至哈希表2的时候,会涉及大量的数据拷贝,此时可能会对redis造成阻塞,无法服务其他请求
渐进式rehash
主要是在rehash进行期间,每次哈希表元素进行新增、删除、查找或者更新操作时,redis除了会执行对应的操作之外,还会顺序将哈希表1中索引位置上的所有key-value迁移到哈希表2上
rehash触发条件
负载因子 哈希表已保存节点数量 / 哈希表大小
触发rehash操作的条件主要有两个:
当负载因子大于等于1,并且redis没有执行RDB快照或者AOF重写时,就会进行rehash操作
当负载因子大于等于5,不管有没有执行RDB或AOF,都会强制进行rehash操作
3.5 整数集合
当一个set对象只包含整数值元素,且元素数量不大时,就会使用整数集这个数据结构作为底层实现。
整数集合也是用数组实现,当把一个新元素加入整数集合时,如果新元素的类型比现有元素的类型都要长时,整数集合需要先升级,也就是按新元素的类型扩展数组的空间大小,然后才能将新元素加入整数集合里。
3.6 跳表
redis中只有zset对象的底层实现用到了跳表,跳表的优势是能支持平均O(log N)复杂度的节点查找
跳表是在链表基础上改进过来的,实现了一种多层的有序链表
跳表是一个带有层级关系的链表,每一层级可以包含多个节点,每一个节点通过指针连接起来,实现这一层特性靠跳表节点结构体(zskiplistNode
)类型的level数组。
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
如果要查找「元素:abcd,权重:4」的节点,查找的过程是这样的:
- 先从头节点的最高层开始,L2 指向了「元素:abc,权重:3」节点,这个节点的权重比要查找节点的小,所以要访问该层上的下一个节点;
- 但是该层的下一个节点是空节点( leve[2]指向的是空节点),于是就会跳到「元素:abc,权重:3」节点的下一层去找,也就是 leve[1];
- 元素:abc,权重:3」节点的 leve[1] 的下一个指针指向了「元素:abcde,权重:4」的节点,然后将其和要查找的节点比较。虽然「元素:abcde,权重:4」的节点的权重和要查找的权重相同,但是当前节点的 SDS 类型数据「大于」要查找的数据,所以会继续跳到「元素:abc,权重:3」节点的下一层去找,也就是 leve[0];
- 「元素:abc,权重:3」节点的 leve[0] 的下一个指针指向了「元素:abcd,权重:4」的节点,该节点正是要查找的节点,查询结束。
跳表节点层数设置
跳表相邻两层的节点数量最理想的比例是2:1,查找复杂度可以降低到O(logN)
跳表在创建节点时,会生成范围为[0-1]的随机数,如果这个随机数小于0.25,那么层数就增加一层,然后继续生成下一个随机数,知道随机数的结果大于0.25结束,最终确定该节点的层数
** 为什么用跳表而不用平衡树**
从内存占用上来比较,跳表比平衡树更灵活一些
,跳表中平均每个节点的指针数要比平衡树小。在做范围查找时,跳表要比平衡树简单
:平衡树需要中序遍历找到,跳表只需找到最小值后,对第一层链表进行若干步遍历即可从算法实现难度来比较,跳表比平衡树要简单
。平衡树的插入和删除操作会引发子树的调整,逻辑复杂,而跳表的插入和删除只需要修改相邻节点的指针,操作简单快速
quickList
quickList是由双向链表和压缩列表组成的
即在quickList添加一个元素时,先会检查插入位置的压缩列表是否能容纳该元素,如果能容纳就直接保存到quicklistNode结构里的压缩列表,如果不能容纳,才会新建一个新的quicklistNode结构。
listpack
listpack主要包含三个方面内容:
- encoding:定义该元素的编码类型
不同长度的整数和字符串
- data:实际存放的数据
- len:encoding + data的总长度
listpack和压缩列表相比,没有压缩列表中的prevlen(记录前一个节点长度)的字段,listpack只记录当前节点的长度,当我们向listpack加入一个新元素的时候,不会影响其他节点的长度字段的变化,因此避免了压缩列表的连锁更新问题
5、功能篇
5.1 redis过期删除策略
如何判定key已经过期了
redis会把key带上过期时间存储到一个过期字典
,当我们查询一个key时,redis首先检查该key是否存在于过期字典中:
- 如果不在,就正常读取键值
- 如果存在,就会获取该key的过期时间,然后与当前系统时间进行比对,如果比系统时间大,那就没有过期,否则判定该key已过期。
过期删除策略有哪些
- 查询到再删除,惰性
- 定期删除