redis基本大纲

数据结构与对象

基础数据结构

字符串 sds

  • 存储字符串,与原生的string相比,多了free和len两个字段,分别问未使用的长度以及已经使用的长度,两者之和为sds底层数组的真实长度,因此在append的时候如果free够就不需要重新分配内存了。
  • 和golang的slice类似

链表

  • 双向链表

字典

  • 普通的字典

跳跃表

  • 跳表,每个元素包含存储的元素和对应的分值,查找的时间复杂度为好的为logn,最差为n
  • 按分值有序

整数集合

  • 有序、无重复
  • 底层实现为int8数组,但元素不一定是int8,可能为int16或int32
  • 根据集合中元素的大小分配合适的空间,当全为int8的集合中加入int16的数字时,会将原来int8的数据全部转换为int16,再加入新的元素

压缩列表

  • 也是设计成一种节约内存的数据结构
  • 每个元素节点中包含前一节点的长度,因此可以快速实现从末尾往前的遍历;由于节约内存的缘故,包含的长度的内存大小也是可变的,因此可能出现新增/删除节点时需要对一系列相邻的元素的修改

redis中的对象

redis的类型与底层具体编码的关系如下图所示

redis的类型与底层具体编码的关系

字符串

  • 如果为整数,直接存储整数
  • 如果为长的string,直接存sds的string
  • 如果为短的string,存embstr
    • embstr与sds结构相当,区别在于sds包含xxxx

redis字符串不同编码的区别

各个实现中,如果元素的长度超过某一个值(一般是64字节)时,均会转换存储方式。将长度超过64字节记为事件1

列表

  • 数量少的时候为压缩列表
  • 数量多或者事件1的时候为链表

哈希

  • 元素少的时候为压缩列表,kv保存在相邻的地方
  • 元素多或者事件1的时候为字典

集合

  • 当元素全为数字且元素数量小于某值时,存储为整数集合
  • 元素多的时候为字典

有序集合

  • 当元素数量小时,为压缩列表
  • 当元素数量多或者事件1时,为跳跃表 & 字典

持久化

RDB

save或bgsave命令会触发redis生成rdb文件。其中,save命令会block整个redis进程,bgsave会使redis进程fork出一个子进程,通过子进程来生成rdb文件,同时,父进程能够正常处理请求。

Why does Redis use child processes instead of threads for background RDB persistence?Mainly for Redis performance reasons, we know that Redis's working model for responding to requests from clients is single-process and single-threaded, which can result in competing conditions for data if a thread is started within the main process.So to avoid using locks to degrade performance, Redis chose to start a new child process, have a separate memory copy of the parent process, and perform RDB persistence on that basis.

However, it is important to note that fork takes a certain amount of time, and the parent-child processes occupy the same amount of memory. When the Redis key value is large, fork takes a long time, during which Redis is unable to respond to other commands.In addition, Redis takes up twice as much memory space.

rdb文件本质上是将此时redis中所有的kv都通过特定的编码方式来写入文件中,在redis服务启动的时候,会通过rdb文件来恢复上次保存时redis的内存状态。

AOF

aof持久化通过将redis的写命令写入文件来存储内存状态。同样,redis在启动的时候,可以通过aof文件中顺序执行其中的命令来恢复redis的状态。

当aof功能被打开时,每次执行写操作,该操作都会被写入aof文件的缓冲区,具体缓冲区刷新进文件的时机由配置参数 appendfsync 决定,操作系统可以通过 fsync(fd)命令来强制将 fd写缓冲区中的数据给刷新到磁盘

aof缓冲区写入磁盘的时机

  • appendfsync == always:每次事件处理完成后,都将缓冲区数据刷新入磁盘
  • appendfsync == everysec:每隔一秒钟刷新一次,由子线程完成刷新动作
  • appendfsync == no:不主动刷新进磁盘,由操作系统来决定什么时候写入磁盘

aof重写

由于对一个key可能出现多个操作,所以aof文件中常常有冗余的命令,造成aof文件的大小很大。为了解决这个问题,会对aof文件进行重写。需要注意的是,这里的重写不涉及对原有aof文件的读取压缩操作,而是和rdb类似,遍历所有kv,并将这些kv解释为具体的修改命令,写入aof文件。

在aof后台重写的时候,先fork个子进程去写aof文件,同时,由于后台重写的过程中主进程仍然能接收请求,因此在aof fork子进程,到子进程完成aof文件这段时间内,主进程需要将这段时间的写请求写入某个buffer,在子进程完成aof文件发送信号给主进程后,主进程将buffer中的数据写入aof文件(这段时间内主进程是只做写aof文件这一件事,不接收请求),写完后主进程的数据和aof文件中的数据完全保持一致。

多机

Redis的多机实现主要有主从同步、sentinel以及集群三种。其中,主从同步里从服务器拷贝主服务器的数据,通过冗余数据的方式使从服务器也能接收读请求,提高了整体的性能;sentinel通过监视主从服务器的状态,在主服务器挂掉后,能及时发现并将某一从服务器提升为主服务器,进而能使整体继续提供服务;集群通过将数据进行分片,不同的数据由不同的服务器进行操作,横向提升了整体的性能

下面详细介绍三种多机技术的实现方式

主从同步

主从同步实现了主从服务器的数据同步,当服务器使用slaveof命令时,会成为主服务器的从服务器,数据之间的同步由sync/psync命令实现。旧版redis服务使用sync,新版换成了psync

sync

发送sync时,主服务器开始生成rdb文件,并记录生成过程中接收的写请求,rdb文件生成结束后,将rdb文件和过程中的写请求发送给从服务器,从而完成一致

