分布式缓存:Redis

image.png

单线程模型
网络IO和键值对读写是单线程
持久化,异步删除,数据同步是额外线程执行。

为什么单线程模型这么快?
内存操作,CPU不是瓶颈
没有锁也就没有线程上下文切换的开销
网络IO多路复用提高吞吐量

IO多路复用技术

Redis IO模型


image.png
image.png

基于多路复用的 Redis IO 模型

image.png

思考题:基于以上的IO模型,在架构中应该避免如何使用Redis?

Redis的最终一致性

image.png

数据恢复
内存数据库解决数据丢失的方法
AOF(Append Only File) 日志
由Redis 主线程执行
image.png

以 Redis 收到“set testkey testvalue”命令后记录的日志为例,看看 AOF 日志的内容。其中 “*3”表示当前命令有三个部分,每部分都是由“ +数字”开头,后面紧跟着具体的命令、键或值。这里,“数字”表示这部分中的命令、键或值一共有多少字节。例如,“3 set”表示这部分有 3 个字节,也就是“set”命令。
image.png

三种写回策略(appendfsync)
Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘(阻塞主线程);
Everysec,每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘(不阻塞主线程);
No,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘(阻塞主线程)。

AOF 重写机制

合并AOF中的命令为一条,避免日志过大,便于恢复。


image.png

主线程 fork 出后台的 bgrewriteaof 子进程,bgrewriteaof 子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。重写阶段生成的日志继续放入缓冲中,子进程完成重写后向父进程发信号,最后由父进程信号处理函数合并成全日志并替换老的AOF日志。
处理过程:


image.png

思考题:
AOF重写fork了新线程,真的不会阻塞主线程处理吗?

内核fork一个线程要做的事:
不会copy父线程内存(Copy-on-Write)
拷贝内存页表(虚拟内存和物理内存的映射索引表)

为啥不直接利用AOF日志文件直接生成重写日志呢?

(CoW) Copy-on-Write(写时复制)

虚拟内存
物理内存
内存页表
Swap
在Linux操作系统下,会为每个进程分配一个虚拟内存(32位操作系统为4G)。通过页映射的方式让虚拟内存与物理内存(及硬盘的Swap)建立映射。
当进程真正需要使用内存的时候会申请虚拟内存,此时操作系统会将物理内存与进程的进程的虚拟内存建立绑定。代表该物理内存被进程所占用,但在进程的视野里只有虚拟内存。
同理,当进程主动释放内存的时候,也是释放虚拟内存,进而操作系统会将物理内存释放掉。


image.png

https://github.corp.ebay.com/taojin/distribution-system/blob/master/fork.cpp

在fork()调用之后,只会给子进程分配虚拟内存地址,但是映射到物理内存上都是同一块区域,子进程的代码段、数据段、堆栈都是指向父进程的物理空间。

image.png

父进程中所有对应的内存页都会被标记为只读,父子进程都可以正常读取内存数据,当其中某个进程需要更新数据时,检测到内存页是read-only的,内存管理单元(MMU)便会抛出一个页面异常中断,(page-fault),在处理异常时,内核便会把触发异常的内存页拷贝一份(其他内存页还是共享的一份),让父子进程各自持有一份。

子进程复制了主线程的页表,所以通过页表映射,能读到主线程的原始数据,而当有新数据写入或数据修改时,主线程会把新数据或修改后的数据写到一个新的物理内存地址上,并修改主线程自己的页表映射。所以,子进程读到的类似于原始数据的一个副本,而主线程也可以正常进行修改。

RDB快照
RDB(Redis Database): 和 AOF 相比,RDB 记录的是某一时刻的数据,并不是操作,所以,在做数据恢复时,我们可以直接把 RDB 文件读入内存,很快地完成恢复。

难点:
哪些数据需要快照?
多久快照一次?

Redis 的数据都在内存中,为了提供所有数据的可靠性保证,它执行的是全量快照

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

bgsave 在执行快照的同时,正常处理写操作。只保留快照时刻的数据。


image.png

快照间隔过长可能会丢失数据


image.png

思考题:过于频繁的快照会有什么问题?

全量数据写磁盘会对磁盘IO有较大影响,会造成前一个快照没结束后一个快照又开始了,形成恶性循环。
fork操作执行时,内核需要给子进程拷贝主线程的页表。如果主线程的内存大,页表也相应大,拷贝页表耗时长,会阻塞主线程。

AOF+RDB快照:
内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。


image.png

用AOF和RDB快照的不同场合:
数据不能丢失: AOF + RDB
如果允许分钟级别的数据丢失:RDB
允许秒级别数据丢失:AOF + Everysec

思考题:
一个写操作超过80%的Redis,做数据持久化有什么风险吗?
提示:Copy-on-write

主从库的数据同步

AOF 和RDB保证了单机的数据恢复问题,如何实现一个高可用的Redis呢?
数据尽量少丢失 (AOF, RDB)
二是服务尽量少中断 (多实例,多副本)

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

Redis的读写分离机制

image.png

