Redis试卷
- Redis为什么那么快
- 谈谈i/o复用模型和reactor设计模式
- Redis的线程模型
- 设计redis的key时要注意什么
- Redis的数据结构,各种结构特点和用途
- Redis的数据结构的底层实现
- redis的事务
- Redis内存都有什么
- 内存回收策略
- 内存溢出控制策略
- 简单谈谈redisObject
- Redis的对象类型
- Redis 内存如何优化
- RDB文件,如何写入,写入格式
- AOF文件,如何写入,写入格式,如何优化,文件缓存策略
- RDB和AOF的优缺点
- 主从复制数据如何同步
- 哨兵的工作原理
- 集群工作原理
- 集群如何通信
- redis选举协议
- 集群中数据如何存储
- 集群中数据扩容
- 集群如何处理读写请求(平时和迁移数据)
- 通信故障如何处理
- Redis为什么那么快
单线程、基于内存、纯ANSI C编写、好的存储结构、i/o复用模型
- 谈谈i/o复用模型和reactor设计模式
主要提供三种模型 select 、poll、epoll 、 evport、kqueue
1、支持一个进程所能打开的最大连接数
select:单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是3232,同理64位机器上FD_SETSIZE为3264),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。
poll:poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的。
epoll:虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接。
2、FD剧增后带来的IO效率问题
select:因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。
poll:同上
epoll:因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。
3、 消息传递方式
select:内核需要将消息传递到用户空间,都需要内核拷贝动作
poll:同上
epoll:epoll通过内核和用户空间共享一块内存来实现的。
- Redis的线程模型
- 设计redis的key时要注意什么
- Redis的数据结构,各种结构特点和用途
String、list、hash、set、zset
String:字典、文本、配置
list:双向链表,支持双向的Pop/Push,可以当消息队列、列表等
hash:key-HashMap 存放属性
set:与列表相似,集合中的元素是无序的,因此不能通过索引来操作元素;集合中的元素不能有重复。Redis还支持多个集合取交集、并集、差集
zset:有序集合,排行榜
- redis的事务
没有原子性,有隔离性、一致性、持久性
- Redis内存都有什么
对象内存+缓存内存+自身内存+内存碎片
- 内存回收策略
1.惰性删除:查询key时若失效了就删除了
2.定时任务删除:定时任务。从失效的key中删除一部分
- 内存溢出控制策略
- LRU算法删除超时属性的值
- LRU算法删除值
- 随机删除过期的key
- 随机删除key
- 根据ttl删除将要过期的key
- 不删除拒绝写入报错只读
- 简单谈谈redis的数据存储
- dictEntry(包含key、value、next)
- key是存储在SDS结构中
- redisObject:Value(“world”)既不是直接以字符串存储,也不是像Key一样直接存储在SDS中,而是存储在redisObject中。
- Redis在编译时便会指定内存分配器;内存分配器可以是 libc 、jemalloc或者tcmalloc,默认是jemalloc。
-
简单谈谈redisObject
redisObject中的类型对象类型、内部编码类型、LRU计数时钟、引用计数器、数据指针
- type字段表示对象的类型,占4个比特;目前包括REDIS_STRING(字符串)、REDIS_LIST (列表)、REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。
-
encoding表示对象的内部编码,占4个比特。对于字符串,有int、embstr、raw三种编码
-
lru记录的是对象最后一次被命令程序访问的时间,占据的比特数不同的版本有所不同
-
引用计数器refcount:在于对象的引用计数和内存回收
-
ptr指针指向具体的数据
- reids中的内部编码
- 字符串:int、embstr(小于39字节)、raw(大于39字节),raw要进行2次内存分配、embstr一次就好内存是连续的
- 列表LIST:压缩列表(ziplist)或双端链表(linkedlist)
- 哈希:压缩列表(ziplist)和哈希表(hashtable)两种。
压缩列表用于元素个数少、元素长度小的场景;其优势在于集中存储,节省空间;同时,虽然对于元素的操作复杂度也由O(n)变为了O(1),但由于哈希中元素数量较少,因此操作的时间并没有明显劣势。hashtable:一个hashtable由1个dict结构、2个dictht结构、1个dictEntry指针数组(称为bucket)和多个dictEntry结构组成。
- 集合:整数集合(intset)或哈希表(hashtable)。
- 有序集合:压缩列表(ziplist)或跳跃表(skiplist)。跳跃表是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。除了跳跃表,实现有序数据结构的另一种典型实现是平衡树;大多数情况下,跳跃表的效率可以和平衡树媲美,且跳跃表实现比平衡树简单很多,因此redis中选用跳跃表代替平衡树。跳跃表支持平均O(logN)、最坏O(N)的复杂点进行节点查找,并支持顺序操作。Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成:前者用于保存跳跃表信息(如头结点、尾节点、长度等),后者用于表示跳跃表节点。具体结构相对比较复杂,
- Redis 内存如何优化
- 缩减key和value的长度
- 如果是整型/长整型,Redis会使用int类型(8字节)存储来代替字符串,可以节省更多空间。因此在可以使用长整型/整型代替字符串的场景下,尽量使用长整型/整型。
- 比如Map,List,Set等,这些集合都有一个特点可大可小,根据实际场景来定,一般情况下如果这些集合所包含的Entry不多,并且每个Entry所包含的Value不是很长的情况下,Redis内部使用紧凑格式来存储数据,紧凑格式存储数据在查询场景的算法复杂度是O(N),而类似Map或者Set他们的查询算法复杂度都是O(1)那为什么要这么做呢 ?为了能够节省内存空间,在N很小的时候其实和O(1)没什么区别。所以这里不的不介绍紧凑格式的代表ZIPMap,他的数据结构是这样:
- RDB文件,如何写入,写入格式
触发分为手动触发和自动触发两种,
手动触发:save(直接保存到磁盘)和bgsave(创建新子进程数据保存到磁盘)
自动触发: sava n m,每过n秒至少操作了m次操作才会保存,具体实现:是通过serverCron函数、dirty计数器、和lastsave时间戳来实现的。
serverCron是Redis服务器的周期性操作函数,默认每隔100ms执行一次;
dirty计数器是Redis服务器维持的一个状态,记录了上一次执行bgsave/save命令后,服务器状态进行了多少次修改(包括增删改);而当save/bgsave执行完成后,会将dirty重新置为0。
流程:
1)Redis父进程首先判断:当前是否在执行save,或bgsave/bgrewriteaof(后面会详细介绍该命令)的子进程,如果在执行则bgsave命令直接返回。bgsave/bgrewriteaof 的子进程不能同时执行,主要是基于性能方面的考虑:两个并发的子进程同时执行大量的磁盘写操作,可能引起严重的性能问题。
2)FORK子进程:RDB写入时,会连内存一起Fork出一个新进程(子进程默认会与父进程共享相同的地址空间),遍历新进程内存中的数据写文件,这样就解决了些Snapshot过程中又有新的写入请求进来的问题。这个过程中父进程是阻塞的,Redis不能执行来自客户端的任何命令 。父进程fork后,bgsave命令返回”Background saving started”信息并不再阻塞父进程,并可以响应其他命令
3)子进程创建RDB文件:RDB会先写到临时文件,完了再Rename成,这样外部程序对RDB文件的备份和传输过程是安全的。而且即使写新快照的过程中Server被强制关掉了,旧的RDB文件还在。
3)可配置是否进行压缩,压缩方法是字符串的LZF算法,以及将string形式的数字变回int形式存储。
4)动态所有停止RDB保存规则的方法:redis-cli config set save “”
该持久化的主要缺点是定时快照只是代表一段时间内的内存映像,所以系统重启会丢失上次快照与重启之间所有的数据。
- AOF文件,如何写入,写入格式,如何优化,文件缓存策略
手动触发(bgrewriteaof)和自动触发两种
工作原理:同步命令到 AOF 文件的整个过程可以分为三个阶段:
1、命令写入:Redis 将执行完的命令、命令的参数、命令的参数个数等信息发送到 AOF 程序中。
2、追加AOF缓存:AOF 程序根据接收到的命令数据, 然后每隔一定的时间(比如每隔一秒)将协议内容写入AOF 缓冲区中.
3、文件写入保存:当达到 AOF同步条件,fsync 函数或者 fdatasync 函数会被调用,将 OS Cache 中的数据写入磁盘文件。
随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的。
重写流程:
(1)触发重写:超过AOF的阈值,redis fork一个子进程,进行rewrite操作。子进程基于当前内存中的数据,构建日志,开始往一个新的临时AOF文件中写入日志,:temp-rewriteaof-pid.aof,pid为子进程pid。
(2)开启 AOF 重写缓冲区:同时开启 AOF 重写缓冲区,保存这段时间内新增的写指令。
(3)主进程继续写旧文件:之前的 AOF 操作保持,继续正常工:redis主进程,接收到client新的写操作之后,写入AOF 重写缓冲区,同时新的命令日志也继续写入旧的AOF文件appendonly.aof。
(4)AOF重写缓冲区的指令追加新文件: 重写子进程写入temp-rewriteaof-pid.aof文件结束,redis主进程将将 AOF重写缓冲区的指令追加至新AOF文件 temp-rewriteaof-pid.aof末尾。
(5)文件替换:最后用temp-rewriteaof-pid.aof替换旧AOF文件appendonly.aof。
三种 AOF 保存模式
1)AOF_FSYNC_NO :不保存。
2)AOF_FSYNC_EVERYSEC :每一秒钟保存一次、该策略为AOF的缺省策略。。
3)AOF_FSYNC_ALWAYS :每执行一个命令保存一次
- RDB和AOF的优缺点
RDB:启动是加载
1.性能:Snapshot方式的性能是要明显高于AOF方式的,原因有两点:
2.数据安全:AOF数据安全性高于Snapshot存储
- 主从复制数据如何同步
全量复制和增量复制
全量数据同步:
先执行一次全同步 — 请求master BgSave出自己的一个RDB Snapshot文件发给slave,slave接收完毕后,清除掉自己的旧数据,然后将RDB载入内存。
增量数据同步:
再进行增量同步 — master作为一个普通的client连入slave,将所有写操作转发给slave,没有特殊的同步协议。
- 哨兵的工作原理
心跳、主观下线、客观线下
- 集群工作原理
集群发现meet
1)节点A会为节点B创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典里面。
2)节点A根据CLUSTER MEET命令给定的IP地址和端口号,向节点B发送一条MEET消息。
3)节点B接收到节点A发送的MEET消息,节点B会为节点A创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典里面。
4)节点B向节点A返回一条PONG消息。
5)节点A将受到节点B返回的PONG消息,通过这条PONG消息节点A可以知道节点B已经成功的接收了自己发送的MEET消息。
6)之后,节点A将向节点B返回一条PING消息。
7)节点B将接收到的节点A返回的PING消息,通过这条PING消息节点B可以知道节点A已经成功的接收到了自己返回的PONG消息,握手完成。
8)之后,节点A会将节点B的信息通过Gossip协议传播给集群中的其他节点,让其他节点也与节点B进行握手,最终,经过一段时间后,节点B会被集群中的所有节点认识。
集群消息处理clusterProcessPacket
1)更新接收消息计数器
2)查找发送者节点并且不是handshake节点
3)更新自己的epoch和slave的offset信息
4)处理MEET消息,使加入集群
5)从goosip中发现未知节点,发起handshake
6)对PING,MEET回复PONG
7)根据收到的心跳信息更新自己clusterState中的master-slave,slots信息
8)对FAILOVER_AUTH_REQUEST消息,检查并投票
9)处理FAIL,FAILOVER_AUTH_ACK,UPDATE信息
定时任务clusterCron
1)对handshake节点建立Link,发送Ping或Meet
2)向随机几点发送Ping
3)如果是从查看是否需要做Failover
4)统计并决定是否进行slave的迁移,来平衡不同master的slave数
5)判断所有pfail报告数是否过半数
心跳数据
发送消息头信息Header
1)所负责slots的信息
2)主从信息
3)ip port信息
4)状态信息
发送其他节点Gossip信息
1)ping_sent, pong_received
2)ip, port信息
3)状态信息,比如发送者认为该节点已经不可达,会在状态信息中标记其为PFAIL或FAIL
数据结构
clusterNode结构保存了一个节点的当前状态,比如节点的创建时间,节点的名字,节点当前的配置纪元,节点的IP和地址,等等。
1)slots:位图,由当前clusterNode负责的slot为1
2)salve, slaveof:主从关系信息
3)ping_sent, pong_received:心跳包收发时间
4)clusterLink *link:节点间的连接
5)list *fail_reports:收到的节点不可达投票
集群如何通信
redis选举协议
- 集群中数据如何存储
槽 16348个 ,CRC16(key) % 16384来计算键key属于哪个槽
- 集群中数据扩容
当槽x从Node A向Node B迁移时,Node A和Node B都会有这个槽x,Node A上槽x的状态设置为MIGRATING,Node B上槽x的状态被设置为IMPORTING。
MIGRATING状态
1)如果key存在则成功处理
2)如果key不存在,则返回客户端ASK,客户端根据ASK首先发送ASKING命令到目标节点,然后发送请求的命令到目标节点
3)当key包含多个命令,
a)如果都存在则成功处理
b)如果都不存在,则返回客户端ASK
c)如果一部分存在,则返回客户端TRYAGAIN,通知客户端稍后重试,这样当所有的 key都迁移完毕的时候客户端重试请求的时候回得到ASK,然后经过一次重定向就 可以获取这批键
4)此时不刷新客户端中node的映射关系
IMPORTING状态
1)如果key不在该节点上,会被MOVED重定向,刷新客户端中node的映射关系
2)如果是ASKING命令则命令会被执行,key不在迁移的节点已经被迁移到目标的节点
3)Key不存在则新建
- 集群如何处理读写请求(平时和迁移数据)
槽里面的key还未迁移,并且槽属于迁移中
假如k1属于槽x,并且k1还在Node A
4.2 MOVED请求
槽里面的key已经迁移过去,并且槽属于迁移完
假如k1属于槽x,并且k1不在Node A,而且槽x已经迁移到Node B
4.3 ASK请求
槽里面的key已经迁移完,并且槽属于迁移中
假如k1属于槽x,并且k1不在Node A,而且槽x还是MIGRATING状态
- 通信故障如何处理
故障检测
集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此交换各个节点状态信息,检测各个节点状态:在线状态、疑似下线状态PFAIL、已下线状态FAIL。
当主节点A通过消息得知主节点B认为主节点D进入了疑似下线(PFAIL)状态时,
主节点A会在自己的clusterState.nodes字典中找到主节点D所对应的clusterNode结构,
并将主节点B的下线报告(failure report)添加到clusterNode结构的fail_reports链表中
如果集群里面,半数以上的主节点都将主节点D报告为疑似下线,那么主节点D将被标记为已下线(FAIL)状态,将主节点D标记为已下线的节点会向集群广播主节点D的FAIL消息,
所有收到FAIL消息的节点都会立即更新nodes里面主节点D状态标记为已下线。
将node标记为FAIL需要满足以下两个条件:
1.有半数以上的主节点将node标记为PFAIL状态。
2.当前节点也将node标记为PFAIL状态。
多个从节点选主
1)当从节点发现自己的主节点进行已下线状态时,从节点会广播一条
CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到这条消息,并且具有投票权的主节点向这个从节点投票
2)如果一个主节点具有投票权,并且这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条,CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示这个主节点支持从节点成为新的主节点
3)每个参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,并根据自己收到了多少条这种消息来统计自己获得了多少主节点的支持
4)如果集群里有N个具有投票权的主节点,那么当一个从节点收集到大于等于集群N/2+1张支持票时,这个从节点就成为新的主节点
5)如果在一个配置纪元没有从能够收集到足够的支持票数,那么集群进入一个新的配置纪元,并再次进行选主,直到选出新的主节点为止