公号:码农充电站pro

主页:https://codeshellme.github.io

在这里插入图片描述

这 3 篇文章是我在学习 Redis 的过程中,总结的笔记:

1,Redis 数据类型的底层结构

1.1,Redis 中的数据类型

Redis 中的数据类型及其特点:

在这里插入图片描述

Redis 还提供了 3 种扩展数据类型,分别是:

  • Bitmap:可以把 Bitmap 看作是一个 bit 数组
    • Bitmap 提供了 GETBIT/SETBIT 操作,使用一个偏移值 offset 对 bit 数组的某一个 bit 位进行读和写。
    • Bitmap 的偏移量是从 0 开始算的,也就是说 offset 的最小值是 0。
    • 当使用 SETBIT 对一个 bit 位进行写操作时,这个 bit 位会被设置为 1。
    • Bitmap 还提供了 BITCOUNT 操作,用来统计这个 bit 数组中所有“1”的个数。
    • Bitmap 支持用 BITOP 命令对多个 Bitmap 按位做“”“”“异或”的操作,操作的结果会保存到一个新的 Bitmap 中
  • HyperLogLog:这是一种用于统计基数的数据集合类型,它的最大优势就在于,当集合元素数量非常多时,它计算基数所需的空间总是固定的,而且还很小。
    • 常用命令:PFADD(添加元素)、PFCOUNT(统计数量)
    • 每个 HyperLogLog 只需要花费 12 KB 内存,就可以计算接近 2^64 个元素的基数。和元素越多就越耗费内存的 Set 和 Hash 类型相比,HyperLogLog 就非常节省空间。
    • 注意 HyperLogLog 的统计规则是基于概率的,所以它的统计结果是有一定误差的,标准误算率是 0.81%。这意味着,你使用 HyperLogLog 统计的 UV 是 100 万,但实际的 UV 可能是 101 万。如果需要精确统计结果,最好还是用 Set 或 Hash 类型。
  • GEO:GEO 类型的底层数据结构就是用 Sorted Set 来实现的。主要用于提供位置信息类服务,比如打车软件,附近的餐馆等。常用命令:
    • GEOADD 命令:用于把一组经纬度信息和相对应的一个 ID 记录到 GEO 类型集合中;
    • GEORADIUS 命令:会根据输入的经纬度位置,查找以这个经纬度为中心的一定范围内(可以自己定义)的其他元素。

1.2,全局哈希表

Redis 的高性能离不开高效的数据结构,其使用一个全局哈希表来存储所有的键值对:

在这里插入图片描述

1.3,数据类型的底层结构

Redis 中的 5 种数据类型及其对应的底层数据结构:

在这里插入图片描述

整数数组和双向链表

整数数组和双向链表通过数组下标或者链表的指针逐个元素访问,操作复杂度基本是 O(N),操作效率比较低;它们的优势是节省空间。

压缩列表

压缩列表类似于一个数组,数组中的每一个元素都保存一个数据。压缩列表在表头有三个字段,表尾有一个字段:

  • zlbytes:列表长度
  • zltail:列表尾的偏移量
  • zllen:列表中的元素个数
  • zlend:表示列表结束

在这里插入图片描述

压缩列表的查找效率:

  • 定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)
  • 而查找其他元素时,只能逐个查找,复杂度是 O(N)

跳表

跳表在链表的基础上,增加了多级索引,通过索引位置的几个跳转(在多级索引上跳来跳去),实现数据的快速定位(其查找复杂度是 O(logN)),如下图所示:

在这里插入图片描述

Hash 类型底层结构什么时候使用压缩列表,什么时候使用哈希表呢?

Redis 中有两个配置项:

  • hash-max-ziplist-entries:表示用压缩列表保存时,哈希集合中的最大元素个数
  • hash-max-ziplist-value:表示用压缩列表保存时,哈希集合中单个元素的最大长度

当 Hash 集合中写入的元素个数超过了 hash-max-ziplist-entries,或者写入的单个元素大小超过了 hash-max-ziplist-value,Redis 就会自动把 Hash 类型的实现结构由压缩列表转为哈希表。一旦从压缩列表转为了哈希表,Hash 类型就会一直用哈希表进行保存,而不会再转回压缩列表了。

