redis概述

 

       Redis 是互联网技术领域使用最为广泛的存储中间件,它是「Remote Dictionary Service」的首字母缩写,也就是「远程字典服务」。Redis 以其超高的性能、完美的文档、 简洁易懂的源码和丰富的客户端库支持在开源中间件领域广受好评。

 

1.数据库的结构

  Redis 中的每个数据库,都由一个 redis.h/redisDb 结构表示:

typedef struct redisDb {
// 保存着数据库以整数表示的号码
int id;
// 保存着数据库中的所有键值对数据
// 这个属性也被称为键空间(key space)
dict *dict;
// 保存着键的过期信息
109
Redis 设计与实现, 第一版
dict *expires;
// 实现列表阻塞原语,如 BLPOP
// 在列表类型一章有详细的讨论
dict *blocking_keys;
dict *ready_keys;
// 用于实现 WATCH 命令
// 在事务章节有详细的讨论
dict *watched_keys;
} redisDb;

Redis 服务器初始化时, 它会创建出 redis.h/REDIS_DEFAULT_DBNUM 个数据库, 并将所有数据库保存到redis.h/redisServer.db 数组中, 每个数据库的 id 为从 0 到 REDIS_DEFAULT_DBNUM - 1 的值。

1.1数据库的切换

       当执行 SELECT number 命令时,程序直接使用 redisServer.db[number] 来切换数据库。

1.2数据库键空间

Redis 是一个键值对数据库(key-value pairs database),所以它的数据库本身也是一个字 典(俗称 key space):

  • 字典的键是一个字符串对象。
  • 字典的值则可以是包括字符串列表哈希表集合有序集在内的任意一种 Redis 类型 对象。

redisDb 结构的 dict 属性中,保存着数据库的所有键值对数据。

 

添加、删除、更新、取值等几个主要操作。

因为数据库本身是一个字典,所以对数据库的操作基本上都是对字典的操作,加上以下一些维护操作:

  • 更新键的命中率和不命中率,这个值可以用 INFO 命令查看;
  • 更新键的 LRU 时间,这个值可以用 OBJECT 命令来查看;
  • 删除过期键(稍后会详细说明);
  • 如果键被修改了的话,那么将键设为脏(用于事务监视),并将服务器设为脏(等待 RDB 保存);
  • 将对键的修改发送到 AOF 文件和附属节点,保持数据库状态的一致;

除了上面展示的键值操作之外,还有很多针对数据库本身的命令,也是通过对键空间进行处理来完成的:

  • FLUSHDB 命令:删除键空间中的所有键值对。
  • RANDOMKEY 命令:从键空间中随机返回一个键。
  • DBSIZE 命令:返回键空间中键值对的数量。
  • EXISTS 命令:检查给定键是否存在于键空间中。
  • RENAME 命令:在键空间中,对给定键进行改名。等等。

1.3键的过期时间

     在数据库中,所有键的过期时间都被保存在 redisDb 结构的 expires 字典里。expires 字典的键是一个指向 dict 字典(键空间)里某个键的指针,而字典的值则是键所指向的数据库键的到期时间,这个值以 long long 类型表示。

下图展示了一个含有三个键的数据库,其中 number book 两个键带有过期时间:

 

通过 expires 字典,可以用以下步骤检查某个键是否过期:

  1. 检查键是否存在于 expires 字典:如果存在,那么取出键的过期时间;
  2. 检查当前 UNIX 时间戳是否大于键的过期时间:如果是的话,那么键已经过期;否则, 键未过期。

过期键的清除:

  1. 定时删除:在设置键的过期时间时,创建一个定时事件,当过期时间到达时,由事件处理 器自动执行键的删除操作。
  2. 惰性删除:放任键过期不管,但是在每次从 dict 字典中取出键值时,要检查键是否过 期,如果过期的话,就删除它,并返回空;如果没过期,就返回键值。
  3. 定期删除:每隔一段时间,对 expires 字典进行检查,删除里面的过期键。

 

2.通讯模型

           Redis 是个单线程程序!他采用的是非阻塞多路复用IO模型<<网络IO模型>>.

 

指令队列

    Redis 会将每个客户端套接字都关联一个指令队列。客户端的指令通过队列来排队进行顺序处理,先到先服务。

应队列

    Redis 同样也会为每个客户端套接字关联一个响应队列。Redis 服务器通过响应队列来将指令的返回结果回复给客户端.

时任务

   Redis 的定时任务会记录在一个称为最小堆的数据结构中。这个堆中,最快要执行的任务排在堆的最上方。在每个循环周期,Redis 都会将最小堆里面已经到点的任务立即进行处理。处理完毕后,将最快要执行的任务还需要的时间记录下来,这个时间就是 select 系统调用的 timeout 参数。

