Redis

Redis

入门

问题现象:针对的是海量用户,高并发

为什么需要redis?

首先说关系型数据库的自身瓶颈:

 性能瓶颈:磁盘IO性能低下
 扩展瓶颈:数据关系复杂,扩展性差,不便于大规模集群

解决思路:

降低磁盘IO次数,越低越好 —— 内存存储

去除数据间关系,越简单越好 —— 不存储关系,仅存储数据

这就是NoSQL:即 Not-Only SQL( 泛指非关系型的数据库),作为关系型数据库的补充 。

常见 Nosql 数据库:
 Redis
 memcache
 HBase
 MongoDB

NoSQL作用: 应对基于海量用户和海量数据前提下的数据处理问题

NoSQL特征:
 可扩容,可伸缩
 大数据量下高性能
 灵活的数据模型
 高可用

一般都是MySQL做存储,redis作缓存

Redis

概念: Redis (REmote DIctionary Server) 是用 C 语言开发的一个开源的高性能键值对( key-value)数据库

特征:

  1. 数据间没有必然的关联关系
  2. 内部采用单线程机制进行工作
  3. 高性能。官方提供测试数据, 50个并发执行100000 个请求,读的速度是110000 次/s,写的速度是81000次/s。
  4. 多数据类型支持
     字符串类型 string
     列表类型 list
     散列类型 hash
     集合类型 set
     有序集合类型 sorted_set
  5. 持久化支持。可以进行数据灾难恢复

Redis 的应用

为热点数据加速查询(主要场景),如热点商品、热点新闻、热点资讯、推广类等高访问量信息等
任务队列,如秒杀、抢购、购票排队等
即时信息查询,如各位排行榜、各类网站访问统计、公交到站信息、在线人数信息(聊天室、网站)、设
备信号等
时效性信息控制,如验证码控制、投票控制等
分布式数据共享,如分布式集群架构中的 session 分离
消息队列
分布式锁

Redis的下载与安装

Redis 的下载与安装
Linux 版
 Redis 高级开始使用
 以4.0 版本作为主版本
(适用于企业级开发)

Redis 的下载
Windows 版本
 Redis 入门使用
 以 3.2 版本作为主版本
 下载地址: https://github.com/MSOpenTech/redis/tags

核心文件:
 redis-server.exe 服务器启动命令
 redis-cli.exe 命令行客户端
 redis.windows.conf redis核心配置文件
 redis-benchmark.exe 性能测试工具
 redis-check-aof.exe AOF文件修复工具
 redis-check-dump.exe RDB文件检查工具( 快照持久化文件)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-05NTrMNj-1619617395471)(C:\Users\75666\AppData\Roaming\Typora\typora-user-images\1617174987621.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QHAR4Tls-1619617395475)(C:\Users\75666\AppData\Roaming\Typora\typora-user-images\1617175027170.png)]

Redis 的基本操作

信息查询

功能:根据 key 查询对应的 value,如果不存在,返回空( nil)

命令

get key

范例

get name

清除屏幕信息

clear

退出客户端命令行模式

quit
exit
<ESC>

功能:获取命令帮助文档,获取组中所有命令信息名称

help 命令名称
help @组名

redis单线程

redis单线程:主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程

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

额外插入:

多线程的开销:“使用多线程,可以增加系统吞吐率,或是可以增加系统扩展性。” 如果对于一个多线程的系统来说,在有合理的资源分配的情况下,可以增加系统中处理请求操作的资源实体,进而提升系统能够同时处理的请求数,即吞吐率。如左图。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S7Uq6cPs-1619617395479)(C:\Users\75666\AppData\Roaming\Typora\typora-user-images\1617505350793.png)]

但是,通常情况下,在我们采用多线程后,如果没有良好的系统设计,实际得到的结果,其实是右图所展示的那样。我们刚开始增加线程数时,系统吞吐率会增加,但是,再进一步增加线程时,系统吞吐率就增长迟缓了,有时甚至还会出现下降的情况。

为什么?多个线程访问共享资源。

一个关键的瓶颈在于,系统中通常会存在被多线程同时访问的共享资源,比如一个共享的数据结构。当有多个线程要修改这个共享资源时,为了保证共享资源的正确性,就需要有额外的机制进行保证,而这个额外的机制,就会带来额外的开销。

多线程编程模式面临的共享资源的并发访问控制问题。

如果没有精细的设计,比如说,只是简单地采用一个粗粒度互斥锁,就会出现不理想的结果:即使增加了线程,大部分线程也在等待获取访问共享资源的互斥锁,并行变串行,系统吞吐率并没有随着线程的增加而增加。

而且,采用多线程开发一般会引入同步原语来保护共享资源的并发访问,这也会降低系统代码的易调试性和可维护性。为了避免这些问题,Redis 直接采用了单线程模式。

单线程 Redis 为什么那么快?

内存上处理,高效率的数据结构,比如哈希表和跳表。

采用多路复用IO。

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

首先,我们要弄明白网络操作的基本 IO 模型和潜在的阻塞点。毕竟,Redis 采用单线程进行 IO,如果线程被阻塞了,就无法进行多路复用了。