1.4,哈希冲突

Redis 使用链式哈希的方式来解决哈希冲突。

在这里插入图片描述

哈希冲突链上的元素只能通过指针逐一查找再操作。当哈希表里写入的数据越来越多,就会导致某些哈希冲突链过长,进而导致这个链上的元素查找耗时长,效率降低。

1.5,rehash 操作

Redis 使用 Rehash 来处理哈希链过长的问题,通过增加现有的哈希桶数量,让逐渐增多的 entry 元素能在更多的桶之间分散保存。

为了使 rehash 操作更高效,Redis 默认使用了两个全局哈希表:

  • 哈希表 1
  • 哈希表 2

一开始,默认使用哈希表 1,此时的哈希表 2 并没有被分配空间。Redis 的 rehash 过程分三步:

  • 给哈希表 2 分配更大的空间(比如是当前哈希表 1 的两倍)
  • 把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中
    • 该过程涉及大量的数据拷贝,如果一次性把哈希表 1 中的数据都迁移完,会造成 Redis 线程阻塞,无法服务其他请求
    • 为了避免这个问题,Redis 采用了渐进式 rehash:把一次性大量拷贝的开销,分摊到了多次处理请求的过程中
      • 即拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。
  • 释放哈希表 1 的空间

渐进式 rehash 过程图如下:

在这里插入图片描述

2,Redis 的 IO 模型

通常所说的Redis 是单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供服务的主要流程。

但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,是由额外的线程执行的。

2.1,Redis 为什么使用单线程

Redis 为什么使用单线程,而不是多线程或多进程?

2020 年 5 月,Redis 6.0 的稳定版发布了,Redis 6.0 中提出了多线程模型。

在这里插入图片描述

多线程的主要问题在于:系统中通常会存在被多线程同时访问的共享资源,当有多个线程要修改这个共享资源时,就需要额外的机制进行保证全安性,从而带来了额外的开销。

2.2,多路复用机制

Redis 采用多路复用机制,使其在网络 IO 操作中能并发处理大量的客户端请求,实现高吞吐率。

在这里插入图片描述

在网络连接的建立(accept)、数据的读取(recv)和发送(send)都有可能导致 socket 阻塞,从而阻塞整个线程,影响并发处理能力。

所以,高效 IO 模型中 socket 必须是非阻塞的。

Socket 的非阻塞模式设置,主要体现在三个关键的函数调用上:

在这里插入图片描述

多路复用机制

Linux 中的 IO 多路复用机制是指,一个线程处理多个 IO 流,也就是 select/epoll 机制,该机制允许内核中,同时存在多个监听套接字和已连接套接字

select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数

3,Redis 的持久化

Redis 的持久化主要有两大机制,即 AOFAppend Only File)日志和 RDB 快照Redis DataBase)。

3.1,AOF 机制

AOF 叫做写后日志:先执行命令,把数据写入内存,然后才记录日志。

在这里插入图片描述

AOF 里记录的是 Redis 收到的每一条命令,这些命令是以文本形式保存的。

在这里插入图片描述

AOF 的优缺点:

  • 优点
    • Redis 在向 AOF 里面记录日志的时候,并不需要对命令进行语法检查,因为在内存中执行命令时,已经检查过。
    • Redis 是在命令执行后才记录日志,所以不会阻塞当前的写操作。
  • 缺点
    • 数据丢失风险:比如刚执行完一个命令,还没有来得及记日志就宕机了
    • AOF 虽然避免了对当前命令的阻塞,但可能会给下一个操作带来阻塞风险
      • 因为 AOF 日志也是在主线程中执行的,如果日志文件在写磁盘时,磁盘写压力大,就会导致写盘很慢,进而导致后续的操作也无法执行了

appendfsync 参数的三个可选值控制了 AOF 的写盘时机:

  • Always:每个写命令执行完,立马同步地将日志写回磁盘
    • 对于 always 策略来说,Redis 需要确保每个操作记录日志都写回磁盘,如果用后台子线程异步完成,主线程就无法及时地知道每个操作是否已经完成了,这就不符合 always 策略的要求了。
    • 所以,always 策略并不使用后台子线程来执行。
  • Everysec:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
    • fsync 的执行时间很长,如果是在 Redis 主线程中执行 fsync,就容易阻塞主线程
    • 所以,当写回策略配置为 everysec 时,Redis 会使用后台的子线程异步完成 fsync 的操作
  • No:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。

