redis学习笔记

最近在学习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;

最后说明下集群握手和槽分配原理

cluster meet

假设要向A节点发送cluster meet命令,将B节点加入到A所在的集群,则A节点收到命令后,执行的操作如下:

1)  A为B创建一个clusterNode结构,并将其添加到clusterState的nodes字典中

2)  A向B发送MEET消息

3)  B收到MEET消息后,会为A创建一个clusterNode结构,并将其添加到clusterState的nodes字典中

4)  B回复A一个PONG消息

5)  A收到B的PONG消息后,便知道B已经成功接收自己的MEET消息

6)  然后,A向B返回一个PING消息

7)  B收到A的PING消息后,便知道A已经成功接收自己的PONG消息,握手完成

8)  之后,A通过Gossip协议将B的信息广播给集群内其他节点,其他节点也会与B握手;一段时间后,集群收敛,B成为集群内的一个普通节点

通过上述过程可以发现,集群中两个节点的握手过程与TCP类似,都是三次握手:A向B发送MEET;B向A发送PONG;A向B发送PING。

cluster addslots

集群中槽的分配信息,存储在clusterNode的slots数组和clusterState的slots数组中,两个数组的结构前面已做介绍;二者的区别在于:前者存储的是该节点中分配了哪些槽,后者存储的是集群中所有槽分别分布在哪个节点。

cluster addslots命令接收一个槽或多个槽作为参数,例如在A节点上执行cluster addslots {0..10}命令,是将编号为0-10的槽分配给A节点,具体执行过程如下:

1)  遍历输入槽,检查它们是否都没有分配,如果有一个槽已分配,命令执行失败;方法是检查输入槽在clusterState.slots[]中对应的值是否为NULL。

2)  遍历输入槽,将其分配给节点A;方法是修改clusterNode.slots[]中对应的比特为1,以及clusterState.slots[]中对应的指针指向A节点

3)  A节点执行完成后,通过节点通信机制通知其他节点,所有节点都会知道0-10的槽分配给了A节点

原文地址:https://www.cnblogs.com/lgh344902118/p/14906252.html