插入:

基本 IO 模型与阻塞点

以 Get 请求为例,SimpleKV (模拟redis的)为了处理一个 Get 请求,需要监听客户端请求(bind/listen),和客户端建立连接(accept),从 socket 中读取请求(recv),解析客户端发送请求(parse),根据请求类型读取键值数据(get),最后给客户端返回结果,即向 socket 中写回数据(send)。下图显示了这一过程,其中,bind/listen、accept、recv、parse 和 send 属于网络 IO 处理,而 get 属于键值数据操作。既然 Redis 是单线程,那么,最基本的一种实现是在一个线程中依次执行上面说的这些操作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CL6BkhOm-1619617395483)(C:\Users\75666\AppData\Roaming\Typora\typora-user-images\1617505986363.png)]

网络IO中潜在的阻塞点,分别是 accept() 和 recv()。

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

所以采用网络复用IO,支持非阻塞模式。

非阻塞模式

Socket 网络模型的非阻塞模式设置,主要体现在三个关键的函数调用上,如果想要使用 socket 非阻塞模式,就必须要了解这三个函数的调用返回类型和设置模式。

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

基于多路复用的高性能 I/O 模型

Linux 中的 IO 多路复用机制是指一个线程处理多个 IO 流。例如:select/epoll 机制。

简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。

内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。

Redis 网络框架调用 epoll 机制,让内核监听这些套接字。此时,Redis 线程不会阻塞在某一个特定的监听或已连接套接字上,也就是说,不会阻塞在某一个特定的客户端请求处理上。正因为此,Redis 可以同时和多个客户端连接并处理请求,从而提升并发性。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6KWo32Xa-1619617395486)(C:\Users\75666\AppData\Roaming\Typora\typora-user-images\1617506650937.png)]上图就是基于多路复用的 Redis IO 模型。图中的多个 FD 就是刚才所说的多个套接字。

为了在请求到达时能通知到 Redis 线程,select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数。

那么,回调机制是怎么工作的呢?其实,select/epoll 一旦监测到 FD 上有请求到达时,就会触发相应的事件

这些事件会被放进一个事件队列,Redis 单线程对该事件队列不断进行处理。这样一来,Redis 无需一直轮询是否有请求实际发生,这就可以避免造成 CPU 资源浪费。同时,Redis 在对事件队列中的事件进行处理时,会调用相应的处理函数,这就实现了基于事件的回调。因为 Redis 一直在对事件队列进行处理,所以能及时响应客户端请求,提升 Redis 的响应性能。

以连接请求和读数据请求为例:

这两个请求分别对应 Accept 事件和 Read 事件,Redis 分别对这两个事件注册 accept 和 get 回调函数。当 Linux 内核监听到有连接请求或读数据请求时,就会触发 Accept 事件和 Read 事件,此时,内核就会回调 Redis 相应的 accept 和 get 函数进行处理。

持久化

redis是因为在内存中存贮,一旦宕机,数据就丢失了。所以,持久化来了!!!

目前,Redis 的持久化主要有两大机制,即 AOF(Append Only File)日志和 RDB 快照。

AOF 日志是如何实现的?

说到日志,我们比较熟悉的是数据库的写前日志(Write Ahead Log, WAL),也就是说,在实际写数据前,先把修改的数据记到日志文件中,以便故障时进行恢复。不过,AOF 日志正好相反,它是写后日志,“写后”的意思是 Redis 是先执行命令,把数据写入内存,然后才记录日志,如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5lQIpHI6-1619617395488)(C:\Users\75666\AppData\Roaming\Typora\typora-user-images\1617669640573.png)]

为什么要用写后日志呢?

  1. 因为为了避免额外的检查开销,Redis 在向 AOF 里面记录日志的时候,并不会先去对这些命令进行语法检查。所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错。而写后日志这种方式,就是先让系统执行命令,只有命令能执行成功,才会被记录到日志中,否则,系统就会直接向客户端报错。可以避免出现记录错误命令的情况。

  2. AOF 还有一个好处:它是在命令执行后才记录日志,所以不会阻塞当前的写操作。

风险

  1. 如果刚执行完一个命令,还没有来得及记日志就宕机了,那么这个命令和相应的数据就有丢失的风险。

(如果此时 Redis 是用作缓存,还可以从后端数据库重新读入数据进行恢复,但是,如果 Redis 是直接用作数据库的话,此时,因为命令没有记入日志,所以就无法用日志进行恢复了。)

  1. AOF 虽然避免了对当前命令的阻塞,但可能会给下一个操作带来阻塞风险。

    (AOF 日志也是在主线程中执行的,如果在把日志文件写入磁盘时,磁盘写压力大,就会导致写盘很慢,进而导致后续的操作也无法执行)

三种写回策略

其实,对于这个问题,AOF 机制给我们提供了三个选择,也就是 AOF 配置项 appendfsync 的三个可选值。Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;

Everysec,每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;

No,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。

但是上述方法还是或多或少有点儿不够完美。