三种写回策略的优缺点:

在这里插入图片描述

AOF 重写机制

随着接收的写命令越来越多,AOF 文件会越来越大,这会引发三个问题:

  • 文件系统本身对文件大小有限制,无法保存过大的文件;
  • 如果文件太大,之后再往里面追加命令记录的话,效率也会变低;
  • 如果发生宕机,为了故障恢复,AOF 中记录的命令要一个个被重新执行,如果日志文件太大,整个恢复过程就会非常缓慢

AOF 文件是以追加的方式,逐一记录接收到的写命令的。当一个键值对被多条写命令反复修改时,AOF 文件会记录相应的多条命令

而在AOF 重写的时候,是根据这个键值对当前的最新状态,为它生成对应的写入命令。这样,一个键值对在重写日志中只用一条命令就行了。

在这里插入图片描述

AOF 重写过程是否会阻塞主线程

要把整个数据库的最新数据的操作日志都写回磁盘,是一个非常耗时的过程

因此,重写过程是由后台子进程 bgrewriteaof 来完成的,这避免了阻塞主线程,导致数据库性能下降。

每次执行重写时,主线程 fork 出后台的 bgrewriteaof 子进程。此时,fork 会把主线程的内存拷贝一份给 bgrewriteaof 子进程,这里面就包含了数据库的最新数据。

如果业务应用对延迟非常敏感,但同时允许一定量的数据丢失,那么,可以把配置项 no-appendfsync-on-rewrite 设置为 yes

no-appendfsync-on-rewrite yes
  • 这个配置项设置为 yes 时,表示在 AOF 重写时,不进行 fsync 操作。
    • 即 Redis 把写命令写到内存后,不调用后台线程进行 fsync 操作,就可以直接返回了。当然,如果此时实例发生宕机,就会导致数据丢失
  • 这个配置项设置为 no(默认配置)时,在 AOF 重写时,Redis 会调用后台线程进行 fsync 操作,这就会给实例带来阻塞

3.2,RDB 快照机制

内存快照,是指内存中的数据在某一个时刻的状态记录(RDB 记录的是某一时刻的数据,并不是操作)。

RDB 快照是指:把某一时刻的内存状态以文件的形式写到磁盘上,这个快照文件称为 RDB 文件

在做数据恢复时,可以直接把 RDB 文件读入内存,很快地完成恢复。

给内存的全量数据做快照,把它们全部写入磁盘会花费很多时间。而且,全量数据越多,RDB 文件就越大,往磁盘上写数据的时间开销就越大。

Redis 提供了两个命令来生成 RDB 文件,分别是 savebgsave

  • save:在主线程中执行,会导致阻塞
  • bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞(不影响整个系统的读写服务)
    • 这也是 Redis RDB 文件生成的默认配置
    • bgsave 子进程是由主线程 fork(会阻塞主线程) 生成的,可以共享主线程的所有内存数据。
    • bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件
      • 在写 RDB 文件的过程中,如果主线程对这些数据进行读操作,那么,主线程和 bgsave 子进程相互不影响。
      • 如果主线程要修改一块数据,那么,这块数据就会被复制一份,生成该数据的副本。然后,主线程在这个数据副本上进行修改。同时,bgsave 子进程可以继续把原来的数据写入 RDB 文件。这叫做写时复制技术Copy-On-Write, COW),由操作系统提供。

使用 INFO 命令查看 Redis 的 latest_fork_usec 指标值,表示最近一次 fork 的耗时。

写时复制的底层原理

主线程 fork 出 bgsave 子进程后,bgsave 子进程复制了主线程的页表(保存了所有数据)。当 bgsave 子进程生成 RDB 时,是根据页表读取这些数据,再写入磁盘中。

写时复制是指,主线程在有写操作时,才会把这个新写或修改后的数据写入到一个新的物理地址中,并修改自己的页表映射。

从下图中可看出,主线程修改了它自己的页表,而并没有影响子进程的页表。

