1.缓存是什么?
缓存分为本地缓存和分布式缓存。 以Java为例,guava实现的就是本地缓存,生命周期随JVM销毁而结束。起多个服务实例,就有多份缓存,不具有一致性。
redis和memcached这种叫分布式缓存,多服务实例共用一份缓存数据。缓存具有一致性。
2.redis的IO多路复用?
https://draveness.me/redis-io-multiplexing/
redis采用reactor设计模式的方式来实现文件事件处理器。
文件事件处理器使用 I/O 多路复用模块同时监听多个 FD,当 accept、read、write 和 close 文件事件产生时,文件事件处理器就会回调 FD 绑定的事件处理器。
虽然整个文件事件处理器是在单线程上运行的,但是通过 I/O 多路复用模块的引入,实现了同时对多个 FD 读写的监控,提高了网络通信模型的性能,同时也可以保证整个 Redis 服务实现的简单。
多个FD----->IO多路复用模块----->文件事件分发器----->事件处理器(accept/read/write/close)
实际上是对select epoll(linux) kqueue(macOS) evport(Solaries10)的封装、根据不同的系统采用不同的模块,都没选中就用select
在这最好了解一下 epoll select 多路io复用方法。
3.Redis内存淘汰策略
maxmemory-policy volatile-lru
lru 最近最久未使用 优先被清除
ttl 对于有过期时间的,优先清除快到时间的。
random 随机清除 (肯定不能用这玩楞啊)
noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。 这个是没配置内存过期策略就这么搞。
4.redis过期键删除策略
过期策略通常有以下三种:
Redis的数据结构存储了过期时间。 过期就是执行了 del key
定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。(每隔100ms部分key检查一次)
(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)
Redis中同时使用了惰性过期和定期过期两种过期策略。
5.怎么判断一个key是过期的呢?
redisDb结构中的expires字典中保存了数据库中所有键的过期时间。
- 判断key是否存在于过期字典中
- 通过过期字典拿到key的过期时间,判断当前UNIX时间戳是否大于key时间
6.Redis解决key冲突?
链地址法 hash 表相当于一个hash的数组,然后每个数组位存储成一个链表(hashMap就是,链超过8就转红黑树)
Redis 的哈希表使用链地址法(separate chaining)来解决键冲突: 每个哈希表节点都有一个 next 指针, 多个哈希表节点可以用 next 指针构成一个单向链表, 被分配到同一个索引上的多个节点可以用这个单向链表连接起来, 这就解决了键冲突的问题。
7.redis不支持回滚?
一个Redis事务中,当 一条命令执行有问题会导致该命令失败,后续的命令正常执行。而出现其它故障的时候(down机),执行就被终止了。redis事务是不支持回滚和原子性的、
8.Redis 存储一个键值 从客户端到磁盘的过程?
先加载到内存。然后看持久化方式,根据RDB AOF而不同。 rdb按slave配置的时间进行打快照。 aof追加命令到.rdb文件里面
9.分布式寻址算法?
https://www.cnblogs.com/myseries/p/10959050.html
深入剖析Redis系列(三) - Redis集群模式搭建与原理详解 - 掘金
- hash 算法(大量缓存重建)
来了一个 key,首先计算 hash 值,然后对节点数取模。然后打在不同的 master 节点上。一旦某一个 master 节点宕机,所有请求过来,都会基于最新的剩余 master 节点数去取模,尝试去取数据。这会导致大部分的请求过来,全部无法拿到有效的缓存,导致大量的流量涌入数据库。 target = hash(key)/ 质数
简单hash算法计算的值会依赖于质数。如果再加入一个分区则之前的hash映射都会失效,无法动态调整。解决此问题的办法为使用一致性hash,一致性hash可以解决大部分hash引用失效的问题。
2.一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡) https://www.cnblogs.com/myseries/p/10959050.html
把全量的缓存空间当做一个环形存储结构,节点和key都做hash处理放到环上,两个节点段的key属于顺时针的节点所拥有,即使一个节点挂点,只是这一段的key归属于另一个顺时针节点了。这段的key会重新加载数据库。
一致性哈希算法可以将数据尽可能平均的存储到N台缓存服务器上,提高系统的负载均衡,并且当有缓存服务器加入或退出集群时,尽可能少的影响现有缓存服务器的命中率,减少数据对后台服务的大量冲击
3.redis cluster 的 hash slot 算法
redis cluster 有固定的 16384 个 hash slot,对每个 key 计算 CRC16 值,然后对 16384 取模,可以获取 key 对应的 hash slot。每个节点负责维护一部分槽以及槽所映射的键值数据。
redis cluster 中每个 master 都会持有部分 slot,比如有 3 个 master,那么可能每个 master 持有 5000 多个 hash slot。hash slot 让 node 的增加和移除很简单,增加一个 master,就将其他 master 的 hash slot 移动部分过去,减少一个 master,就将它的 hash slot 移动到其他 master 上去。移动 hash slot 的成本是非常低的。客户端的 api,可以对指定的数据,让他们走同一个 hash slot,通过 hash tag 来实现。
例如:Redis Cluster 采用虚拟槽分区,所有的键根据哈希函数映射到 0~16383 整数槽内,计算公式:slot = CRC16(key)& 16384。每个节点负责维护一部分槽以及槽所映射的键值数据。
缓存的key hash结果是和slot绑定的,而不是和服务器节点绑定,所以节点的更替只需要迁移slot。(不能同一主从都挂)
10.缓存异常
1、雪崩:同一时间,大面积缓存失效,后面请求打到数据库。例如大量的key到期。 https://juejin.im/post/5c9a67ac6fb9a070cb24bf34
解决:分散key的过期时间 或者 使用分布式锁来实现,分布式锁常采用redis来实现,简单的就是 set resourceName value ex 5 nx
2、穿透:缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。
解决:布隆过滤器(在缓存层之前加布隆过滤器/Redis4.0带了,但是要小心使用) 或者如果查询结果返回为空,将空值进行缓存. 对于解决分布式服务并发读写还是得用分布式锁.
3、击穿:指一个key非常热点,大并发集中对这个key进行访问,当这个key在失效的瞬间,仍然持续的大并发访问就穿破缓存,转而直接请求数据库。
使用互斥锁,对于分布式集群应该使用分布式锁。
4、缓存降级
对不同功能的缓存设置熔断机制。相当于资源就这么多,让次要的业务别占资源,资源给核心业务使用。
11.什么是布隆过滤器? https://www.cnblogs.com/cpselvis/p/6265825.html
Redis4.0带有这个了。
解决如网页 URL 去重、垃圾邮件识别、大集合中重复元素的判断和缓存穿透等问题。
实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
如果不用布隆过滤器,最好的方式就是hash,但是hash虽然快,但是比较耗内存、
布隆过滤器(Bloom Filter)的核心实现是一个超大的位数组和几个哈希函数。假设位数组的长度为m,哈希函数的个数为k
假设集合里面有3个元素{x, y, z},哈希函数的个数为3。首先将位数组进行初始化,将里面每个位都设置位0。对于集合里面的每一个元素,将元素依次通过3个哈希函数进行映射,每次映射都会产生一个哈希值,这个值对应位数组上面的一个点,然后将位数组对应的位置标记为1。查询W元素是否存在集合中的时候,同样的方法将W通过哈希映射到位数组上的3个点。如果3个点的其中有一个点不为1,则可以判断该元素一定不存在集合中。反之,如果3个点都为1,则该元素可能存在集合中。注意:此处不能判断该元素是否一定存在集合中,可能存在一定的误判率。可以从图中可以看到:假设某个元素通过映射对应下标为4,5,6这3个点。虽然这3个点都为1,但是很明显这3个点是不同元素经过哈希得到的位置,因此这种情况说明元素虽然不在集合中,也可能对应的都是1,这是误判率存在的原因。
使用布隆过滤器的好处:
省空间: 不需要存储完整的元素,只需要对元素进行hash然后将bit向量表中的某个位设为1即可(先不考虑碰撞问题)
查找快: 只需要对查找的元素进行hash然后看bit向量表中对应的位是否为1。
缺点:
1.因为碰撞,会有一定的误报率( 不在集合的一定不在, 在的不一定在 )。这个可以通过使用多个hash函数减少误报,但无法完全消除。
2.不支持删除操作(还是因为碰撞,会出现误删的问题)。
12.如何保证缓存与数据库双写时的数据一致性?
读的时候先读缓存,没有读库再更新到缓存里。
更新的时候,删除缓存,并更新数据库。下次查的时候会刷到缓存里面。
强一致性:读写串行化。这样写的时候读不了。
13.单线程的Redis
Redis利用队列技术讲并发访问变成串行访问。
Redis的瓶颈在于内存而不是CPU
避免了多线程的上下文切换。
14.分布式系统下的Redis事务(redis并发竞争key)
因为key不一定在一个分片上。Redis事务比较鸡肋。
可以考虑分布式锁+时间戳。大家去抢锁,抢到锁就做set操作即可。发现自己的value的时间戳早于缓存中的时间戳,那就不做set操作了(这里把时间戳存到缓存数据结构里) 或者使用队列。
15.Redis集群 https://www.cnblogs.com/kevingrace/p/7955725.html
https://juejin.im/post/5b8fc5536fb9a05d2d01fb11
Redis3.0自带的Redis cluster
Redis Cluster 采用虚拟槽分区,所有的键根据哈希函数映射到 0~16383 整数槽内,计算公式:slot = CRC16(key)& 16383。每个节点负责维护一部分槽以及槽所映射的键值数据
客户端随机地请求任意一个Redis 实例,然后由Redis 将请求转发给正确的Redis 节点。Redis Cluster 实现了一种混合形式的查询路由,但并不是直接 将请求从一个Redis 节点转发到另一个Redis 节点,而是在客户端的帮助下直接重定向( redirected)到正确的Redis 节点。
redis cluster一般是主从的,如果一个主节点挂掉了。会将对应从节点的数据槽转移到hash顺序的主节点。某个结点挂了的话,因为数据在其他结点上有备份,所以其他结点顶上来就可以继续提供服务,保证了Availability。
集群进入fail状态的必要条件
- 某个主节点和所有从节点全部挂掉,我们集群就进入faill状态。
- 如果集群超过半数以上master挂掉,无论是否有slave,集群进入fail状态.
- 如果集群任意master挂掉,且当前master没有slave.集群进入fail状态
16.Redis cluster 一个master节点down掉之后怎么办?
集群是有主从的,如果down掉的节点没有从节点,哪集群就变成fail状态了。
假设down掉的主节点是有从节点的,那么由于集群采用 hash slot算法。 将数据hash到虚拟槽,然后将虚拟槽平均分配给不同的节点。从节点的虚拟槽和主节点是一样的,那么主节点要是down掉,就选举从节点作为主节点继续提供服务。而不是将槽乱挪。(最开始我以为是挪槽呢)
17.Redis Cluster集群的处理流程全梳理
在单机模式下,Redis对请求的处理很简单。Key存在的话,就执行请求中的操作;Key不存在的话,就告诉客户端Key不存在。然而在集群模式下,因为涉及到请求重定向和Slot迁移,所以对请求的处理变得很复杂,流程如下:
- 检查Key所在Slot是否属于当前Node?
- 计算crc16(key) % 16384得到Slot
- 查询clusterState.slots负责Slot的结点指针
- 与myself指针比较
- 若不属于,则响应MOVED错误重定向客户端
- 若属于且Key存在,则直接操作,返回结果给客户端
- 若Key不存在,检查该Slot是否迁出中?(clusterState.migrating_slots_to)
- 若Slot迁出中,返回ASK错误重定向客户端到迁移的目的服务器上
- 若Slot未迁出,检查Slot是否导入中?(clusterState.importing_slots_from)
- 若Slot导入中且有ASKING标记,则直接操作
- 否则响应MOVED错误重定向客户端
18.Redis Cluster容错机制failover总结
https://www.cnblogs.com/kevingrace/p/7955725.html
failover是redis cluster的容错机制,是redis cluster最核心功能之一;它允许在某些节点失效情况下,集群还能正常提供服务。
redis cluster采用主从架构,任何时候只有主节点提供服务,从节点进行热备份,故其容错机制是主从切换机制,即主节点失效后,选取一个从节点作为新的主节点。在实现上也复用了旧版本的主从同步机制。
从纵向看,redis cluster是一层架构,节点分为主节点和从节点。从节点挂掉或失效,不需要进行failover,redis cluster能正常提供服务;主节点挂掉或失效需要进行failover。另外,redis cluster还支持manual failover,即人工进行failover,将从节点变为主节点,即使主节点还活着。下面将介绍这两种类型的failover。
1)主节点失效产生的failover
a)(主)节点失效检测
一般地,集群中的节点会向其他节点发送PING数据包,同时也总是应答(accept)来自集群连接端口的连接请求,并对接收到的PING数据包进行回复。当一个节点向另一个节点发PING命令,但是目标节点未能在给定的时限(node timeout)内回复时,那么发送命令的节点会将目标节点标记为PFAIL(possible failure)。
由于节点间的交互总是伴随着信息传播的功能,此时每次当节点对其他节点发送 PING 命令的时候,就会告知目标节点此时集群中已经被标记为PFAIL或者FAIL标记的节点。相应的,当节点接收到其他节点发来的信息时, 它会记下那些被其他节点标记为失效的节点。 这称为失效报告(failure report)。
如果节点已经将某个节点标记为PFAIL,并且根据节点所收到的失效报告显式,集群中的大部分其他主节点(n/2+1)也认为那个节点进入了失效状态,那么节点会将那个PFAIL节点的状态标记为FAIL。
一旦某个节点被标记为FAIL,关于这个节点已失效的信息就会被广播到整个集群,所有接收到这条信息的节点都会将失效节点标记为FAIL。
b)选举主节点
一旦某个主节点进入 FAIL 状态, 集群变为FAIL状态,同时会触发failover。failover的目的是从从节点中选举出新的主节点,使得集群恢复正常继续提供服务。
整个主节点选举的过程可分为申请、授权、升级、同步四个阶段:
(1)申请
新的主节点由原已失效的主节点属下的所有从节点中自行选举产生,从节点的选举遵循以下条件:
a、这个节点是已下线主节点的从节点;
b、已下线主节点负责处理的哈希槽数量非空;
c、主从节点之间的复制连接的断线时长有限,不超过 ( (node-timeout * slave-validity-factor) + repl-ping-slave-period )。
如果一个从节点满足了以上的所有条件,那么这个从节点将向集群中的其他主节点发送授权请求,询问它们是否允许自己升级为新的主节点。
从节点发送授权请求的时机会根据各从节点与主节点的数据偏差来进行排序,让偏差小的从节点优先发起授权请求。
(2)授权
其他主节点会遵信以下三点标准来进行判断:
a、 发送授权请求的是从节点,而且它所属的主节点处于FAIL状态 ;
b、 从节点的currentEpoch〉自身的currentEpoch,从节点的configEpoch>=自身保存的该从节点的configEpoch;
c、 这个从节点处于正常的运行状态,没有被标记为FAIL或PFAIL状态;
如果发送授权请求的从节点满足以上标准,那么主节点将同意从节点的升级要求,向从节点返回CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK授权。
(3)升级
一旦某个从节点在给定的时限内得到大部分主节点(n/2+1)的授权,它就会接管所有由已下线主节点负责处理的哈希槽,并主动向其他节点发送一个PONG数据包,包含以下内容:
a、 告知其他节点自己现在是主节点了
b、 告知其他节点自己是一个ROMOTED SLAVE,即已升级的从节点;
c、告知其他节点都根据自己新的节点属性信息对配置进行相应的更新
(4)同步
其他节点在接收到ROMOTED SLAVE的告知后,会根据新的主节点对配置进行相应的更新。特别地,其他从节点会将新的主节点设为自己的主节点,从而与新的主节点进行数据同步。
至此,failover结束,集群恢复正常状态。
此时,如果原主节点恢复正常,但由于其的configEpoch小于其他节点保存的configEpoch(failover了产生较大的configEpoch),故其配置会被更新为最新配置,并将自己设新主节点的从节点。
另外,在failover过程中,如果原主节点恢复正常,failover中止,不会产生新的主节点。
2)Manual Failover
Manual Failover是一种运维功能,允许手动设置从节点为新的主节点,即使主节点还活着。
Manual Failover与上面介绍的Failover流程大都相同,除了下面两点不同:
a)触发机制不同,Manual Failover是通过客户端发送cluster failover触发,而且发送对象只能是从节点;
b)申请条件不同,Manual Failover不需要主节点失效,failover有效时长固定为5秒,而且只有收到命令的从节点才会发起申请。
另外,Manual Failover分force和非force,区别在于:非force需要等从节点完全同步完主节点的数据后才进行failover,保证不丢失数据,在这过程中,原主节点停止写操作;而force不进行进行数据完整同步,直接进行failover。
3)集群状态检测
集群有OK和FAIL两种状态,可以通过CLUSTER INFO命令查看。当集群发生配置变化时, 集群中的每个节点都会对它所知道的节点进行扫描,只要集群中至少有一个哈希槽不可用(即负责该哈希槽的主节点失效),集群就会进入FAIL状态,停止处理任何命令。
另外,当大部分主节点都进入PFAIL状态时,集群也会进入FAIL状态。这是因为要将一个节点从PFAIL状态改变为FAIL状态,必须要有大部分主节点(n/2+1)认可,当集群中的大部分主节点都进入PFAIL时,单凭少数节点是没有办法将一个节点标记为FAIL状态的。 然而集群中的大部分主节点(n/2+1)进入了下线状态,让集群变为FAIL,是为了防止少数存着主节点继续处理用户请求,这解决了出现网络分区时,一个可能被两个主节点负责的哈希槽,同时被用户进行读写操作(通过禁掉其中少数派读写操作,证保只有一个读写操作),造成数据丢失数据问题。
说明:上面n/2+1的n是指集群里有负责哈希槽的主节点个数。
19.Redis中的pipeline
管道嘛,肯定是传输用的。 发送命令-〉命令排队-〉命令执行-〉返回结果
管道(pipeline)可以一次性发送多条命令并在执行完后一次性将结果返回,pipeline通过减少客户端与redis的通信次数来实现降低往返延时时间
- 原生批命令mget是原子性,pipeline是非原子性
- 原生批命令一命令多个key, 但pipeline支持多命令(存在事务),非原子性
- 原生批命令是服务端实现,而pipeline需要服务端与客户端共同完成
20.redis的底层数据结构
注:本篇按照Redis设计与实现这本书来写。基于Redis3.0版本
redis使用了 SDS、链表、字典(哈希表)、跳跃表、整数集合、压缩列表 几种数据类型,我们操作的api是对这几个数据结构的封装
SDS 简单动态字符串
Redis是用c语言写的,自然而言遵循c语言的特性。c语言字符串就有一堆坑,比如缓冲区溢出,获取字符串长度o(n)复杂度。
对于c语言来说,字符串存储在字符数组里面,每次字符串的变更导致数组的变更,从而进行内存重新分配。因为内存重分配涉及复杂的算法, 并且可能需要执行系统调用, 所以它通常是一个比较耗时的操作。
为了避免 C 字符串的这种缺陷, SDS 通过未使用空间解除了字符串长度和底层数组长度之间的关联: 在 SDS 中, buf 数组的长度不一定就是字符数量加一, 数组里面可以包含未使用的字节, 而这些字节的数量就由 SDS 的 free 属性记录。
通过未使用空间, SDS 实现了空间预分配和惰性空间释放两种优化策略。
空间预分配
空间预分配用于优化 SDS 的字符串增长操作: 当 SDS 的 API 对一个 SDS 进行修改, 并且需要对 SDS 进行空间扩展的时候, 程序不仅会为 SDS 分配修改所必须要的空间, 还会为 SDS 分配额外的未使用空间。
如果对 SDS 进行修改之后, SDS 的长度(也即是 len 属性的值)将小于 1 MB , 那么程序分配和 len 属性同样大小的未使用空间, 这时 SDS len 属性的值将和 free 属性的值相同。 通过这种预分配策略, SDS 将连续增长 N 次字符串所需的内存重分配次数从必定 N 次降低为最多 N 次。
惰性空间释放
惰性空间释放用于优化 SDS 的字符串缩短操作: 当 SDS 的 API 需要缩短 SDS 保存的字符串时, 程序并不立即使用内存重分配来回收缩短后多出来的字节, 而是使用 free 属性将这些字节的数量记录起来, 并等待将来使用。
比如一个 free 为5 buf[]= redis 的sds对象,变成re 这样就是 free=3 buf[]=re 此时不会立刻释放掉free的内存,而是提供api等你啥时候缺了再释放,所以不用担心惰性空间释放策略会造成内存浪费。比如内存灌满了等等、
二进制安全
C语言字符串还有一个缺点是 \0表示结尾,那么字符中有空格肯定会有问题。这些限制使得C 字符串只能保存文本数据, 而不能保存像图片、音频、视频、压缩文件这样的二进制数据。
所有 SDS API 都会以处理二进制的方式来处理 SDS 存放在 buf 数组里的数据, 程序不会对其中的数据做任何限制、过滤、或者假设 —— 数据在写入时是什么样的, 它被读取时就是什么样。因为 SDS 使用 len 属性的值而不是空字符来判断字符串是否结束
C 字符串与SDS 区别
- 获取字符串长度的复杂度为 O(N) 。 获取字符串长度的复杂度为 O(1) 。
- API 是不安全的,可能会造成缓冲区溢出。 API 是安全的,不会造成缓冲区溢出。
- 修改字符串长度 N 次必然需要执行 N 次内存重分配。 修改字符串长度 N 次最多需要执行 N 次内存重分配。
- 只能保存文本数据。 可以保存文本或者二进制数据。
- 可以使用所有 <string.h> 库中的函数。 可以使用一部分 <string.h> 库中的函数。
链表
链表提供了高效的节点排重能力,以及顺序的节点访问方式。
链表,存储了next和prev指针。有头结点和尾节点的指针,访问头尾是o(1) 其他位置是o(n).存储了链表的长度o(1)
使用adlist.h/list对链表进行封装如下。链表的查找、删除、修改是o(n),其余操作是o(1)的
字典
字典又称关联数组、map。 Redis中的字典实际上就是哈希表数组。 Redis中所有的key是唯一的,实际上就是一个字典。
Redis的数据库就是用字典作为底层实现的,对数据库的crud也是构建在字典上的,所以很快的。set a "hello world"
对key进行hash,存储的是hash之后的值,如果发生冲突就链地址法,冲突的位置连成一个链表。
如下为dictEntry的具体内部结构。next指针就是连成链表用的。
字典的数据结构如下:
rehash 当hash进行扩展时需要进行rehash操作,扩展2^n
在进行拓展或者压缩的时候,可以直接将所有的键值对rehash 到ht[1]中,这是因为数据量比较小。在实际开发过程中,这个rehash 操作并不是一次性、集中式完成的,而是分多次、渐进式地完成的。
渐进式rehash 的详细步骤:
- 为ht[1] 分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
- 在几点钟维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash 开始
- 在rehash 进行期间,每次对字典执行CRUD操作时,程序除了执行指定的操作以外,还会将ht[0]中的数据rehash 到ht[1]表中,并且将rehashidx加一
- 当ht[0]中所有数据转移到ht[1]中时,将rehashidx 设置成-1,释放ht[0]。然后将ht[1]改成ht[0],重新分配ht[1]
字典除了释放所有键值对是o(n),其余操作都是o(1)
跳跃表
skiplist是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员是比较长的字符串时,Redis就会使用跳跃表作为有序结合键的底层实现。
和字典、链表不同的是,跳跃表只是在两个地方用到了,有序集合键&&集群节点中作为内部数据结构。
上图是一个节点的数据结构。跳跃表肯定是多个这种节点组成然后又包了一下的。
每新增一个上图数据结构的节点就会根据幂次定律生成一个介于1-32之间的值作为level数组的大小。这个大小就是层的高度。整体的排序根据score的大小来进行排序,可以理解为高端的平衡树。其中层里面的span是记录两个节点之间的距离。
跳跃表就是多个跳跃节点组成的,持有最高层、长度等的一个数据结构、
其中level是存储的节点最高的。如上图就是o3所在的L5
length就是遍历到尾部指针需要的遍历层度,如上就是o1---o2---o3 所在的层,一共三层。
可以看出不是每层都一样多,是1-32随机的,而多少个节点值就是多少的length,只不过访问的时候由于层指针的原因会遍历的相对链表来说更少。
其中level[] 就是上图中的 L(n)数组,对应节点所在的L(n)相连,存储到*forward前进指针,其中span存储的是与其余相连节点的距离
所以跳跃表是有平均复杂度o(logn)和最坏复杂度的o(n)
整数集合
整数集合是集合键(set)的底层实现之一,当一个集合只包括整数值元素。并且这个集合的元素不多时,就采用整数集合进行存储。
升级:当加入一个新元素,int32比int8要数据类型长时,需要将之前的元素全都升级为int32
降级:整数集合不支持降级的操作。
压缩列表
压缩列表是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。
压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构。
每个压缩列表节点可以保存一个字节数组或者一个整数值。
压缩列表的组成previous_entry_length encoding content
- previous_entry_length前一个节点的长度。
- encoding记录了节点的content数据所保存的数据类型和长度。
- content属性负责保存节点的值,节点值可以是一个字节数组或者整数。值得类型和长度由节点encoding决定。
压缩列表的时间复杂度查、删除、增加、修改都是o(n)相当于节约内存牺牲时间。
对象
就是我们直接用的string list set zset hash
encoding存储的是对象使用了什么数据结构作为对象的底层实现。下面是数据结构与数据类型的对应关系。
其中还包括 lru 属性,记录对象最后一次被命令程序访问的时间。
内存回收
redis在自己的对象系统中构建了一个引用计数信息,在适当的时候自动释放对象并进行内存回收。
Java是把引用计数回收这个方式给否了的。因为循环引用,但是在Redis并不存在循环引用。
对象共享
如果一个对象例如 “100” 在多个地方使用,那么就引用+1,然后指针指向这个redisObject对象的内存地址。
object refcount key 看引用计数数量。
Redis会在初始化服务器是创建一万个字符串对象 0-9999
object idletime 展示 当前时间 - lru时间
参考:《Redis设计与实现》
https://draveness.me/redis-io-multiplexing/ io多路复用
https://www.cnblogs.com/myseries/p/10959050.html hash寻址
https://juejin.im/post/5c9a67ac6fb9a070cb24bf34 缓存异常
https://www.cnblogs.com/cpselvis/p/6265825.html 布隆过滤器
https://www.cnblogs.com/kevingrace/p/7955725.html Redis集群!
https://juejin.im/post/5b8fc5536fb9a05d2d01fb11 Redis集群