原因分析:

  1. “同步写回”可以做到基本不丢数据,但是它在每一个写命令后都有一个慢速的落盘操作,不可避免地会影响主线程性能;

  2. “每秒写回”采用一秒写回一次的频率,避免了“同步写回”的性能开销,虽然减少了对系统性能的影响,但是如果发生宕机,上一秒内未落盘的命令操作仍然会丢失。所以,这只能算是,在避免影响主线程性能和避免数据丢失两者间取了个折中。

  3. 虽然“操作系统控制的写回”在写完缓冲区后,就可以继续执行后续的命令,但是落盘的时机已经不在 Redis 手中了,只要 AOF 记录没有写回磁盘,一旦宕机对应的数据就丢失了;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-huMOai9H-1619617395490)(C:\Users\75666\AppData\Roaming\Typora\typora-user-images\1617670528284.png)]

一定要小心 AOF 文件过大带来的性能问题。

毕竟,AOF 是以文件的形式在记录接收到的所有写命令。随着接收的写命令越来越多,AOF 文件会越来越大。

这里的“性能问题”,主要在于以下三个方面:一是,文件系统本身对文件大小有限制,无法保存过大的文件;二是,如果文件太大,之后再往里面追加命令记录的话,效率也会变低;三是,如果发生宕机,AOF 中记录的命令要一个个被重新执行,用于故障恢复,如果日志文件太大,整个恢复过程就会非常缓慢,这就会影响到 Redis 的正常使用。

所以,我们就要采取一定的控制手段,这个时候,AOF 重写机制就登场了。

重写机制

重写机制:AOF 重写机制就是在重写时,Redis 根据数据库的现状创建一个新的 AOF 文件,也就是说,读取数据库中的所有键值对,然后对每一个键值对用一条命令记录它的写入。

比如说,当读取了键值对“testkey”: “testvalue”之后,重写机制会记录 set testkey testvalue 这条命令。这样,当需要恢复时,可以重新执行该命令,实现“testkey”: “testvalue”的写入。

日志文件太大了怎么办?

为什么重写机制可以把日志文件变小呢?

实际上,**重写机制具有“多变一”功能。**所谓的“多变一”,也就是说,旧日志文件中的多条命令,在重写后的新日志中变成了一条命令。

我们知道,AOF 文件是以追加的方式,逐一记录接收到的写命令的。当一个键值对被多条写命令反复修改时,AOF 文件会记录相应的多条命令。但是,在重写的时候,是根据这个键值对当前的最新状态,为它生成对应的写入命令。这样一来,一个键值对在重写日志中只用一条命令就行了,而且,在日志恢复时,只用执行这条命令,就可以直接完成这个键值对的写入了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IGUILeA5-1619617395492)(C:\Users\75666\AppData\Roaming\Typora\typora-user-images\1617672476922.png)]

当我们对一个列表先后做了 6 次修改操作后,列表的最后状态是[“D”, “C”, “N”],此时,只用 LPUSH u:list “N”, “C”, "D"这一条命令就能实现该数据的恢复,这就节省了五条命令的空间。对于被修改过成百上千次的键值对来说,重写能节省的空间当然就更大了。

虽然 AOF 重写后,日志文件会缩小,但是,要把整个数据库的最新数据的操作日志都写回磁盘,仍然是一个非常耗时的过程。那么,重写会不会阻塞主线程?

AOF 重写会阻塞吗?

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

AOF 日志是由主线程写回。

此处总结为**“一个拷贝,两处日志”。**

​ **“一个拷贝”**就是指,每次执行重写时,主线程 fork 出后台的 bgrewriteaof 子进程。此时,fork 会把主线程的内存拷贝一份给 bgrewriteaof 子进程,这里面就包含了数据库的最新数据。然后,bgrewriteaof 子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。

“两处日志”

因为主线程未阻塞,仍然可以处理新来的操作。此时,**如果有写操作,第一处日志就是指正在使用的 AOF 日志,Redis 会把这个操作写到它的缓冲区。**这样一来,即使宕机了,这个 AOF 日志的操作仍然是齐全的,可以用于恢复。

而**第二处日志,就是指新的 AOF 重写日志。**这个操作也会被写到重写日志的缓冲区。这样,重写日志也不会丢失最新的操作。等到拷贝数据的所有操作记录重写完成后,重写日志记录的这些最新操作也会写入新的 AOF 文件,以保证数据库最新状态的记录。此时,我们就可以用新的 AOF 文件替代旧文件了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-duEWvc2T-1619617395495)(C:\Users\75666\AppData\Roaming\Typora\typora-user-images\1617673021687.png)]

总结来说,每次 AOF 重写时,Redis 会先执行一个内存拷贝,用于重写;然后,使用两个日志保证在重写过程中,新写入的数据不会丢失。而且,因为 Redis 采用额外的线程进行数据重写,所以,这个过程并不会阻塞主线程。

以下内容为课后思考题,摘抄出精选回答

  1. AOF 日志重写的时候,是由 bgrewriteaof 子进程来完成的,不用主线程参与,我们今天说的非阻塞也是指子进程的执行不阻塞主线程。但是,你觉得,这个重写过程有没有其他潜在的阻塞风险呢?如果有的话,会在哪里阻塞?
  2. AOF 重写也有一个重写日志,为什么它不共享使用 AOF 本身的日志呢?