在这里插入图片描述

多久做一次快照?

如果做快照的频率比较密集(比如一秒一次),则会带来两个问题:

  • 频繁将全量数据写入磁盘,会给磁盘带来很大压力
  • fork 操作本身会阻塞主线程,而且主线程的内存越大,阻塞时间越长

AOF 与 RDB 的另一个区别:

  • AOF 记录的是每一次写命令,数据最全,但文件体积大,数据恢复速度慢
  • RDB 采用二进制 + 数据压缩的方式写磁盘,这样文件体积小,数据恢复速度快

3.3,混合 AOF 与 RDB

Redis 4.0 中提出了一个混合使用 AOF 日志和内存快照的方法。

简单来说,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作;等到下一次做全量快照时,就可以清空上一次的 AOF 日志。

关于 AOF 和 RDB 的选择的三点建议:

  • 如果数据不能丢失时,内存快照和 AOF 的混合使用是一个很好的选择;
  • 如果允许分钟级别的数据丢失,可以只使用 RDB;
  • 如果只用 AOF,优先使用 everysec 的配置选项,因为它在可靠性和性能之间取了一个平衡。

4,Redis 集群原理

Redis 提供了主从库模式,以保证数据副本的一致,主从库之间采用的是读写分离的方式。

  • 对于读操作:主库、从库都可以接收;
  • 对于写操作:首先到主库执行,然后,主库将写操作同步给从库。

在这里插入图片描述

主从库模式采用读写分离,所有数据的修改只会在主库上进行。主库有了最新的数据后,会同步给从库,这样,主从库的数据就是一致的。

4.1,主从库之间的第一次数据同步

启动多个 Redis 实例的时候,它们相互之间就可以通过 replicaof(Redis 5.0 之前使用 slaveof)命令形成主库和从库的关系,之后会按照三个阶段完成数据的第一次同步。

例如,现在有:

  • 实例 1(172.16.19.3)
  • 实例 2(172.16.19.5)

在实例 2 上执行以下这个命令后,实例 2 就变成了实例 1 的从库,并从实例 1 上复制数据:

replicaof  172.16.19.3  6379

主从库之间的第一次数据同步:

在这里插入图片描述

psync 命令包含了主库的 runID 和复制进度 offset 两个参数:

  • runID:每个 Redis 实例启动时都会自动生成的一个随机 ID,用来唯一标记这个实例
    • 当从库和主库第一次复制时,因为不知道主库的 runID,所以将 runID 设为“?”
  • offset:此时设为 -1,表示第一次复制

FULLRESYNC 响应命令带上两个参数:主库 runID 和主库目前的复制进度 offset。

一旦主从库完成了全量复制,它们之间就会一直维护一个网络连接(长连接),主库会通过这个连接将后续陆续收到的命令操作再同步给从库。

4.2,主-从-从模式

如果从库比较多,那么数据的同步过程就会占用主库较多的资源。这时,我们可以使用主从从模式,该模式将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库上。

在这里插入图片描述

4.3,当主从间网络断开

在 Redis 2.8 之前,如果主从库在命令传播时出现了网络闪断,那么,从库就会和主库重新进行一次全量复制(开销非常大)。

从 Redis 2.8 开始,网络断了之后,主从库会采用增量复制的方式继续同步。

全量复制是同步所有数据,而增量复制只会把主从库网络断连期间主库收到的命令,同步给从库。

当主从库断连后,主库会把断连期间收到的写操作命令,写入 replication buffer,同时也会把这些操作命令也写入 repl_backlog_buffer 这个缓冲区。

repl_backlog_buffer 是一个环形缓冲区,主库会记录自己写到的位置(master_repl_offset),从库则会记录自己已经读到的位置(slave_repl_offset )。

正常情况下,这两个偏移量基本相等:

在这里插入图片描述

主从库的连接恢复之后,从库首先会给主库发送 psync 命令,并把自己当前的 slave_repl_offset 发给主库,主库只用把 master_repl_offset 和 slave_repl_offset 之间的命令操作同步给从库。

由于 repl_backlog_buffer 是一个环形缓冲区,在缓冲区写满后,主库会继续写入,此时,就会覆盖掉之前写入的操作,这会导致主从库间的数据不一致。