管道本质

上图就是一个完整的请求交互流程图。我用文字来仔细描述一遍:

  1. 户端进程调用 write 将消息写到操作系统内核为套接字分配的发送缓冲 send buffer
  2. 户端操作系统内核将发送缓冲的内容发送到网卡网卡硬件将数据通际路由」送到服务器的网卡
  3. 务器操作系统内核将网卡的数据放到内核为套接字分配的接收缓冲 recv buffer
  4. 务器进程调用 read 从接收缓冲中取出消息进行处理
  5. 务器进程调用 write 将响应消息写到内核为套接字分配的发送缓冲 send buffer
  6. 务器操作系统内核将发送缓冲的内容发送到网卡网卡硬件将数据通际路由」送到客户端的网卡
  7. 户端操作系统内核将网卡的数据放到内核为套接字分配的接收缓冲 recv buffer
  8. 户端进程调用 read 从接收缓冲中取出消息返回给上层业务逻辑进行处理
  9. 结束

3.备份持久化

        Redis 支持两种形式的持久化,一种是RDB快照(snapshotting),另外一种是AOF(append-only-file)《持久化(Persistence)

RDB

    Redis 使用操作系统的多进程 COW(Copy On Write) 机制来实现快照持久化,这个机制 很有意思,也很少人知道。多进程 COW 也是鉴定程序员知识广度的一个重要指标.

Redis 在持久化时会调用 glibc 的函数 fork 产生一个子进程,快照持久化完全交给子进来处理,父进程继续处理客户端请求。子进程刚刚产生时,它和父进程共享内存里面的代段和数据段。这时你可以将父子进程想像成一个连体婴儿,共享身体(内存)

如果进程要对内存数据修改,这个时候就会使用操作系统的 COW 机制来进行数据段页面的分离。数据段是由很多操作系统的页面组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复制一份分离出来,然后对这个复制的页面进行修改。这时子进程相应的页面是没有变化的,还是进程产生时那一瞬间的数据。

子进程因为数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再也不会改变,这也是为什么 Redis 的持久化叫「快照」的原因。接下来子进程就可以非常安心的遍历数据了进行序列化写磁盘了。

AOF 

      AOF 日志存储的是 Redis 服务器的顺序指令序列,AOF 日志只记录对内存进行修改的 指令记录。

      假设 AOF 日志记录了自 Redis 实例创建以来所有的修改性指令序列,那么就可以通过对一个空的 Redis 实例顺序执行所有的指令,也就是「重放」,来恢复 Redis 当前实例的内存数据结构的状态。

     Redis 会在收到客户端修改指令后,先进行参数校验,如果没问题,就立即将该指令文本存储到 AOF 日志中,也就是先存到磁盘,然后再执行指令。这样即使遇到突发宕机,已经存储到 AOF 日志的指令进行重放一下就可以恢复到宕机前的状态。

    Redis 在长期运行的过程中,AOF 的日志会越变越长。如果实例宕机重启,重放整个AOF 日志会非常耗时,导致长时间 Redis 无法对外提供服务。所以需要对 AOF 日志瘦身。

    对 AOF 日志进行瘦身。其原理就是开辟一个子进程对内存进行遍历转换成一系列 Redis 的操作指令,序列化到一个新的 AOF 日志文件中。 序列化完毕后再将操作期间发生的增量 AOF 日志追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件了,瘦身工作就完成了。

RDB与AOF比较

RDB的优点

  • RDB是一个紧凑的单一内存文件,很方便传送与恢复且速度快(大数据)
  • RDB通过父进程fork出一个子进程来实现,父进程不需要再做其他IO操作,所以RDB持久化方式可以最大化redis的性能.

RDB的缺点

  • 如果你希望在redis意外停止工作(例如电源中断)的情况下丢失的数据较多的数据(主要看备份RDB的时间)
  • RDB 需要经常fork子进程来保存数据集到硬盘上,当数据集比较大的时候,fork的过程是非常耗时的,可能会导致Redis在一些毫秒级内不能响应客户端的请求.

AOF 优点

  • 使用AOF 会让你的Redis更加耐久: 你可以使用不同的fsync策略:无fsync,每秒fsync,每次写的时候fsync.默认使用每秒fsync策略,Redis的性能依然很好(fsync是由后台线程进行处理的,主线程会尽力处理客户端请求),一旦出现故障,你最多丢失1秒的数据.(根据官方的测试结果fsync时长和返回客户信息的时长接近,几乎不掉数据《consistency-test试验》)
  • AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂解析。 