问题1,Redis采用fork子进程重写AOF文件时,潜在的阻塞风险包括:fork子进程 和 AOF重写过程中父进程产生写入的场景,下面依次介绍。

a、fork子进程,fork这个瞬间一定是会阻塞主线程的(注意,fork时并不会一次性拷贝所有内存数据给子进程,老师文章写的是拷贝所有内存数据给子进程,我个人认为是有歧义的),fork采用操作系统提供的写实复制(Copy On Write)机制,就是为了避免一次性拷贝大量内存数据给子进程造成的长时间阻塞问题,但fork子进程需要拷贝进程必要的数据结构,其中有一项就是拷贝内存页表(虚拟内存和物理内存的映射索引表),这个拷贝过程会消耗大量CPU资源,拷贝完成之前整个进程是会阻塞的,阻塞时间取决于整个实例的内存大小,实例越大,内存页表越大,fork阻塞时间越久。拷贝内存页表完成后,子进程与父进程指向相同的内存地址空间,也就是说此时虽然产生了子进程,但是并没有申请与父进程相同的内存大小。那什么时候父子进程才会真正内存分离呢?“写实复制”顾名思义,就是在写发生时,才真正拷贝内存真正的数据,这个过程中,父进程也可能会产生阻塞的风险,就是下面介绍的场景。

b、fork出的子进程指向与父进程相同的内存地址空间,此时子进程就可以执行AOF重写,把内存中的所有数据写入到AOF文件中。但是此时父进程依旧是会有流量写入的,如果父进程操作的是一个已经存在的key,那么这个时候父进程就会真正拷贝这个key对应的内存数据,申请新的内存空间,这样逐渐地,父子进程内存数据开始分离,父子进程逐渐拥有各自独立的内存空间。因为内存分配是以页为单位进行分配的,默认4k,如果父进程此时操作的是一个bigkey,重新申请大块内存耗时会变长,可能会产阻塞风险。另外,如果操作系统开启了内存大页机制(Huge Page,页面大小2M),那么父进程申请内存时阻塞的概率将会大大提高,所以在Redis机器上需要关闭Huge Page机制。Redis每次fork生成RDB或AOF重写完成后,都可以在Redis log中看到父进程重新申请了多大的内存空间。

问题2,AOF重写不复用AOF本身的日志,一个原因是父子进程写同一个文件必然会产生竞争问题,控制竞争就意味着会影响父进程的性能。二是如果AOF重写过程中失败了,那么原本的AOF文件相当于被污染了,无法做恢复使用。所以Redis AOF重写一个新文件,重写失败的话,直接删除这个文件就好了,不会对原先的AOF文件产生影响。等重写完成之后,直接替换旧文件即可。

老师回答摘录:

文章中的歧义:fork子进程时,子进程是会拷贝父进程的页表,即虚实映射关系,而不会拷贝物理内存。子进程复制了父进程页表,也能共享访问父进程的内存数据了,此时,类似于有了父进程的所有内存数据。

Huge page。这个特性大家在使用Redis也要注意。Huge page对提升TLB命中率比较友好,因为在相同的内存容量下,使用huge page可以减少页表项,TLB就可以缓存更多的页表项,能减少TLB miss的开销。

但是,这个机制对于Redis这种喜欢用fork的系统来说,的确不太友好,尤其是在Redis的写入请求比较多的情况下。因为fork后,父进程修改数据采用写时复制,复制的粒度为一个内存页。如果只是修改一个256B的数据,父进程需要读原来的内存页,然后再映射到新的物理地址写入。一读一写会造成读写放大。如果内存页越大(例如2MB的大页),那么读写放大也就越严重,对Redis性能造成影响。

Huge page在实际使用Redis时是建议关掉的。

RDB(Redis DataBase)

另一种持久化方法:内存快照

内存快照,就是指内存中的数据在某一个时刻的状态记录。

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

Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave。

save:在主线程中执行,会导致阻塞;

bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的默认配置。

快照时数据能修改吗?

相应的解决办法——写时复制技术(Copy-On-Write, COW)

跟拍照一样,我们希望数据在照的时候不要动,但是这样就不能写了,但是这样数据是会有潜在问题的。这意味着redis不能对数据进行写操作,会造成影响。

常见的误区

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

对此,相应的解决办法——写时复制技术(Copy-On-Write, COW)

Redis 就会借助操作系统提供的写时复制技术(Copy-On-Write, COW),在执行快照的同时,正常处理写操作。

简单来说,bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。如果都是读操作,影响不大。但是,如果主线程要修改一块数据,那么,这块数据就会被复制一份,生成该数据的副本。然后,bgsave 子进程会把这个副本数据写入 RDB 文件,而在这个过程中,主线程仍然可以直接修改原来的数据。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8FKkQllg-1619617395497)(C:\Users\75666\AppData\Roaming\Typora\typora-user-images\1617772805197.png)]

多久做一次快照?

是不是在很短的时间间隔快照都可以,虽然bgsave不阻塞主线程,那是不是能频繁的快照?肯定不行,因为如果频繁地执行全量快照,也会带来两方面的开销。