因此需要调整 repl_backlog_size 参数:

  • repl_backlog_size 调整为缓冲空间大小 * 2
  • 缓冲空间大小 = 主库写入命令速度 * 操作大小 - 主从库间网络传输命令速度 * 操作大小

增大 repl_backlog_size 只能缓解该问题,而不能彻底解决该问题。如果并发请求量非常大,连两倍的缓冲空间都存不下新操作请求的话,此时,主从库数据仍然可能不一致。针对这种情况:

  • 一方面,可以根据 Redis 所在服务器的内存资源再适当增加 repl_backlog_size 值,比如说设置成缓冲空间大小的 4 倍
  • 另一方面,可以考虑使用切片集群来分担单个主库的请求压力

4.4,Redis 主从切换原理

当主库挂了,就需要运行一个新主库,比如说把一个从库切换为主库。

在 Redis 主从集群中,哨兵机制是实现主从库自动切换的关键机制。

哨兵就是一个运行在特殊模式下的 Redis 进程,它主要复制三项任务:

  • 监控:判断主库是否真的挂了。哨兵进程在运行时,周期性地给所有的主从库发送 PING 命令,检测它们是否仍然在线运行。
    • 如果从库没有在规定时间内响应哨兵的 PING 命令,哨兵就会把它标记为“下线状态”;
    • 如果主库没有在规定时间内响应哨兵的 PING 命令,哨兵就会判定主库下线,然后开始自动切换主库的流程(选主)。
  • 选主:从很多个从库里,按照一定的规则选择一个从库实例,把它作为新的主库
  • 通知:把新主库的相关信息通知给从库和客户端
    • 哨兵会把新主库的连接信息发给其他从库,让它们执行 replicaof 命令,和新主库建立连接,并进行数据复制。
    • 哨兵会把新主库的连接信息通知给客户端,让它们把请求操作发到新主库上

如何判断主库下线

哨兵对主库的下线判断有“主观下线”和“客观下线”两种:

  • 如果发现从库对 PING 命令的响应超时了,哨兵就会先把它标记为主观下线
  • 如果检测的是主库,哨兵还不能简单地把它标记为“主观下线”(有可能判断错误)

为了减少误判,通常会采用多实例组成的集群模式进行部署,这也被称为哨兵集群。引入多个哨兵实例一起来判断,就可以避免单个哨兵因为自身网络状况不好,而误判主库下线的情况。

只有大多数的哨兵实例,都判断主库已经主观下线了,主库才会被标记为客观下线,接下来,哨兵会进行主从切换流程

客观下线的标准就是:

  • 当有 N 个哨兵实例时,要有 N/2 + 1 个实例判断主库为“主观下线”,才能最终判定主库为“客观下线”
  • 当然,有多少个实例做出“主观下线”的判断才行,可以由 Redis 管理员自行设定

哨兵如何选主

在多个从库中,先按照一定的筛选条件,把不符合条件的从库去掉。然后,我们再按照一定的规则,给剩下的从库逐个打分,将得分最高的从库选为新主库。

在这里插入图片描述

在选主时,除了要检查从库的当前在线状态,还要判断它之前的网络连接状态

如何之前的网络连接状态呢?使用配置项 down-after-milliseconds * 10

  • down-after-millisecond 是认定主从库断连的最大连接超时时间。如果在 down-after-milliseconds 毫秒内,主从节点都没有通过网络联系上,我们就认为主从节点断连了
  • 如果发生断连的次数超过了 10 次,就说明这个从库的网络状况不好,不适合作为新主库。

打分规则有三轮,只要在某一轮中,有从库得分最高,那么它就是主库了,选主过程到此结束。如果没有出现得分最高的从库,那么就继续进行下一轮。

  • 从库优先级:可以通过 slave-priority 配置项,给从库设置不同优先级。
    • 如果从库的优先级都一样,那么哨兵开始第二轮打分。
  • 从库复制进度:和旧主库同步程度最接近的从库得分高。
    • 哪个从库的 slave_repl_offset 最接近主库的 master_repl_offset,则得分最高
    • 如果无法区分,则进行第三轮打分
  • 从库 ID 号:每个实例都会有一个 ID,ID 号最小的从库得分最高,会被选为新主库。