缺点是,在从数据库断线后重新与主数据库建立主从同步时,主服务器仍然是生成rdb文件并发送给从数据库,即使主从之间可能只差了少量命令

psync

psync代表部分同步,在未建立连接的情况下,与sync工作方式一致

与sync不同的是,主从复制的过程中两方均会保存一个复制偏移量,同时主服务器保存了近期发送的写请求buffer以及对应请求的偏移量。在断线重连的时候,主服务器根据复制偏移量的不同以及从服务器的复制偏移量是否在buffer中决定是否部分重传

slaveof相关代码解释如下:

  • slaveof 命令:slaveof $ip $port,当前server成为 $ip:$port 的从服务器
    • 将状态设置完成后,server.repl_state 进入 REDIS_REPL_CONNECT 状态
    • replicationCron 定时事件中,如果 server.repl_state == REDIS_REPL_CONNECT 时,绑定主服务器fd的读写事件,事件handler为syncWithMaster,并将 server.repl_state 设为 REDIS_REPL_CONNECTING 状态
    • syncWithMaster中向server发送ping,等待master的pong,状态修改为REDIS_REPL_RECEIVE_PONG,发送ping是为了验证双方之间的套接字是否能正常读写以及主服务器是否能正常处理请求
    • 当收到主服务器的fd读事件的时候
    • 读pong返回
    • 向fd发送请求询问是否执行部分同步(下面以不能部分同步为例)
    • 向主服务器发送SYNC请求
    • 创建 temp-%d.%ld.rdb 的临时文件用于接收主服务器的rdb文件
    • 将handler修改为readSyncBulkPayload,监听主fd的读取事件
    • 状态修改为 REDIS_REPL_TRANSFER
    • 当接着收到fd读取事件的时候
    • 先读rdb文件总体的大小,写入 repl_transfer_size 字段
    • 将rdb文件读取进入之前产生的临时rdb文件
    • 如果接收的全部长度 == repl_transfer_size 时,将临时rdb文件原子的rename为dump.rdb
    • flush原来的db
    • 载入rdb文件
    • 在createClient中将主服务器的fd的handler重新注册成 readQueryFromClient
    • 状态修改为 REDIS_REPL_CONNECTED

sentinel

哨兵负责监控主服务器的健康状态,在主服务器挂掉的情况下会将从服务器升级为主服务器

哨兵在最开始启动时只知道主节点的地址,在与主节点进行通信的时候,主节点会将他的从节点以及监听他的哨兵信息发送给该哨兵。因此,哨兵通过与主节点进行通信,可以获取从节点以及其他哨兵的信息,进而与他们建立连接

主观下线

哨兵会以一定的频率向其他哨兵、主从服务器发送ping命令,根据返回的结果来判断其他服务器是否在线,如果超过一定时间相应的服务器还没有返回有效的回复,则哨兵认为该服务器主观下线

客观下线

当一个哨兵观察到一个主服务器下线的情况,他会向别的哨兵发送命令,询问是否他们也觉得该主服务器下线,当收到超过一定数量的确定后,表明该主服务器进入客观下线状态

故障转移

当主服务器被认定为客观下线时,需要在从服务器中选择一个服务器,把它设定为新的主服务器,以重新接受写请求。故障转移的流程如下:多个哨兵之间会根据 raft 协议来选定出一个主哨兵,之后主哨兵根据规则选取一个从服务器,将其提升为主服务器(对该从服务器使用slaveof no one命令,对其他从服务器使用slaveof该从服务器)

集群

集群中的节点通过cluster meet $ip $port来互相建立连接,整个数据库被分为16394个槽,每个节点负责其中的一些槽,负责的槽之间不重叠,当所有的槽都被分配给节点时,集群的状态为上线,可以对外提供服务。各个节点直接也会进行通信,告诉别的节点自己负责的槽有哪些

当有命令发送到集群某个节点时,该节点首先对命令的key做hash操作,并对16394取余,来确定该key应该是落在哪个槽中;随后,查找该槽是由哪个节点负责的,如果是自己负责,则处理该命令,否则,向客户端返回MOVED $slot $ip:$port,来通知客户端负责该key的节点的ip和port,客户端可以进而重新将命令发送给对应的节点

redis也支持槽的重分配,当添加个主节点或者下掉某个主节点时,就会用到重分配的功能。在重分配的过程中,会出现数据的转移,即某些数据需要重新被分配到新的节点中。在转移的过程中,会出现一部分数据在新的节点,而一部分由于没来得及转移完成,还留在老的节点。如果转移中进行了命令的处理,当命令发送到老的节点上,这时如果老节点发现数据还在,则直接处理请求;如果发现已经被转移到了新节点中,则发送ASK $slot $ip:$port命令来重定向该请求

其他功能

事务

multi关键字代表了事务的开启,在事务开启后,该客户端之后发送的请求都不会执行,只是单纯的被缓存下来,直到服务器收到exec或者discard请求。在收到exec后,服务器会将缓存的multi与exec之间的命令按顺序执行,执行的结果也按照顺序回复给客户端

另外,watch命令可以用来去监控某个key,如果在执行exec之前监控的key被修改了,则服务器将拒绝执行该事务

watch的实现:当使用watch来监控某个key时,服务端会保存哪些key被哪些客户端所监控了;如果现在有别的客户端修改了那些key,则会将对应的客户端的某个flag设置为true,在客户端执行exec操作时,如果服务端发现该客户端的flag为true,则证明事务执行的过程中,监控的某些key已经被修改了,因此会拒绝事务的执行

lua

在执行lua脚本时是原子操作,所以需要特别注意的是lua脚本一定得比较高效,不然如果很耗时的话整个redis服务器就在执行lua脚本,没法正常对外接受请求了,这也是为什么一般不建议用lua脚本的原因