一方面,频繁将全量数据写入磁盘,会给磁盘带来很大压力,多个快照竞争有限的磁盘带宽,前一个快照还没有做完,后一个又开始做了,容易造成恶性循环。

另一方面,bgsave 子进程需要通过 fork 操作从主线程创建出来。虽然,子进程在创建后不会再阻塞主线程,但是,fork 这个创建过程本身会阻塞主线程,而且主线程的内存越大,阻塞时间越长。如果频繁 fork 出 bgsave 子进程,这就会频繁阻塞主线程了。(在fork的一瞬间是阻塞的)

增量快照

增量快照,就是指,做了一次全量快照后,后续的快照只对修改的数据进行快照记录,这样可以避免每次全量快照的开销。

RDB与AOF相比,

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

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

白话文:简单来说,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。

这样一来,快照不用很频繁地执行,这就避免了频繁 fork 对主线程的影响。而且,AOF 日志也只用记录两次快照间的操作,也就是说,不需要记录所有操作了,因此,就不会出现文件过大的情况了,也可以避免重写开销。

AOF和RDB 能够做到数据尽量少丢失。

主从库机制

为什么redis具有高可靠性,有两层含义**:一是数据尽量少丢失,二是服务尽量少中断**。

服务尽量少中断怎么做呢?——Redis 的做法就是增加副本冗余量,就是将一份数据同时保存在多个实例上。(做多个备份的意思)

怎么做到这么多实例个体间的数据同步呢?

主从库模式

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

读操作:主库、从库都可以接收;

写操作:首先到主库执行,然后,主库将写操作同步给从库。

示例图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HsOV8XFr-1619617395499)(C:\Users\75666\AppData\Roaming\Typora\typora-user-images\1618661382896.png)]

为啥要读写分离?读写一致不行吗?为什么不行?

如果读写一致,肯定就是加锁,还有设计协商实例间是否完成一致等麻烦事,会带来巨大的开销。主从库读写分离,就是先给主库读写数据,从库只进行读操作,分离开来,然后再将主库的新数据同步给从库。再达到数据一致。

再琢磨,主库怎样同步给从库呢?

主库怎样同步给从库呢?

首先,先启动多个redis实例时,通过replicaof(Redis 5.0 之前使用 slaveof)命令形成主库和从库的关系,之后会按照三个阶段完成数据的第一次同步。

例如,现在有实例 1(ip:172.16.19.3)和实例 2(ip:172.16.19.5),我们在实例 2 上执行以下这个命令后,实例 2 就变成了实例 1 的从库,并从实例 1 上复制数据:

replicaof  172.16.19.3  6379

第一次同步三阶段

第一阶段:建立连接,协商同步,主要是为全量复制做准备

从库和主库建立起连接,并告诉主库即将进行同步,主库确认回复后,主从库间就可以开始同步了。

psync命令,(psync 命令包含了主库的 runID 和复制进度 offset 两个参数。)具体查看 06-数据同步 https://time.geekbang.org/column/article/272852

注意:FULLRESYNC 响应表示第一次复制采用的 全量复制 (生成 RDB 文件和传输 RDB 文件。),也就是说,主库会把当前所有的数据都复制给从库。

第二阶段:主库同步数据给从库。

从库收到数据后,在本地完成数据加载。(依赖于RDB文件)

具体过程:详见极客时间06——数据同步

主库执行 bgsave 命令,生成 RDB 文件,接着将文件发给从库。从库接收到 RDB 文件后,会先清空当前数据库,(从库在通过 replicaof 命令开始和主库同步前,可能保存了其他数据。为了避免之前数据的影响,从库需要先把当前数据库清空。)然后加载 RDB 文件。

在主库将数据同步给从库的过程中,主库不会被阻塞,仍然可以正常接收请求。要不然服务中断了就。

那么万一数据同步给从库的过程中有数据写进来,该怎么办呢?好办,replication buffer。

为了保证主从库的数据一致性,主库会在内存中用专门的 replication buffer,记录 RDB 文件生成后收到的所有写操作。

第三阶段:主库发送新命令给从库。把RDB之后的文件发给从库就可以了。(执行完后主从库就实现同步)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nkSw1ZjM-1619617395500)(C:\Users\75666\AppData\Roaming\Typora\typora-user-images\1618662167664.png)]

全量复制会占用大量的时间,如果从库数量很多,而且都要和主库进行全量复制的话,就会导致主库忙于 fork 子进程生成 RDB 文件,进行数据全量同步。fork 这个操作会阻塞主线程处理正常请求,从而导致主库响应应用程序的请求速度变慢。 一方面,fork会阻塞导致响应变慢,另一方面,传输 RDB 文件也会占用主库的网络带宽,同样会给主库的资源使用带来压力。

所以采用 “主 - 从 - 从” 模式。通过“主 - 从 - 从”模式将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库上。(就是分配班长——小组长——组员)

“主 - 从 - 从” 模式


replicaof  所选从库的IP 6379

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ptty8Go5-1619617395500)(C:\Users\75666\AppData\Roaming\Typora\typora-user-images\1618663376468.png)]