4.5,哨兵集群

通过部署多个实例,就形成了一个哨兵集群,即使有某个哨兵实例出现故障挂掉了,其他哨兵还能继续协作完成主从库切换的工作。

在配置哨兵的信息时,我们只需要用到下面的这个配置项,设置主库的 IP 和端口,并没有配置其他哨兵的连接信息。

sentinel monitor <master-name> <ip> <redis-port> <quorum> 

哨兵实例之间可以相互发现,要归功于 Redis 提供的 pub/sub 机制,也就是发布 / 订阅机制

哨兵只要和主库建立起了连接,就可以在主库上发布消息了,比如说发布它自己的连接信息(IP 和端口)。同时,它也可以从主库上订阅消息,获得其他哨兵发布的连接信息。当多个哨兵实例都在主库上做了发布和订阅操作后,它们之间就能知道彼此的 IP 地址和端口。

在主从集群中,主库上有一个名为__sentinel__:hello的频道,不同哨兵就是通过它来相互发现,实现互相通信的。

在这里插入图片描述

哨兵通过向主库发送 INFO 命令来获取从库的信息,从而与从库建立连接,实现对从库的监控。

在这里插入图片描述

通过 pub/sub 机制,哨兵之间可以组成集群;同时,哨兵又通过 INFO 命令,获得了从库连接信息,也能和从库建立连接,并进行监控了。

哪个哨兵执行主从切换?

哨兵集群需要通过投票选出一个 Leader 去执行主从切换,选举过程(三个哨兵,quorum 为 2)如下:

在这里插入图片描述

quorum 是哨兵配置文件中的一个配置项,当某个哨兵的投票数达到 quorum 时,它才能称为 Leader。

在 T1 时刻,S1 判断主库为“客观下线”,它想成为 Leader,就先给自己投一张赞成票,然后分别向 S2 和 S3 发送命令,表示要成为 Leader。

在 T2 时刻,S3 判断主库为“客观下线”,它也想成为 Leader,所以也先给自己投一张赞成票,再分别向 S1 和 S2 发送命令,表示要成为 Leader。

哨兵选举领导者这个场景,使用的是 Raft 共识算法,因为它足够简单,且易于实现。

4.6,切片集群

Redis 切片集群用于保存海量数据,切片集群涉及到多个实例的分布式管理问题。

切片集群,也叫分片集群,是指启动多个 Redis 实例组成一个集群,然后按照一定的规则,把收到的数据划分成多份,每一份用一个实例来保存。

  • 如果把 25GB 的数据平均分成 5 份(也可以不做均分),使用 5 个实例来保存,每个实例只需要保存 5GB 数据

在这里插入图片描述

切片集群是一种保存大量数据的通用机制,从 3.0 开始,官方提供了一个名为 Redis Cluster 的方案,用于实现切片集群。

在 Redis 3.0 之前,Redis 官方并没有提供切片集群方案,但是,当时业界已经有了一些切片集群的方案,例如基于客户端分区的 ShardedJedis,基于代理的 CodisTwemproxy 等。

切片数据如何分布?

Redis Cluster 方案采用哈希槽(Hash Slot),来处理数据和实例之间的映射关系。在 Redis Cluster 方案中,一个切片集群共有 16384 个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中。

在部署 Redis Cluster 方案时,可以使用 cluster create 命令创建集群,此时,Redis 会自动把这些槽平均分布在集群实例上

  • 例如,如果集群中有 N 个实例,那么,每个实例上的槽个数为 16384/N 个。

当每个机器的硬件配置不一样时,也可以使用 cluster meet 命令手动建立实例间的连接,形成集群,再使用 cluster addslots 命令,指定每个实例上的哈希槽个数。

如下示例:

在这里插入图片描述

示意图中的切片集群一共有 3 个实例,同时假设有 5 个哈希槽。我们可以通过下面的命令手动分配哈希槽:

  • 实例 1 保存哈希槽 0 和 1
  • 实例 2 保存哈希槽 2 和 3
  • 实例 3 保存哈希槽 4