AOF 缺点

  • 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。
  • 根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB 。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。 不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间(latency)。

混合持久化

        重启 Redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。

       在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升——混合持久化。

 

 

4.主从同步

        Redis 的主从数据是异步同步的,所以分布式的 Redis 系统并不满足「一致性」要求。 当客户端在 Redis 的主节点修改了数据后,立即返回,即使在主从网络断开的情况下,主节点依旧可以正常对外提供修改服务,所以 Redis 满足「可用性」。Redis 保证「最终一致性」,从节点会努力追赶主节点,最终从节点的状态会和主节点的状态将保持一致。如果网络断开了,主从节点的数据将会出现大量不一致,一旦网络恢复,从节点会采用多种策略努力追赶上落后的数据,继续尽力保持和主节点一致。《复制(Replication)

4.1增量同步

       Redis 同步的是指令流(AOF),主节点会将那些对自己的状态产生修改性影响的指令记录在本地的内存 buffer 中,然后异步将 buffer 中的指令同步到从节点,从节点一边执行同步的指令流来达到和主节点一样的状态,一遍向主节点反馈自己同步到哪里了 (偏移量)

     因为内存的 buffer 是有限的,所以 Redis 主库不能将所有的指令都记录在内存 buffer中。Redis 的复制内存 buffer 是一个定长的环形数组,如果数组内容满了,就会从头开始覆盖前面的内容。

 

     如果因为网络状况不好(其它情况),从节点在短时间内无法和主节点进行同步,那么当网络状况恢复时,Redis 的主节点中那些没有同步的指令在 buffer 中有可能已经被后续的指令覆盖掉 了,从节点将无法直接通过指令流来进行同步,这个时候就需要用到更加复杂的同步机制 — — 快照同步。

4.2快照同步

       快照同步是一个非常耗费资源的操作,它首先需要在主库上进行一次 bgsave 将当前内存的数据全部快照到磁盘文件中,然后再将快照文件的内容全部传送到从节点。从节点将快照文件接受完毕后,立即执行一次全量加载,加载之前先要将当前内存的数据清空。加载完毕后通知主节点继续进行增量同步。

     在整个快照同步进行的过程中,主节点的复制 buffer 还在不停的往前移动,如果快照同步的时间过长或者复制 buffer 太小,都会导致同步期间的增量指令在复制 buffer 中被覆盖,这样就会导致快照同步完成后无法进行增量复制,然后会再次发起快照同步,如此极有可能会陷入快照同步的死循环。所以务必配置一个合适的复制 buffer 大小参数,避免快照复制的死循环

4.3Sentinel

    目前我们讲的 Redis 还只是主从方案,最终一致性。读者们可思考过,如果主节点凌晨 3 点突发宕机怎么办?我们必须有一个高可用方案来抵抗节点故障,当故障发生时可以自动进行从主切换仿佛什么事也没发生一样。Redis 官方提供 了这样一种方案 —— Redis Sentinel(哨兵)《日志一致性协议Raft》。《高可用性(High Availability)

 

它负责持续监控主从节点的健康,当主节点挂掉时,自动选择一个最优的从节点切换为主节点。客户端来连接集群时,会首先连接 sentinel,通过 sentinel 来查询主节点的地址,然后再去连接主节点进行数据交互。当主节点发生故障时,sentinel 会将最新的主节点地址通知客户端。如此应用程序将无需重启即可自动完成节点切换。Sentinel 会持续监控已经挂掉了主节点,待它恢复后,原先挂掉的主节点现在变成了从节点,从新的主节点那里建立复制关系。

        Redis 主从采用异步复制,意味着当主节点挂掉时,从节点可能没有收到全部的同步消息,这部分未同步的消息就丢失了。如果主从延迟特别大,那么丢失的数据就可能会特别多。Sentinel 无法保证消息完全不丢失,但是也尽可能保证消息少丢失。它有两个选项可以限制主从延迟过大。

min-slaves-to-write 1
min-slaves-max-lag 10

第一个参数表示主节点必须至少有一个从节点在进行正常复制,否则就停止对外写服务,丧失可用性。
何为正常复制,何为异常复制?
这个就是由第二个参数控制的,它的单位是秒,表示如果 10s 没有收到从节点的反馈,就意味着从节点同步不正常,要么网络断开了,要么一直没有给反馈。

 

