最近在学习redis,用问答方式检查下自己的学习情况
一、简单的介绍下redis?
redis是key-value型的缓存数据库,key为字符串类型,value支持五种类型,分别是字符串,列表,哈希表,集合,有序集合。
二、redis数据类型底层是如何实现的?
redis中最基础的类型是字符串,其他复合类型的元素都是字符串。
redis中key是字符串,底层使用SDS存储
value是数据类型,底层使用redisObject存储
redis数据类型底层使用redisObject实现。
typedef struct redisObject { unsigned type:4; unsigned encoding:4; unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */ int refcount; void *ptr; } robj;
redisObject包含以下字段
type 占4个比特,表示类型,值为支持的五种类型,最多支持2^4=16种类型
encoding 占4个比特,表示编码方式。每个类型都至少有两种编码方式,最多也是支持16种编码方式。
lru 64位系统占24比特,表示程序最近访问该对象的时间。
refCount 占4个字节,表示引用计数,redis默认使用0-9999作为共享对象(数据可以通过OBJ_SHARED_INTEGERS调整),每当有共享对象被引用,引用计数加一,反之减一。当引用计数为零会被回收。
*ptr 占8个字节,表示实际存储对象的指针
redisObject对象大小为
4bit+4bit+24bit+4Byte+8Byte=16Byte
字符串底层使用SDS(simple dynamic string)简单动态字符串实现
struct sdshdr { int len; int free; char buf[]; };
SDS包含三个字段,分别是len,free,[]buf。
len int类型,占4个字节。表示已使用字符串长度
free int类型,占4个字节,表示未使用长度,len+free+1等于字符串总长度
buf[]表示字节数组,存储具体的字符串以 结尾
SDS占用空间为4+4+字符串长度+1( )=9Byte+字符串长度
其实如果学过golang,会发现SDS跟[]byte是类似的,len相同都表示已使用长度,[]byte中cap-len就等于free
使用sds好处在于
获取字符串长度为o(1)
存储二进制文件,不以 标记字符串结束,而是用len
三、简单描述下redis五种类型编码方式和转换关系(3.0以下)
编码转换在Redis写入数据时完成,且转换过程不可逆,只能从小内存编码向大内存编码转换。
1、字符串,长度不能超过512MB
1、int:8个字节的长整型,字符串值为整型时,用long整型表示
2、embstr:长度小于等于39字节的字符串。embstr和raw都使用redisObject和sds保存数据,区别在于embstr使用只分配一次内存空间,redisObject和sds一起分配,是连续的。
raw需要分配两次。embstr好处在于少分配一次空间,并且数据连在一起,寻找方便。坏处是字符串增加需要重新分配内存。所以embstr实现为只读。
3、raw:长度大于39字节的字符串。
embstr长度为39的原因是
39=64(jemalloc分配)-16(redisObject)+9(SDS)
编码转换,只能由embstr转换为raw。如果修改embstr对象,修改后的对象为raw,无论长度是否达到39字节。
2、列表,可以存储2^32-1个有序字符串。支持两端插入和弹出,并且可以获取指定位置元素,可以充当数组、列表、栈等。
1、压缩列表
2、双向链表
元素个数小于512个,并且所有字符串对象都不足64字节菜使用压缩列表。只能由压缩列表转化为双端链表
3、哈希表
1、压缩列表
2、hashTable
4、集合 无序,不能重复
1、整数集合
typedef struct intset{ uint32_t encoding; uint32_t length; int8_t contents[]; } intset;
2、hashTable
集合中元素数量小于512个;集合中所有元素都是整数值,才使用整数集合,否则使用哈希表。只能由整数集合转换为哈希表
5、有序集合 有序 不能重复
1、压缩列表
2、跳跃表
有序集合中元素数量小于128个;有序集合中所有成员长度都不足64字节,才使用压缩列表,否则使用跳跃表。只能由压缩列表转化为跳跃表。
参考自https://www.cnblogs.com/kismetv/p/8654978.html
四、redis中如何实现持久化存储
redis中提供了两种持久化存储方式,分别是RDB和AOF
RDB是通过对进程内存数据进行一次快照,然后将快照存储到硬盘实现的持久化存储
AOF则是通过将每次的写请求记录到文件中,类型mysql的binlog来实现的持久化存储
RDB持久化是通过调用save或bgsave来实现的
save和bgsave区别是
调用save命令生成RDB文件,整个过程都是阻塞的,服务器不能响应请求。
bgsave是通过fork一个子进程,由子进程来创建RDB文件,只有fork子进程的时候是阻塞的,fork结束后,父进程可以继续工作。
一般我们都使用bgsave命令
分为主动触发和被动触发两种
主动触发
直接调用save、bgsave命令来生成RDB 文件
被动触发
一般通过修改配置文件中的save m n实现
save m n的意思是m秒内发生了n次变化
通过redis中的ServerCron函数,配合dirty计数器和lastsave时间戳来实现。
dirty记录上次save、bgsave执行成功后数据的修改次数,每次修改都加一,save和bgsave执行成功后重置为零
lastsave记录上次save和bgsave执行成功后的时间
serverCron函数100ms会检查一次是否满足save m n条件,
判断当前时间-lastsave是否>m,并且dirty是否>=n
满足条件则执行save、bgsave。
除了以上情况,主从复制的全量复制以及shutdown关机命令关机之前都会自动执行rdb持久化。
bgsave执行过程
1、判断服务器是否正在执行save、bgsave、bg_rewrite_aof命令,如果正在执行,则直接返回
2、父进程fork出子进程,该过程是阻塞的。
3、fork成功后,bgsave返回信息给父进程,父进程解除阻塞。
3、子进程根据父进程当前内存快照,生成RDB文件,并对原有的RDB文件进行原子替换
4、子进程创建RDB成功后,发送信号给父进程,父进程更新统计信息。
AOF持久化
分为三个阶段
1、命令追加
redis会将写命令同时写入aof_buf缓冲区中
2、文件写入和同步
aof_buf缓冲区根据不同同步策略,通过fsync函数将数据写入到文件
支持策略如下
always:即缓冲区一有写命令就写入文件,这样做会对磁盘io造成影响,严重影响性能
no:即不同步,由操作系统来将缓冲区数据写入文件。一般是30s一次
everysec:即每秒同步一次,是比较折中的策略,推荐使用。
3、文件重写
随着时间推移,aof文件会越来越大,redis会定时重写aof文件来压缩。
aof重写是将redis进程内数据转化为写明了,同步到新的aof文件中,不会操作旧aof文件
重写能够压缩aof文件的原因如下
过期数据不写入
无效命令不写入
将多个命令合并
重写触发
手动触发
执行bgrewriteaof命令,类似bgsave,fork子进程,子进程负责具体工作
自动触发
由auto_aof_rewrite_min_size和auto_aof_rewrite_percentage
auto_aof_rewrite_min_size:即执行重写时aof文件最小体积,一般默认64MB,超过这个大小才可能重写
autof_aof_rewrite_percentage:即当前aof文件与上一个aof文件文件的比值
两个参数都满足才自动触发
重写流程
1、redis判断当前是否在执行save、bgsave、bgrewriteaof命令,是直接返回
2、父进程fork创建子进程,父进程阻塞
3、fork后,bgrewriteaof返回信息给父进程,父进程不在阻塞。redis写命令依然写入aof缓冲区,根据appendfsync策略同步到硬盘,保证aof机制正确
4、fork使用写时复制计数,子进程只共享fork操作时的内存数据,但是父进程仍然在响应请求,因此redis使用aof_rewrite_aof保存在子进程重写aof文件中发生的写请求,防止数据丢失。
在bgrewriteaof执行期间,写明了同时追加到aof_buf和aof_rewrite_buf
5、子进程根据内存快照,按照命令合并规则写入新的aof文件
6、子进程写完新的aof文件后,向父进程发信号,父进程更新统计信息
7、父进程将aof_rewrite_buf数据写入新的aof文件中,保证aof文件保存的数据库状态和服务器一致。
9、使用新的aof文件替换老文件,完成aof重写。
启动时加载
redis默认开启rdb,关闭aof。开启aof需要appendonly设置为yes。
redis启动时,判断是否开启aof持久化,是则加载aof文件恢复数据。只有关闭aof持久化,才会载入rdb文件恢复数据。
两个文件加载时都会校验文件的完整性,文件损坏都会打印错误,导致redis启动失败。
参考自https://www.cnblogs.com/kismetv/p/9137897.html
既然讲到了数据库快照,多提下两个常见的快照技术
写时复制和 写重定向
需要理解三个概念,源卷,快照卷和映射表
源卷指的是源数据存储卷,快照卷指的时快照数据存储卷,映射表是快照卷和源卷地址映射关系表
1、写时复制:创建快照过程,如果存在写操作,将新数据放到缓存,将源卷的旧数据拷贝到快照卷,并将映射关系写入映射表,然后将缓存的新数据写入源卷。
2、写重定向:创建快照过程,如果存在写操作,将新数据写入快照卷,并将映射关系写入映射表。
两种技术对比
写时复制将旧数据存储在快照卷中,新的数据存储在源卷中,写数据需要拷贝,所以性能会差一点,但是读时候直接读源卷,性能高。适合读多写少场景。恢复数据时需要从快照卷上拷贝。
写重定向将旧数据存储在源卷中,新数据存储在快照卷中。写数据时重定向到快照卷,只写一次,性能比写时复制高,但是读时候需要判断数据是否在快照卷上,如果不是还需要去源卷上差,性能差。适合读少写多场景。
恢复数据时直接删除快照卷和映射表即可。
五、redis的主从复制原理
为了实现redis的负载均衡和数据备份,redis还提供了主从复制功能。
主要是在多个服务器上运行redis,并且通过主从复制同步数据。一般主服务器提供写,从服务器提供读实现读写分离从而达到负载均衡的效果。并且每个服务器都有一份相同的数据。
主从复制可以通过参数配置,从服务器使用slaveof(5.0以下)或replicaof(5.0以上命令+主服务器ip+主服务器端口号可以开启主从复制。
主从复制分为三个阶段
1、建立连接
1.1:从服务器保存主服务器的ip和端口号
1.2:从服务器1秒调用一次复制定时函数replicationCron。通过ip和端口号建立与主服务器的连接,建立成功后,从服务器会创建一个专门的文件事件处理器处理后续的工作。主服务器将从服务器当作客户端。
1.3:从服务器向主服务器发送ping命令,试探主服务器是否可以开始主从复制。
一般会收到三种响应
1.3.1:主服务器返回pong,表示主服务器可以正常开启主从复制。
1.3.2:请求超时,主服务器未返回响应。则可能发生网络故障。断开连接,重连。
1.3.2:主服务器返回pong之外的数据。表示主服务器暂时无法开启主从复制。断开连接,重连。
1.4:身份认证,如果从服务器开启了masterauth,则会开始进行身份认证,判断masterauth和主服务器的密码是否相同,不同则拒绝后续请求。
1.5:认证通过后,从服务器向主服务器发送自己的端口号信息。主服务器保存该信息。连接建立成功,进入下一个阶段
2、数据同步
该阶段主从服务器互相发送请求,互为客户端。
主从服务的数据同步分为两种,全量复制和部分复制。
全量复制表示将主服务器的所有数据都发送给从服务器,一般在初次复制或者无法进行部分复制的情况下才会进行全量复制。
部分复制表示将中断期间主服务器收到的写请求发生给从服务器。一般在由于网络中断等原因断开连接时恢复主从复制使用。
redis2.8之前只支持sync命令,即全量复制。2.8之后新增了psync命令,可以开启部分复制。
全量复制过程如下(从服务器发送全量复制命令或者主服务器判断无法进行部分复制,开启全量复制)
2.1:从服务器向主服务器发送psync请求,
2.2:主服务器调用bgsave命令,通过子进程生成rdb文件,并通过一个复制缓冲区记录期间所有写请求。
2.3:rdb文件创建完成并发送给从服务器,从服务器删除所有数据,加载rdb文件。该过程从服务器是阻塞的。无法响应命令。
2.4:从服务器加载完成后,主服务器将复制缓冲区数据发送给从服务器,从服务器执行写请求,保证主从服务器数据一致性。
2.5:如果从服务器开启了aof,则会触发bgrewriteaof执行,保证aof文件更新至主服务器最新状态
部分复制的一些概念
runid,每个redis服务器都会有唯一的一个runid,根据runid会判断上次复制的主服务器是否发生改变,如果发生改变,则会开启全量复制。
偏移量offset:主从服务器维护一个复制偏移量,记录每次发送数据的字节数,每传播N个字节数据,offset+N。用于判断主从服务器是否一致。
复制积压缓冲区:主服务器记录redis最近执行的一些命令,是个先进先出的队列,大小默认1MB。作用是备份写命令。除了写命令还存储了复制偏移量。无论由多少个从服务器,都只有一个复制积压缓冲区。
redis根据从服务器的偏移量将复制积压缓冲区的请求发送给从服务器,如果偏移量不在复制积压缓冲区中,说明网络中断时间太长,复制积压缓冲区溢出,无法开启部分复制。只能进行全量复制。
3、命令传播
数据同步完成后,主服务器会将写请求发送给从服务器,从服务器接受并执行。保证数据一致性
除了发送写命令, 主从服务器还维护心跳机制。PING和REPLCONF ACK。
主服务器给每个一段时间给从服务器发送ping命令,默认10s。为了让从服务器进行超时判断。
从服务器向主服务器发送REPLCONF ACK命令,每秒1s。REPLCONF ACK offset。offset指的是从服务器的偏移量。通过REOLCONF ACK可以检测主从服务器网络状态,检测命令是否丢失。
主从复制虽然实现了负载均衡和数据备份,但是没有实现主服务器的高可用,一但主服务器故障,需要手动切换,非常不方便,所以redis还提供了哨兵和集群机制。
六、redis的哨兵机制介绍
redis哨兵机制核心共呢个是主节点自动故障转移(哨兵机制实际用的少,节点数量多还是推荐使用集群)
哨兵节点是特殊的redis节点,不存储数据
哨兵配置
通过配置sentinel monitor master服务器名称 masterip master端口号
哨兵实现原理
1、定时任务
每个哨兵节点维护3个定时任务
1.1:向主从节点发送info命令获取最新主从结构
1.2:通过发布订阅获取其他哨兵节点信息
1.3:通过向其他节点发送ping命令进行心跳检测,判断是否下线
2、主观下线:在心跳检测的定时任务中,如果其他节点超时未恢复,哨兵节点将其客观下线。
3、客观下线:哨兵节点对主节点进行主观下线后,通过sentinel is-master-down-by-addr命令询问其他哨兵该主节点状态,如果判断主节点下线的哨兵数量达到一定数值,对主节点进行客观下线。
客观下线是主节点才有的概念,从节点主观下线后,不会由后续客观下线和故障转移操作。因为哨兵主要监控主节点,保证主节点的高可用。
4、选举领导者哨兵节点:当主节点客观下线后,各个哨兵进行协商,选举一个领导者哨兵节点,由该领导者节点对主节点做故障转移操作。
选举采用Raft算法,基本思路是先到先得。
5、故障转移:领导者节点需要对主节点做故障转移
5.1:选择新的主节点:先过滤固件库的从节点,选择优先级最高的,其次是复制偏移量最大的,最后是runid最小的。
5.2:更新主从状态,通过slaveof no one(replicaof no one),当选出节点成为主节点,并将其他节点作为其从节点
5.3:已将下线的主节点重新上线后,会被设置为新的主节点的从节点。
一般哨兵数量不止一个,一共是奇数,便于投票决策。
哨兵机制虽然可以保证主节点的高可用,实现对主节点的自动故障转移,但是无法对从节点进行自动故障转移。在读写分离场景下,从节点故障会导致读服务不可用。并且哨兵也无法解决写操作的负载均衡,以及存储能力受到限制问题。需要使用集群来实现这些功能。
七、redis集群介绍
redis的集群方案解决了存储能力收单机限制,无法实现写操作负载均衡的问题,实现了较为完善的高可用方案。
集群的作用
1、数据分区:将数据分散到多个节点,突破单机内存限制,并且每个主节点都可以对外提供读写服务,提高了集群响应能力。
2、高可用:支持主从复制和主节点自动故障转移,当任一节点故障,集群仍然可以对外提供服务。
集群搭建
1、启动节点:将节点以集群方式启动,此时节点是独立的,互相没有联系
2、节点握手:让独立的节点练成网络
3、分配槽:将16384个槽分配给主节点
4、指定主从关系:为从节点指定主节点
基本原理
集群最主要的功能是数据分区。数据分区常见有顺序分区和哈希分区,哈希分区由于天然随机性,使用广泛。集群的分区方案也是用的哈希分区一种。
哈希分区的思路是:对数据特征值如key做哈希,根据哈希值决定数据落在哪个节点。常见哈希分区包括,哈希取余分区,一致性哈希分区,带虚拟节点的一致性哈希分区。
衡量数据分区好坏标准很多,最重要的是
1、数据是否分布均匀
2、增加和删除节点对数据分布的影响。
哈希取余分区思路:
通过计算key的hash值,对节点数量取余,决定数据映射到那个节点。数据是否分布均匀取决于哈希函数。但是该方案对节点变化很敏感,所有数据都需要重新计算映射关系,会引发大规模数据迁移。
一般不推荐
一致性哈希分区思路
一致性哈希算法将哈希值空间组织成一个虚拟的圆环,范围为0-2^32-1,对于每个key计算hash值,确定数据在环上位置,然以后从此位置顺时针往下找到第一台服务器,将数据映射到该服务器。
相比哈希取余分区,一致性哈希分区将增删节点影响限制在相邻节点。但是该方案在节点数量过少时,对单节点影响很大,可能造成数据严重不平衡。以三个节点为例,本来每个节点持有三分之一的数据,一旦一个
节点挂了。两外两个节点一个持有三分之一,一个持有三分之二。数据严重不平衡。
一般也不推荐
带虚拟节点的一致性哈希分区
一致性哈希分区缺点在于节点数量太少会造成数据不均匀,所以我们可以通过创建虚拟节点来增加节点数量。每个节点对应持有相对平均的虚拟节点。这样的话即使某个节点挂了,数据也可以被均匀分配到其他节点,
而不是分配到一个节点
redis集群使用的就是该方案。其中虚拟节点被成为槽。
数据之前的映射关系从hash->实际节点到hash->槽->实际节点。
槽数量远小于2^32,但远大于实际节点数量,一般为16384
数据分区的思路以及为何如此设计说的应该比较明白了
下面说下集群通信
节点间通信,按通信协议可以分为几种类型,单对单,广播,gossip协议等。
集群中单对单对资源消耗太大,一般不使用。
广播:向集群内所有节点发送消息,优点:集群收敛速度快,所有节点获取集群信息时一致的。缺点:每个消息都需要发送给所有节点,对cpu和网络消耗大
gossip协议:在节点有限的网络中,每个节点随机与部分节点通信,(不是真正随机,按照特定规则选择通信节点),经过一段杂乱无章的通信,每个节点状态达成一致
优点:比广播消耗资源少 缺点:集群收敛速度慢
集群节点采用固定频率每秒10次的定时任务进行通信相关工作;判断是否需要发送消息以及消息类型,确定接受节点,发送消息。
消息类型分为5种
meet消息:在节点握手节点,节点收到客户端的CLUSTER MEET命令,会向新加入的节点发送meet消息,请求将新节点加入集群。新节点收到后回复pong消息
ping消息:集群里每个节点每秒钟会选择部分节点发送PING消息,接收者收到消息后会回复一个PONG消息。PING消息的内容是自身节点和部分其他节点的状态信息;
作用是彼此交换信息,以及检测节点是否在线。PING消息使用Gossip协议发送,接收节点的选择兼顾了收敛速度和带宽成本,具体规则如下:
(1)随机找5个节点,在其中选择最久没有通信的1个节点
(2)扫描节点列表,选择最近一次收到PONG消息时间大于cluster_node_timeout/2的所有节点,防止这些节点长时间未更新。
pong消息:PONG消息封装了自身状态数据。可以分为两种:
第一种是在接到MEET/PING消息后回复的PONG消息;
第二种是指节点向集群广播PONG消息,这样其他节点可以获知该节点的最新信息,例如故障恢复后新的主节点会广播PONG消息。
fail消息:
当一个主节点判断另一个主节点进入FAIL状态时,会向集群广播这一FAIL消息;接收节点会将这一FAIL消息保存起来,便于后续的判断。
publish消息:
节点收到PUBLISH命令后,会先执行该命令,然后向集群广播这一消息,接收节点也会执行该PUBLISH命令。
节点需要专门的数据结构存储集群状态,最关键的结构是clusterNode,记录节点状态,clusterState记录集群整体状态
clusterNode
typedef struct clusterNode { //节点创建时间 mstime_t ctime; //节点id char name[REDIS_CLUSTER_NAMELEN]; //节点的ip和端口号 char ip[REDIS_IP_STR_LEN]; int port; //节点标识:整型,每个bit都代表了不同状态,如节点的主从状态、是否在线、是否在握手等 int flags; //配置纪元:故障转移时起作用,类似于哨兵的配置纪元 uint64_t configEpoch; //槽在该节点中的分布:占用16384/8个字节,16384个比特;每个比特对应一个槽:比特值为1,则该比特对应的槽在节点中;比特值为0,则该比特对应的槽不在节点中 unsigned char slots[16384/8]; //节点中槽的数量 int numslots; ………… } clusterNode;
clusterState
typedef struct clusterState { //自身节点 clusterNode *myself; //配置纪元 uint64_t currentEpoch; //集群状态:在线还是下线 int state; //集群中至少包含一个槽的节点数量 int size; //哈希表,节点名称->clusterNode节点指针 dict *nodes; //槽分布信息:数组的每个元素都是一个指向clusterNode结构的指针;如果槽还没有分配给任何节点,则为NULL clusterNode *slots[16384]; ………… } clusterState;
最后说明下集群握手和槽分配原理