redis-cli -h 172.16.19.3 –p 6379 cluster addslots 0,1
redis-cli -h 172.16.19.4 –p 6379 cluster addslots 2,3
redis-cli -h 172.16.19.5 –p 6379 cluster addslots 4

注意:在手动分配哈希槽时,需要把 16384 个槽都分配完,否则 Redis 集群无法正常工作

在集群中,实例和哈希槽的对应关系并不是一成不变的,最常见的变化有两个:

  • 在集群中,实例有新增或删除,Redis 需要重新分配哈希槽;
  • 为了负载均衡,Redis 需要把哈希槽在所有实例上重新分布一遍。

4.7,主从同步中的问题

1,主从数据不一致

主从数据不一致,就是指客户端从从库中读取到的值和主库中的最新值并不一致;这是由于主从库间的命令复制是异步进行的,主库收到新的写命令后,会发送给从库。但是,主库并不会等到从库实际执行完命令后,再把结果返回给客户端,而是主库自己在本地执行完命令后,就会向客户端返回结果了。

如何知道主从库间的复制进度?

Redis 的 INFO replication 命令可以查看主库接收写命令的进度信息(master_repl_offset)和从库复制写命令的进度信息(slave_repl_offset),用 master_repl_offset 减去 slave_repl_offset,就能得到从库和主库间的复制进度差值了。

在这里插入图片描述

2,读到过期数据

