• Redis学习笔记


    Redis

    1. 基础

    内存键值数据库,采用哈希表作为索引,一个哈希表,其实就是一个数组,数组的每个元素称为一个哈希桶。所以,我们常说,一个哈希表是由多个哈希桶组成的,每个哈希桶中保存了键值对数据。哈希桶中的元素保存的并不是值本身,而是指向具体值的指针。这也就是说,不管值是 String,还是集合类型,哈希桶中的元素都是指向它们的指针
    在这里插入图片描述
    Redis 解决哈希冲突的方式,就是链式哈希。链式哈希也很容易理解,就是指同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接。
    在这里插入图片描述
    当哈希冲突越来越多,会导致某些哈希冲突链过长,进而导致这个链上的元素查找耗时长,效率降低。所以,Redis 会对哈希表做 rehash操作。rehash 也就是增加现有的哈希桶数量,让逐渐增多的 entry 元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。

    1.1. Rehash的操作

    为了使 rehash 操作更高效,Redis 默认使用了两个全局哈希表:哈希表 1 和哈希表 2。一开始,当你刚插入数据时,默认使用哈希表 1,此时的哈希表 2 并没有被分配空间。随着数据逐步增多,Redis 开始执行 rehash,这个过程分为三步:

    1. 给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;
    2. 把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;
    3. 释放哈希表 1 的空间。
    1.1.1. 存在的问题

    这个过程看似简单,但是第二步涉及大量的数据拷贝,如果一次性把哈希表 1 中的数据都迁移完,会造成 Redis 线程阻塞,无法服务其他请求。此时,Redis 就无法快速访问数据了。

    1.1.2. 渐进式Rehash

    简单来说就是在第二步拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的entries。
    在这里插入图片描述

    Redis 能够在实际业务场景中得到广泛的应用,就是得益于支持多样化类型的 value。
    Redis通过网络框架访问键值数据库
    单线程
    在这里插入图片描述

    1.2 Redis中值的数据类型

    String(字符串)、List(列表)、Hash(哈希)、Set(集合)和 Sorted Set(有序集合)

    在这里插入图片描述

    1.2.1. 压缩列表介绍

    压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。和数组不同的是,压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。
    在这里插入图片描述

    1.2.2. 跳表

    有序链表只能逐一查找元素,导致操作起来非常缓慢,于是就出现了跳表。具体来说,跳表在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位,如下图所示:
    在这里插入图片描述

    1.2.3. 时间复杂度

    在这里插入图片描述

    Redis 是单线程,主要是指 Redis 的网络 IO和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的

    1.3. Redis为什么用单线程?

    因为多线程编程模式面临共享资源的并发访问控制问题,即使增加了线程,大部分线程也在等待获取访问共享资源的互斥锁,并行变串行,系统吞吐率并没有随着线程的增加而增加。而且,采用多线程开发一般会引入同步原语来保护共享资源的并发访问,这也会降低系统代码的易调试性和可维护性。为了避免这些问题,Redis 直接采用了单线程模式。

    1.4. Redis的单线程为什么那么快?

    一方面,Redis 的大部分操作在内存上完成,再加上它采用了高效的数据结构,例如哈希表和跳表,这是它实现高性能的一个重要原因。另一方面,就是 Redis 采用了多路复用机制,使其在网络 IO 操作中能并发处理大量的客户端请求,实现高吞吐率。

    1.4.1. 单线程网络IO
    1.4.1.1. 潜在的阻塞点

    在这里插入图片描述
    当 Redis监听到一个客户端有连接请求,但一直未能成功建立起连接时,会阻塞在 accept() 函数这里,导致其他客户端无法和 Redis 建立连接。类似的,当 Redis 通过 recv() 从一个客户端读取数据时,如果数据一直没有到达,Redis 也会一直阻塞在 recv()。这就导致 Redis 整个线程阻塞,无法处理其他客户端请求,效率很低。

    1.4.1.2. 非阻塞模式

    在 socket 模型中,不同操作调用后会返回不同的套接字类型。socket() 方法会返回主动套接字,然后调用 listen() 方法,将主动套接字转化为监听套接字,此时,可以监听来自客户端的连接请求。最后,调用 accept() 方法接收到达的客户端连接,并返回已连接套接字。

    在这里插入图片描述

    针对监听套接字,我们可以设置非阻塞模式:当 Redis 调用 accept() 但一直未有连接请求到达时,Redis 线程可以返回处理其他操作,而不用一直等待。但是,你要注意的是,调用 accept() 时,已经存在监听套接字了。虽然 Redis 线程可以不用继续等待,但是总得有机制继续在监听套接字上等待后续连接请求,并在有请求时通知 Redis。
    类似的,我们也可以针对已连接套接字设置非阻塞模式:Redis 调用 recv() 后,如果已连接套接字上一直没有数据到达,Redis 线程同样可以返回处理其他操作。我们也需要有机制继续监听该已连接套接字,并在有数据达到时通知 Redis。这样才能保证 Redis 线程,既不会像基本 IO 模型中一直在阻塞点等待,也不会导致 Redis无法处理实际到达的连接请求或数据。

    1.4.1.3. 多路复用机制

    用什么机制继续监听呢?

    IO 多路复用机制是指一个线程处理多个 IO 流,比如就是我们经常听到的linux的select/epoll 机制。

    简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个IO 流的效果。

    在这里插入图片描述
    为了在请求到达时能通知到 Redis 线程,select/epoll 提供了基于事件的回调机制, 一旦监测到 FD 上有请求到达时,针对不同事件的发生,调用相应的处理函数。

    1.5. 持久化

    Redis 的持久化模块能支持两种方式:日志(AOF)和快照(RDB)

    1.5.1. AOF日志

    写后日志。先执行命令,把数据写入内存,再记录日志
    在这里插入图片描述
    写后日志的好处是:先让系统执行命令,只有命令能执行成功,才会被记录到日志中,否则,系统就会直接向客户端报错。
    1. 避免出现记录错误命令的情况
    2. 不会阻塞当前的写操作

    1.5.1.1. 三种写回策略

    存在的风险

    1. 刚执行完命令还没记录日志就宕机了
    2. 单线程写日志可能会给下一操作带来阻塞风险

    基于此,AOF 机制给我们提供了三个选择,也就是 AOF 配置项appendfsync 的三个可选值。

    • Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
    • Everysec,每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
    • No,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓
      冲区,由操作系统决定何时将缓冲区内容写回磁盘。
      在这里插入图片描述
    1.5.1.2. 重写机制

    解决日志文件太大的问题
    Redis 根据数据库的现状创建一个新的 AOF 文件,旧日志文件中的多条命令,在重写后的新日志中变成了一条命令。

    一个拷贝,两处日志
    一个拷贝,主线程会fork出bgrewriteaof子进程,内存会拷贝一份,然后由bgrewriteaof子进程逐一把拷贝的数据写成操作,记入重写日志。
    两处日志,主线程仍然会处理新来的操作,此时的写操作记录在主线程的AOF日志的缓冲区中,即使宕机也能恢复,因为这个 AOF 日志的操作仍然是齐全的,可以用于恢复;等bgrewirteaof子进程拷贝完后,主进程的最新操作也会写入新的AOF文件

    在这里插入图片描述

    1.5.2. RDB快照

    把某一时刻的状态以文件的形式写到磁盘上,也就是快照。这样一来,即使宕机,快照文件也不会丢失,数据的可靠性也就得到了保证

    Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave。
    save:在主线程中执行,会导致阻塞;
    bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是Redis RDB 文件生成的默认配置。

    避免阻塞和正常处理写操作并不是一回事。此时,主线程的确没有阻塞,可以正常接收请求,但是,为了保证快照完整性,它只能处理读操作,因为不能修改正在执行快照的数据。

    1.5.2.1. 写时复制(copy-on-write)

    bgsave进程与主线程共享数据,但当主线程要修改数据时,会复制原来的数据生成一个副本,bgsave子进程就把这个副本数据写入RDB文件,这个过程中,主线程仍可以不受影响的修改数据
    在这里插入图片描述

    1.5.2.2. 频繁全量快照的问题与解决

    有两个方面的开销问题

    1. 频繁将全量数据写入磁盘,会给磁盘造成很大压力,多个快照竞争有限的磁盘带宽。
    2. bgsave子进程是由主线程fork出来的,fork这个操作本身会阻塞主线程,且内存越大,阻塞越久

    改进,使用增量快照
    在这里插入图片描述
    但使用增量快照需要额外的元数据信息去记住哪些数据被修改了,比如只有32字节的键值对,为了记录被修改的元数据信息可能就需要8字节,这就有点得不偿失了。

    况且,虽然快照恢复速度快,但是频率不好把握,如果频率太低,两次快照间一旦宕机,就可能有比较多的数据丢失。如果频率太高,又会产生额外开销。

    如何做到使用RDB的快速恢复,兼顾较小开销又尽量少丢数据呢?
    Redis 4.0 中提出了一个混合使用 AOF 日志和内存快照的方法。简单来说,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。
    在这里插入图片描述

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

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

    1.6. 数据同步

    为什么说Redis具有高可靠性?主要在以下两方面

    1. 数据尽量少丢失
    2. 服务尽量少中断

    那么,持久化的AOF和RDB是保证了数据尽量少丢失的。那如何保证服务尽量少中断呢?
    Redis的做法就是增加副本的冗余量,一份数据多个实例,即使其中一个实例出了异常,另外的实例也能正常使用。

    如何保证数据副本的一致性呢?

    1.6.1. Redis的主从库模式

    主从库采用读写分离的方式

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

    在这里插入图片描述

    1.6.1.1. 主从库第一次同步

    当启动多个Redis实例的时候,就可以通过replicaof(Redis5之前用slaveof)命令形成主从库的关系
    在从库上执行

    replicaof 主库ip 6379
    
    • 1

    之后会经历三个阶段来进行第一次同步
    在这里插入图片描述

    1. 从库给主库发送psync欲建立连接,其中这里后接的参数有两个,runID和offset
    • runID:每个库都会有一个唯一的runID标记这个实例。由于第一次连接不知道主机的runId,所以此时为?
    • offset:复制进度,此时为-1表示第一次进度
      主库收到后会返回FULLRESYNC命令,携带了主库的runID和数据的复制进度offset(第一次是全量数据复制)
    1. 主库会通过执行bgsave命令生成RDB文件,当从库接收到RDB文件后,首先清空数据库(怕之前的数据影响当前),再加载RDB文件文件。主库在生成RDB文件的过程中若有新的写请求的话会在内存中repilcation buffer记录。
    2. 把新的写请求发送给从库,从库再执行操作
      经过这三阶段,主从库就同步了
    1.6.1.2. 主库第一次数据同步的压力问题

    从以上分析不难发现,比较耗时的操作有两个

    • 生成RDB文件
    • 传输RDB文件
      如果从库数量很多,那么主库就会忙于fork子进程生成RDB文件,fork会阻塞主线程处理正常请求,导致响应应用程序速度变慢。传输RDB文件同样占用主库的网络带宽,对资源使用造成压力。
    1.6.1.3. 解决方式——“主-从-从模式”

    通过“主 - 从 - 从”模式将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库上

    具体就是选定一个从库来级联其他从库,让其他从库从这个指定的从库拉数据进行同步

    在这里插入图片描述
    如果主从库完成了全量复制,将会一直维护一个基于长连接的命令传播,可以避免频繁建立连接的开销。但这个过程中,存在网络断连或阻塞的风险点

    1. 6.1.4. 网络断连的解决方法——增量复制

    在Redis2.8前网络断连会重新进行全量复制,开销非常大
    在Redis2.8后,变成增量复制,把主从库网络断连期间主库收到的命令,同步给从库

    网络断连的增量复制奥妙在于 repl_backlog_buffer 这个缓冲区,之前提到这个缓冲区是建立第一次复制时第二阶段用来记录生成RDB文件期间新的写操作。

    repl_backlog_buffer 是一个环形缓冲区,主库会记录自己写到的位置,从库则会记录自己已经读到的位置。
    在这里插入图片描述
    主从库的连接恢复之后,从库首先会给主库发送 psync 命令,并把自己当前的偏移量发给主库,主库判断断连期间写的偏移量和从库当前的偏移量,把两个偏移量中间的命令给从库同步就行

    因为是个环形缓冲区,所以在缓冲区写满后,主库会继续写入,此时,就会覆盖掉之前写入的操作。如果从库的读取速度比较慢,就有可能导致从库还未读取的操作被主库新写的操作覆盖了,这会导致主从库间的数据不一致。

    解决办法是调整repl_backlog_size 这个参数。这个参数和所需的缓冲空间大小有关。缓冲空间的计算公式是:缓冲空间大小 = 主库写入命令速度 X 操作大小 - 主从库间网络传输命令速度 X 操作大小。在实际应用中,考虑到可能存在一些突发的请求压力,我们通常需要把这个缓冲空间扩大一倍,即repl_backlog_size = 缓冲空间大小 * 2,这也就是 repl_backlog_size 的最终值。

    1.6.2. 哨兵机制
    1.6.2.1. 主库故障问题

    在主库故障的时候通常会有三个问题

    1. 主库真的挂了吗?
    2. 该选择哪个从库作为主库?
    3. 怎么把新主库的相关信息通知给从库和客户端呢?

    哨兵机制是实现主从库自动切换的关键机制,它有效地解决了主从复制模式下故障转移的这三个问题。

    1.6.2.2. 哨兵机制基本流程

    哨兵其实就是一个运行在特殊模式下的 Redis 进程,主从库实例运行的同时,它也在运行。哨兵主要负责的就是三个任务:
    监控、选主(选择主库)和通知

    1. 监控:周期性给所有主从库发送ping命令,当没有在规定时间响应哨兵的ping命令时,哨兵将把它标记为下线状态,如果是主库,则触发自动切换主库流程
    2. 选主:主库挂了之后,按照一定规则从从库里挑一个出来当主库
    3. 通知:选举新主库后,把新主库的连接信息发给其他从库,让它们执行 replicaof 命令,和新主库建立连接,并进行数据复制。同时,哨兵会把新主库的连接信息通知给客户端,让它们把请求操作发到新主库上。
      在这里插入图片描述
      在监控任务中,哨兵需要判断主库是否处于下线状态;
      在选主任务中,哨兵也要决定选择哪个从库实例作为主库
    1.6.2.2.1. 如何判断主库的下线状态

    在监控中,从库一般标记为主观下线,但主库不能轻易被判断,需要判断为客观下线(既定事实的下线)才能下线,因为如果是哨兵的误判,后续的选主和通知会带来额外的开销

    如何减少误判:
    哨兵集群,使用少数服从多数的方式,当多数哨兵认为主库主观下线,则标记主库为客观下线
    在这里插入图片描述

    1.6.2.2.2. 选主流程

    哨兵按照一定筛选条件去掉不符合条件的从库,再按照一定规则进行打分,分最高的成为新主库
    在这里插入图片描述
    一定筛选条件:从库的在线状态、稳定的网络连接
    一定打分规则:从库配置的优先级、与主库的同步程度、id号最小的从库优先

    首先,哨兵会按照在线状态、网络状态,筛选过滤掉一部分不符合要求的从库,然后,依次按照优先级、复制进度、ID 号大小再对剩余的从库进行打分,只要有得分最高的从库出现,就把它选为新主库。

    1.6.2.3. 哨兵通信机制
    1.6.2.3.1. 哨兵如何互相发现

    哨兵实例通过以下命令连接主库

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

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

    哨兵实际上是通过订阅“sentinel:hello”的频道实现互相通信。只有订阅了同一个频道的应用,才能通过发布的消息进行信息交换

    在这里插入图片描述

    1.6.2.3.2. 哨兵是如何知道从库的 IP 地址和端口的呢?

    给主库发送 INFO 命令,主库接受到这个命令后,就会把从库列表返回给哨兵。接着,哨兵就可以根据从库列表中的连接信息,和每个从库建立连接,并在这个连接上持续地对从库进行监控。
    在这里插入图片描述

    1.6.2.3.3. 客户端怎么获取哨兵执行事件的流程进度

    基于哨兵自身的 pub/sub 功能,这实现了客户端和哨兵之间的事件通知。
    在这里插入图片描述
    订阅不同频道可以获取不同的信息

    让客户端从哨兵这里订阅消息了。具体的操作步骤是,客户端读取哨兵的配置文件后,可以获得哨兵的地址和端口,和哨兵建立网络连接。然后,我们可以在客户端执行订阅命令,来获取不同的事件消息

    1.6.2.3.4. 如何确定由哪个哨兵执行主从切换

    这里类似于主库的客观下线,也是一个投票的过程

    当一个哨兵判断主库主观下线之后,通过频道像其他哨兵发送送 is-master-down-by-addr 命令。接着,其他实例会根据自己和主库的连接情况,做出 Y 或 N 的响应,Y 相当于赞成票,N 相当于反对票。
    在这里插入图片描述
    一个哨兵获得了仲裁所需的赞成票数后,就可以标记主库为“客观下线”。赞成票数的规定数值是在配置文件中的quorum配置的,假如有5个哨兵,quorum配置的是3,那么这个哨兵至少要获得3张赞成票才能标记主库为客观下线。

    leader选举
    当获取了足够的赞成票后,这个哨兵会给其他哨兵发信息,表示自己想来执行这个主从切换的流程,先给自己投一张票,再让其他哨兵进行投票,投票选出来的就是leader。注意,每个哨兵只能投一次票,如果投给自己或别人了,就默认拒绝下次投票。只有获取半数以上的哨兵赞成票,才能成为leader
    在这里插入图片描述

  • 相关阅读:
    「浙江科聪新品发布」新品发布潜伏顶升式移动机器人专用控制器
    手撕520页PDF高级文档,成功“挤掉”7年开发架构师,牛逼
    更快更稳更安全!天翼云CDN了解一下
    java.lang.Enum类下equals(Object other)方法起什么作用呢?
    2023最新盲盒交友脱单系统源码
    科大讯飞2022秋招笔试知识点总结(超详细)
    键鼠自动化2.0展示
    Redis(05)| 数据结构-哈希表
    【MySQL】MySQL 5.7 的生命周期将于2023年结束,大家来拥抱 MySQL 5.8 吧
    SpringBoot自带序列化工具Jackson如何做到序列化忽略某个字段,反序列化忽略某个字段???
  • 原文地址:https://blog.csdn.net/weixin_41541562/article/details/126290446