5.集群

      在大数据高并发场景下,单个 Redis 实例往往会显得捉襟见肘。首先体现在内存上,单个Redis的内存不宜过大,内存太大会导致 rdb 文件过大,进一步导致主从同步时全量同步时间过长,在实例重启恢复时也会消耗很长的数据加载时间,特别是在云环境下,单个实例内存往往都是受限的。其次体现在 CPU 的利用率上,单个 Redis 实例只能利用单个核心,这单个核心要完成海量数据的存取和管理工作压力会非常大。

    正是在这样的大数据高并发的需求之下,Redis 集群方案应运而生。它可以将众多小内 存的 Redis 实例综合起来,将分布在多台机器上的众多 CPU 核心的计算能力聚集到一起,完成海量数据存储和高并发读写操作。

5.1Codis

      Codis 使用 Go 语言开发,它是一个代理中间件,它和 Redis 一样也使用 Redis 协议对外提供服务,当客户端向 Codis 发送指令时,Codis 负责将指令转发到后面的 Redis 实例来执行,并将返回结果再转回给客户端。

       Codis 将所有的 key 默认划分为 1024 个槽位(slot),它首先对客户端传过来的 key 进行 crc32 运算计算哈希值,再将 hash 后的整数值对 1024 这个整数进行取模得到一个余数,这个余数就是对应 key 的槽位。每个槽位都会唯一映射到后面的多个 Redis 实例之一,Codis 会在内存维护槽位和Redis 实例的映射关系。这样有了上面 key 对应的槽位,那么它应该转发到哪个Redis 实例就很明确了。

槽位数量默认是 1024,它是可以配置的,如果集群节点比较多,建议将这个数值配置大一些,比如 20484096

不同的 Codis 实例之间槽位关系如何同步

       Codis 将槽位关系存储在 zk 中,并且提供了一个 Dashboard 可以用来观察和修改槽位关系,当槽位关系变化时,Codis Proxy 会监听到变化并重新同步槽位关系,从而实现多个Codis Proxy 之间共享相同的槽位关系配置。

扩容

      刚开始 Codis 后端只有一个 Redis 实例,1024 个槽位全部指向同一个 Redis。然后一个 Redis 实例内存不够了,所以又加了一个 Redis 实例。这时候需要对槽位关系进行调整,将一半的槽位划分到新的节点。这意味着需要对这一半的槽位对应的所有 key 进行迁移,迁移到新的 Redis 实例。

     Codis 通过 SLOTSSCAN 扫描出待迁移槽位的所有的 key,然后挨个迁移每个 key 到新的 Redis 节点。当 Codis 接收到位于正在迁移槽位中的 key 后,会立即强制对当前的单个 key 进行迁移,迁移完成后,再将请求转发到新的 Redis 实例

Codis 的代价

       Codis 给 Redis 带来了扩容的同时,也损失了其它一些特性。因为 Codis 中所有的 key分散在不同的 Redis 实例中,所以事务就不能再支持了,事务只能在单个 Redis 实例中完成。

5.2Cluster

    RedisCluster是Redis的亲儿子,它是去中心化的《Redis 集群教程》《Redis 集群规范》。如图所示:

  1. Redis Cluser采用虚拟槽分区,所有的键根据哈希函数映射到 0~16383整数槽内,计算公式:slot=CRC16(key)&16383。每一个节点负责维护一部分槽以及槽所映射的键值数据。
  2. 哈希槽配置信息在节点间交换信息的普通 ping 包和 pong 包中传输
  3. 当客户端向一个错误的节点发出了指令,该节点会发现指令的key所在的槽位并不归自己管理,这时它会向客户端发送一个特殊的跳转指令携带目标操作的节点地址,告诉客户端去连这个节点去获取数据。

一个节点要更新它的哈希槽表所要遵守的第一个规则如下:

规则 1:如果一个哈希槽是没有赋值的,然后有个已知节点认领它,那么我就会修改我的哈希槽表,把这个哈希槽和这个节点关联起来。

规则 2:如果一个哈希槽已经被赋值了,有个节点它的 configEpoch 比哈希槽当前拥有者的值更大,并且该节点宣称正在负责该哈希槽,那么我们会把这个哈希槽重新绑定到这个新节点上。

扩容迁移

    在迁移过程中,客户端访问的流程会有很大的变化。首先新旧两个节点对应的槽位都存在部分key数据。客户端先尝试访问旧节点,如果对应的数据还在旧节点里面,那么旧节点正常处理。如果对应的数据不在旧节点里面,那么有两种可能,要么该数据在新节点里,要么根本就不存在。旧节点不知道是哪种情况,所以它会向客户端返回一个重定向指令。

参考

《Redis深度历险:核心原理和应用实践》

《Redis设计与实现》

Redis官方文档

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值