这个模式可以分担主库压力的方式,,一旦主从库完成了全量复制,它们之间就会一直维护一个网络连接,主库会通过这个连接将后续陆续收到的命令操作再同步给从库,这个过程也称为基于长连接的命令传播,可以避免频繁建立连接的开销。

这个过程中遇到断网怎么办?客户端只能从旧的从库读取数据了。常见的网络断连或阻塞。

解决办法:

在 Redis 2.8 之前,主从库重新进行一次全量复制(同步所有数据),开销很大。

从 Redis 2.8 开始, 网络断了之后,主从库会采用增量复制的方式继续同步。增量复制只会把主从库网络断连期间主库收到的命令,同步给从库。具体通过 repl_backlog_buffer** 这个缓冲区

repl_backlog_buffer 这个缓冲区

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

repl_backlog_buffer 是一个环形缓冲区(注意:缓冲区写满后,主库会继续写入,此时,就会覆盖掉之前写入的操作),主库会记录自己写到的位置,从库则会记录自己已经读到的位置。

刚开始,读写处于同一位置,写开始后,偏移量增加,从库再复制完写的操作命令之后也再偏移量增加,正常下这俩个偏移量基本相等。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fKbqjXvk-1619617395501)(C:\Users\75666\AppData\Roaming\Typora\typora-user-images\1618664054595.png)]

主从库的连接恢复之后,从库首先会给主库发送 psync 命令,并把自己当前的 slave_repl_offset 发给主库,主库会判断自己的 master_repl_offset 和 slave_repl_offset 之间的差距。

在网络断连阶段,主库可能会收到新的写操作命令,所以,一般来说,master_repl_offset 会大于 slave_repl_offset。此时,主库只用把 master_repl_offset 和 slave_repl_offset 之间的命令操作同步给从库就行。

增量复制的流程。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PxKRPnDY-1619617395502)(C:\Users\75666\AppData\Roaming\Typora\typora-user-images\1618664132066.png)]

注意:如果从库的读取速度比较慢,就有可能导致从库还未读取的操作被主库新写的操作覆盖了,这会导致主从库间的数据不一致。(读写速度相差太大,就会导致数据不一致了)

通过调整 repl_backlog_size 这个参数来避免。

这个参数和所需的缓冲空间大小有关。

缓冲空间的计算公式是:缓冲空间大小 = 主库写入命令速度 * 操作大小 - 主从库间网络传输命令速度 * 操作大小。(实际应用中,将这个缓冲空间扩大一倍)= repl_backlog_size = 缓冲空间大小 * 2

举个例子,如果主库每秒写入 2000 个操作,每个操作的大小为 2KB,网络每秒能传输 1000 个操作,那么,有 1000 个操作需要缓冲起来,这就至少需要 2MB 的缓冲空间。否则,新写的命令就会覆盖掉旧操作了。为了应对可能的突发压力,我们最终把 repl_backlog_size 设为 4MB。

主库挂掉怎么办?——哨兵机制

哨兵机制的基本流程:哨兵主要负责的就是三个任务:监控、选主(选择主库)和通知。

监控

就是周期性发送PING命令检测网络连接是否正常。

如果从库没有在规定时间内响应哨兵的 PING 命令,哨兵就会把它标记为“下线状态”;同样,如果主库也没有在规定时间内响应哨兵的 PING 命令,哨兵就会判定主库下线,然后开始自动切换主库的流程。

选主

主库OVER以后按照一定的规则选择一个从库实例,把它作为新的主库,选择出了新的主库

通知:通知其他从库,让其他从库重新与主库建立连接,并进行数据复制。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0tqexVXY-1619617395504)(C:\Users\75666\AppData\Roaming\Typora\typora-user-images\1618664903654.png)]

具体分析:

在监控任务中,哨兵需要判断主库是否处于下线状态;

在选主任务中,哨兵也要决定选择哪个从库实例作为主库。

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

一个哨兵的监控判断为主观判断,多个哨兵的监控判断为客观判断。

哨兵进程会使用 PING 命令检测它自己和主、从库的网络连接情况,用来判断实例的状态。如果哨兵发现主库或从库对 PING 命令的响应超时了,那么,哨兵就会先把它标记为“主观下线”。

如果检测的是从库,那么,哨兵简单地把它标记为“主观下线”就行了,因为从库的下线影响一般不太大,集群的对外服务不会间断。

为了减少哨兵的误判,通常会采用多实例组成的集群模式进行部署,这也被称为哨兵集群。

示例图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gkZcLgcJ-1619617395505)(C:\Users\75666\AppData\Roaming\Typora\typora-user-images\1618665199383.png)]

如何选定新主库?

按照一定的规则选:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ieB7Eaqd-1619617395506)(C:\Users\75666\AppData\Roaming\Typora\typora-user-images\1618665256159.png)]

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

具体怎么判断呢?你使用配置项 down-after-milliseconds * 10。其中,down-after-milliseconds 是我们认定主从库断连的最大连接超时时间。如果在 down-after-milliseconds 毫秒内,主从节点都没有通过网络联系上,我们就可以认为主从节点断连了。如果发生断连的次数超过了 10 次,就说明这个从库的网络状况不好,不适合作为新主库。