Redis 中设置数据过期时间的命令一共有 4 个:

  • EXPIREPEXPIRE:它们给数据设置的是从命令执行时开始计算的存活时间
  • EXPIREAT 和 PEXPIREAT`:它们会直接把数据的过期时间设置为具体的一个时间点

在这里插入图片描述

一个数据过期后,应该是被删除的,客户端不能再读取到该数据。

但是有时候删除数据不及时,会导致读到过期数据,这是由 Redis 的删除过期数据的策略导致的。

Redis 有两种过期数据删除策略:

  • 惰性删除策略:当一个数据的过期时间到了以后,并不会立即删除数据,而是等到再有请求来读写这个数据时,对数据进行检查,如果发现数据已经过期了,再删除这个数据。
    • 如果客户端从主库上读取留存的过期数据,主库会触发删除操作,此时,客户端并不会读到过期数据。
    • 但是,从库本身不会执行删除操作,如果客户端在从库中访问留存的过期数据,从库并不会触发数据删除。那么,从库会给客户端返回过期数据吗?
      • Redis 3.2 之前的版本,从库在服务读请求时,并不会判断数据是否过期,而是会返回过期数据。
      • 在 3.2 版本后,如果读取的数据已经过期了,从库虽然不会删除,但是会返回空值,这就避免了客户端读到过期数据。
  • 定期删除策略:Redis 每隔一段时间(默认 100ms),就会随机选出一定数量的数据,检查它们是否过期,并把其中过期的数据删除。

当主从库全量同步时,如果主库接收到了一条 EXPIRE 命令,那么,主库会直接执行这条命令。这条命令会在全量同步完成后,发给从库执行。而从库在执行时,就会在当前时间的基础上加上数据的存活时间,这样一来,从库上数据的过期时间就会比主库上延后了

为了避免这种情况,建议在业务应用中使用 EXPIREAT/PEXPIREAT 命令,把数据的过期时间设置为具体的时间点,避免读到过期数据。

总结:

  • 主从数据不一致。Redis 采用的是异步复制,所以无法实现强一致性保证(主从数据时时刻刻保持一致),数据不一致是难以避免的。我给你提供了应对方法:保证良好网络环境,以及使用程序监控从库复制进度,一旦从库复制进度超过阈值,不让客户端连接从库。
  • 对于读到过期数据,这是可以提前规避的,一个方法是,使用 Redis 3.2 及以上版本;另外,你也可以使用 EXPIREAT/PEXPIREAT 命令设置过期时间,避免从库上的数据过期时间滞后。不过,这里有个地方需要注意下,因为 EXPIREAT/PEXPIREAT 设置的是时间点,所以,主从节点上的时钟要保持一致,具体的做法是,让主从节点和相同的 NTP 服务器(时间服务器)进行时钟同步。
3,不合理配置项导致服务挂掉

在这里插入图片描述

4.8,集群中脑裂

脑裂是指在主从集群中,同时有多个主节点,它们都能接收写请求。而脑裂最直接的影响,就是客户端不知道应该往哪个主节点写入数据,结果就是不同的客户端会往不同的主节点上写入数据。而且,严重的话,脑裂会进一步导致数据丢失。

5,将 Redis 用作缓存

5.1,缓存淘汰策略

一般来说,建议把缓存容量(Redis 容量)设置为总数据量的 15% 到 30%,兼顾访问性能和内存空间开销。

对于 Redis 来说,一旦确定了缓存最大容量,比如 4GB,你就可以使用下面这个命令来设定缓存的大小了:

CONFIG SET maxmemory 4gb

当缓存写满后(超过 maxmemory 值),如果再向缓存中写入数据,就涉及到数据的淘汰策略。Redis 中有 8 种数据淘汰策略:

  • 不进行数据淘汰:noeviction默认策略):一旦缓存被写满了,再有写请求来时,Redis 不再提供服务,而是直接返回错误
  • 在设置了过期时间的数据中进行淘汰,包括:
    • volatile-random:在设置了过期时间的键值对中,进行随机删除
    • volatile-ttl: 根据过期时间的先后进行删除,越早过期的越先被删除
    • volatile-lru:使用 LRU 算法筛选设置了过期时间的键值对
    • volatile-lfu(Redis 4.0 后新增):使用 LFU 算法选择设置了过期时间的键值对,它是在 LRU 算法的基础上,同时考虑了数据的访问时效性和数据的访问次数,可以看作是对淘汰策略的优化
  • 在所有数据范围内进行淘汰,包括:
    • allkeys-lru:使用 LRU 算法在所有数据中进行筛选
    • allkeys-random:从所有键值对中随机选择并删除数据
    • allkeys-lfu(Redis 4.0 后新增):使用 LFU 算法在所有数据中进行筛选

淘汰策略使用建议:

  • 优先使用 allkeys-lru 策略。这样可以把最近最常访问的数据留在缓存中,提升应用的访问性能。如果你的业务数据中有明显的冷热数据区分,建议使用 allkeys-lru 策略。
  • 如果业务应用中的数据访问频率相差不大,没有明显的冷热数据区分,建议使用 allkeys-random 策略,随机选择淘汰的数据就行。
  • 如果你的业务中有置顶的需求(比如置顶新闻、置顶视频),可以使用 volatile-lru 策略,同时不给这些置顶数据设置过期时间。这样一来,这些需要置顶的数据一直不会被删除,而其他数据会在过期时根据 LRU 规则进行筛选。

5.2,缓存中的数据和后端数据库中的不一致问题

在实际应用 Redis 缓存时,经常会遇到一些异常问题:

  • 缓存中的数据和数据库中的不一致
  • 缓存雪崩
  • 缓存击穿
  • 缓存穿透

5.3,缓存雪崩

缓存雪崩是指大量的应用请求无法在 Redis 缓存中进行处理,紧接着,应用将大量请求发送到数据库层,导致数据库层的压力激增。

有两种情况会导致缓存雪崩:

  • 缓存中有大量数据同时过期
  • Redis 缓存实例发生故障宕机

5.4,缓存击穿

缓存击穿是指,针对某个访问非常频繁的热点数据的请求,无法在缓存中进行处理,紧接着,访问该数据的大量请求,一下子都发送到了后端数据库,导致了数据库压力激增,会影响数据库处理其他请求。缓存击穿的情况,经常发生在热点数据过期失效时。

5.5,缓存穿透

缓存穿透是指要访问的数据既不在 Redis 缓存中,也不在数据库中,导致请求在访问缓存时,发生缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据。

此时,应用也无法从数据库中读取数据再写入缓存,来服务后续请求,这样一来,缓存也就成了“摆设”,如果应用持续有大量请求访问数据,就会同时给缓存和数据库带来巨大压力。

在这里插入图片描述

5.6,缓存污染

在一些场景下,有些数据被访问的次数非常少,甚至只会被访问一次。当这些数据服务完访问请求后,如果还继续留存在缓存中的话,就只会白白占用缓存空间。这种情况,就是缓存污染