读操作:主库、从库都可以接收
写操作:首先到主库执行,然后,主库将写操作同步给从库。
难点:如何主从同步?如何应对网络断连?

第一次同步

image.png

psync runId offset

RunID: 是每个 Redis 实例启动时都会自动生成的一个随机 ID,用来唯一标记这个实例。当从库和主库第一次复制时,因为不知道主库的 runID,所以将 runID 设为?。
Offset: 从库同步主库的数据状态,此时设为 -1,表示第一次复制。
一旦主从库完成了全量复制,它们之间就会一直维护一个网络连接,主库会通过这个连接将后续陆续收到的命令操作再同步给从库,这个过程也称为基于长连接的命令传播,可以避免频繁建立连接的开销。

思考题:这种同步方法下,如果从库过多会造成什么情况?

主从级联模式

image.png

解决网络断连

repl_backlog_buffer :环形缓冲区,主库会记录自己写到的位置,从库则会记录自己已经读到的位置。

image.png

主库的所有写命令除了传播给从库之外,都会在这个repl_backlog_buffer中记录一份,缓存起来,只有预先缓存了这些命令,当从库断连后,从库重新发送psync offset,主库才能通过 offset之后的增量数据给从库。

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


image.png

分布式主从模式中数据最终一致性的三种模式:全量复制、基于长连接的命令传播、增量复制。

思考题:

  • AOF 记录的操作命令更全,相比于 RDB 丢失的数据更少。那么,为什么主从库间的复制不使用 AOF 呢?
  • 为什么使用环形缓冲区,而不是一个队列?

哨兵机制

一旦主库挂了,需要如何选择新主库?
难点:
如何确定主库真的挂了?
如何选择新的主库?
如何让从库知道新的主库?如何让客户端知道新的主库?

哨兵 (Sentinal) 其实就是一个运行在特殊模式下的 Redis 进程,主从库实例运行的同时,它也在运行。哨兵主要负责的就是三个任务:监控、选主(选择主库)和通知。
监控:判断主库从库是否下线。
主观下线:ping不通从库,标志下线。
客观下线:当有 N 个哨兵实例时,有 N/2 + 1 个实例判断主库为“主观下线”。


image.png

选主:打分决定

  • 优先级配置决定(slave-priority 配置项)
  • Offset最接近决定 (offset单调递增)
  • 选从库runId最小的


    image.png

    多个哨兵的带来的新的问题:
    1.哨兵集群多数实例达成共识,判断出主库“客观下线”后,由哪个实例来执行主从切换呢?
    Raft算法

通知:
哨兵会把新主库的连接信息发给其他从库,让它们执行 replicaof 命令,和新主库建立连接,并进行数据复制
Redis的SDK都提供了通过哨兵拿到实例地址

思考题:
哨兵在操作主从切换的过程中,客户端能否正常地进行请求操作?有什么办法让应用程序不感知服务的中断?

  • 可读不可写。
  • 哨兵主库不响应时间配置(down-after-milliseconds),减少不必要的主从切换。
  • 客户端缓存/消息系统

Redis Sharding

为什么要做sharding(分区)

这跟 Redis 的持久化机制有关系。在使用 RDB 进行持久化时,Redis 会 fork 子进程来完成,fork 操作的用时和 Redis 的数据量是正相关的,而 fork 在执行时会阻塞主线程。数据量越大,fork 操作造成的主线程阻塞的时间越长。所以,在使用 RDB 对 大的内存 的数据进行持久化时,数据量较大,后台运行的子进程在 fork 创建时阻塞了主线程,于是就导致 Redis 响应变慢了。

纵向扩展:升级单个 Redis 实例的资源配置,包括增加内存容量、增加磁盘容量、使用更高配置的 CPU。就像下图中,原来的实例内存是 8GB,硬盘是 50GB,纵向扩展后,内存增加到 24GB,磁盘增加到 150GB。

横向扩展:横向增加当前 Redis 实例的个数,原来使用 1 个 8GB 内存、50GB 磁盘的实例,现在使用三个相同配置的实例。


image.png

难点:
数据切片后,在多个实例之间如何分布?
客户端怎么确定想要访问的数据在哪个实例上?

首先根据键值对的 key,按照CRC16 算法计算一个 16 bit 的值;然后,再用这个 16bit 值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽。每个Redis实例都平均分配哈希槽数目(16384/N)。
Redis 实例会把自己的哈希槽信息发给和它相连接的其它实例,来完成哈希槽分配信息的扩散。当实例之间相互连接后,每个实例就有所有哈希槽的映射关系了。客户端收到哈希槽信息后,会把哈希槽信息缓存在本地。当客户端请求键值对时,会先计算键所对应的哈希槽,然后就可以给相应的实例发送请求了。客户端支持请求重定向。


image.png

思考题:
为什么要加入哈希槽,而不是直接存key和节点的关系表?

  1. Key数目多会导致这个关系表变得巨大。
  2. 为了支持重定向,节点间交换数据量非常大。
  3. 集群扩容缩容,节点有变化,关系表维护成本巨大。

你可能感兴趣的:(分布式缓存:Redis)