三方面:从库优先级、从库复制进度以及从库 ID 号

第一轮:优先级最高的从库得分高。

第二轮:和旧主库同步程度最接近的从库得分高。

第三轮:ID 号小的从库得分高。

哨兵挂掉怎么办?——哨兵集群

基于 pub/sub 机制的哨兵集群组成——发布 / 订阅机制

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

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

只有订阅了同一个频道(就是消息的类别)的应用,才能通过发布的消息进行信息交换。

在主从集群中,主库上有一个名为“_ _sentinel _ _:hello”的频道,不同哨兵就是通过它来相互发现,实现互相通信的。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y5xfj6w1-1619617395507)(C:\Users\75666\AppData\Roaming\Typora\typora-user-images\1618665987842.png)]

通过上述图观看,哨兵互相之间建立起了连接,还得哨兵和从库之间进行建立连接。

哨兵是如何知道从库的 IP 地址和端口的呢?——由哨兵向主库发送 INFO 命令来完成的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GJVcKAv6-1619617395508)(C:\Users\75666\AppData\Roaming\Typora\typora-user-images\1618666161461.png)]

哨兵 2 给主库发送 INFO 命令,主库接受到这个命令后,就会把从库列表返回给哨兵。接着,哨兵就可以根据从库列表中的连接信息,和每个从库建立连接,并在这个连接上持续地对从库进行监控。哨兵 1 和 3 可以通过相同的方法和从库建立连接。

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

但是,哨兵不能只和主、从库连接。因为,主从库切换后,客户端也需要知道新主库的连接信息,才能向新主库发送请求操作。所以,哨兵还需要完成把新主库的信息告诉客户端这个任务。

仍然可以依赖 pub/sub 机制,来帮助我们完成哨兵和客户端间的信息同步。

具体查看08 极客时间

切片集群

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8mdT0Tfc-1619617395511)(C:\Users\75666\AppData\Roaming\Typora\typora-user-images\1618708472494.png)]

使用场景:保存大量数据。

如何保存更多数据?

一个时用大内存,一个时切片集群。也就是纵向扩展和横向扩展。

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6cCACZmN-1619617395514)(C:\Users\75666\AppData\Roaming\Typora\typora-user-images\1618708635141.png)]

优缺点:

缺点:

纵向扩展:

当使用 RDB 对数据进行持久化时,如果数据量增加,需要的内存也会增加,主线程 fork 子进程时就可能会阻塞。(如果你不要求持久化保存 Redis 数据,那么,纵向扩展会是一个不错的选择。)

纵向扩展会受到硬件和成本的限制。

横向扩展:

切片集群不可避免地涉及到多个实例的分布式管理问题。技术上更加复杂。

优点:

纵向扩展的好处是,实施起来简单、直接。

横向扩展不用担心单个实例的硬件和成本限制。在面向百万、千万级别的用户规模时,横向扩展的 Redis 切片集群会是一个非常好的选择。

数据切片和实例的对应分布关系

Redis 3.0开始——Redis Cluster 方案

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

具体的映射过程分为两大步:

首先根据键值对的 key,按照CRC16 算法计算一个 16 bit 的值;

然后,再用这个 16bit 值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽

哈希槽又是如何被映射到具体的 Redis 实例上的呢?

我们在部署 Redis Cluster 方案时,可以使用 cluster create 命令创建集群,此时,Redis 会自动把这些槽平均分布在集群实例上。(例如,如果集群中有 N 个实例,那么,每个实例上的槽个数为 16384/N 个。)

手动建立连接的话使用cluster meet,建立实例间的连接,形成集群,再使用 cluster addslots 命令,指定每个实例上的哈希槽个数。

均匀分配不一定好,因为有时候遇到内存小的实例就会有更大的容量压力,所以可以根据不同实例的资源配置情况,使用 cluster addslots 命令手动分配哈希槽。

例子:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TdGK0UuU-1619617395515)(C:\Users\75666\AppData\Roaming\Typora\typora-user-images\1618709771556.png)]

示意图中的切片集群一共有 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

在集群运行的过程中,key1 和 key2 计算完 CRC16 值后,对哈希槽总个数 5 取模,再根据各自的模数结果,就可以被映射到对应的实例 1 和实例 3 上了。

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

通过哈希槽,切片集群就实现了数据到哈希槽、哈希槽再到实例的分配。

客户端如何定位数据?

客户端为什么可以在访问任何一个实例时,都能获得所有的哈希槽信息呢?这是因为,Redis 实例会把自己的哈希槽信息发给和它相连接的其它实例,来完成哈希槽分配信息的扩散。当实例之间相互连接后,每个实例就有所有哈希槽的映射关系了。

客户端收到哈希槽信息后,会把哈希槽信息缓存在本地。当客户端请求键值对时,会先计算键所对应的哈希槽,然后就可以给相应的实例发送请求了。

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

在集群中,实例有新增或删除,Redis 需要重新分配哈希槽;

为了负载均衡,Redis 需要把哈希槽在所有实例上重新分布一遍。

这个时候,客户端是不知道的,客户端如何获取新的哈希槽的信息呢?

Redis Cluster 方案提供了一种重定向机制

“重定向”,就是指,客户端给一个实例发送数据读写操作时,这个实例上并没有相应的数据,客户端要再给一个新实例发送操作命令。(我的理解,就是加了个中间人,负责传话用的)

那客户端又是怎么知道重定向时的新实例的访问地址呢?

当客户端把一个键值对的操作请求发给一个实例时,如果这个实例上并没有这个键值对映射的哈希槽,那么,这个实例就会给客户端返回下面的 MOVED 命令响应结果,这个结果中就包含了新实例的访问地址。


GET hello:key
(error) MOVED 13320 172.16.19.5:6379

MOVED 命令表示,客户端请求的键值对所在的哈希槽 13320,实际是在 172.16.19.5 这个实例上。通过返回的 MOVED 命令,就相当于把哈希槽所在的新实例的信息告诉给客户端了。

画图说明,,MOVED 重定向命令的使用方法。可以看到,由于负载均衡,Slot 2 中的数据已经从实例 2 迁移到了实例 3,但是,客户端缓存仍然记录着“Slot 2 在实例 2”的信息,所以会给实例 2 发送命令。实例 2 给客户端返回一条 MOVED 命令,把 Slot 2 的最新位置(也就是在实例 3 上),返回给客户端,客户端就会再次向实例 3 发送请求,同时还会更新本地缓存,把 Slot 2 与实例的对应关系更新过来。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tjToBFvQ-1619617395516)(C:\Users\75666\AppData\Roaming\Typora\typora-user-images\1618710312563.png)]

万一数据迁移了一半怎么办?

需要注意的是,在上图中,当客户端给实例 2 发送命令时,Slot 2 中的数据已经全部迁移到了实例 3。在实际应用时,如果 Slot 2 中的数据比较多,就可能会出现一种情况:客户端向实例 2 发送请求,但此时,Slot 2 中的数据只有一部分迁移到了实例 3,还有部分数据没有迁移。**在这种迁移部分完成的情况下,客户端就会收到一条 ASK 报错信息,**如下所示:


GET hello:key
(error) ASK 13320 172.16.19.5:6379

这个结果中的 ASK 命令就表示,客户端请求的键值对所在的哈希槽 13320在 172.16.19.5 这个实例上,但是这个哈希槽正在迁移。

此时,客户端需要先给 172.16.19.5 这个实例发送一个 ASKING 命令。

这个命令的意思是,让这个实例允许执行客户端接下来发送的命令。然后,客户端再向这个实例发送 GET 命令,以读取数据。

借助图片说明以上内容:

在下图中,Slot 2 正在从实例 2 往实例 3 迁移,key1 和 key2 已经迁移过去,key3 和 key4 还在实例 2。客户端向实例 2 请求 key2 后,就会收到实例 2 返回的 ASK 命令。

ASK 命令表示两层含义:

第一,表明 Slot 数据还在迁移中;

第二,ASK 命令把客户端所请求数据的最新实例地址返回给客户端,此时,客户端需要给实例 3 发送 ASKING 命令,然后再发送操作命令。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lm9CwVHp-1619617395517)(C:\Users\75666\AppData\Roaming\Typora\typora-user-images\1618710593796.png)]

和 MOVED 命令不同,ASK 命令并不会更新客户端缓存的哈希槽分配信息。所以,在上图中,如果客户端再次请求 Slot 2 中的数据,它还是会给实例 2 发送请求。这也就是说,ASK 命令的作用只是让客户端能给新实例发送一次请求,而不像 MOVED 命令那样,会更改本地缓存,让后续所有命令都发往新实例。

K 命令就表示,客户端请求的键值对所在的哈希槽 13320在 172.16.19.5 这个实例上,但是这个哈希槽正在迁移。

此时,客户端需要先给 172.16.19.5 这个实例发送一个 ASKING 命令。

这个命令的意思是,让这个实例允许执行客户端接下来发送的命令。然后,客户端再向这个实例发送 GET 命令,以读取数据。

借助图片说明以上内容:

在下图中,Slot 2 正在从实例 2 往实例 3 迁移,key1 和 key2 已经迁移过去,key3 和 key4 还在实例 2。客户端向实例 2 请求 key2 后,就会收到实例 2 返回的 ASK 命令。

ASK 命令表示两层含义:

第一,表明 Slot 数据还在迁移中;

第二,ASK 命令把客户端所请求数据的最新实例地址返回给客户端,此时,客户端需要给实例 3 发送 ASKING 命令,然后再发送操作命令。

[外链图片转存中…(img-lm9CwVHp-1619617395517)]

和 MOVED 命令不同,ASK 命令并不会更新客户端缓存的哈希槽分配信息。所以,在上图中,如果客户端再次请求 Slot 2 中的数据,它还是会给实例 2 发送请求。这也就是说,ASK 命令的作用只是让客户端能给新实例发送一次请求,而不像 MOVED 命令那样,会更改本地缓存,让后续所有命令都发往新实例。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值