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

目录

1.应用 9:大海捞针 —— Scan 

原理 1:鞭辟入里 —— 线程 IO 模型 

原理 2:交头接耳 —— 通信协议

原理 3:未雨绸缪 —— 持久化 

原理 4:雷厉风行 —— 管道 

原理 5:同舟共济 —— 事务 

为什么 Redis 不支持回滚 roll back

原理 6:小道消息 —— PubSub

原理 7:开源节流 —— 小对象压缩 

原理 8:有备无患 —— 主从同步 


1.应用 9:大海捞针 —— Scan 

在平时线上 Redis 维护工作中,有时候需要从 Redis 实例成千上万的 key 中找出特定
前缀的 key 列表来手动处理数据,可能是修改它的值,也可能是删除 key。这里就有一个问
题,如何从海量的 key 中找出满足特定前缀的 key 列表来

Redis 提供了一个简单暴力的指令 keys 用来列出所有满足特定正则字符串规则的 key

这个指令使用非常简单,提供一个简单的正则字符串即可,但是有很明显的两个缺点。 
1、没有 offset、limit 参数,一次性吐出所有满足条件的 key,万一实例中有几百 w 个 
key 满足条件,当你看到满屏的字符串刷的没有尽头时,你就知道难受了

2、keys 算法是遍历算法,复杂度是 O(n),如果实例中有千万级以上的 key,这个指令
就会导致 Redis 服务卡顿,所有读写 Redis 的其它的指令都会被延后甚至会超时报错,因为 
Redis 是单线程程序,顺序执行所有指令,其它指令必须等到当前的 keys 指令执行完了才
可以继续。

Redis 为了解决这个问题,它在 2.8 版本中加入了大海捞针的指令——scan。scan 相比 
keys 具备有以下特点: 

1、复杂度虽然也是 O(n),但是它是通过游标分步进行的,不会阻塞线程; 
2、提供 limit 参数,可以控制每次返回结果的最大条数,limit 只是一个 hint,返回的
结果可多可少; 
3、同 keys 一样,它也提供模式匹配功能; 
4、服务器不需要为游标保存状态,游标的唯一状态就是 scan 返回给客户端的游标整数; 
5、返回的结果可能会有重复,需要客户端去重复,这点非常重要; 
6、遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的; 
7、单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零; 

scan 基础使用 

scan 参数提供了三个参数,第一个是 cursor 整数值,第二个是 key 的正则模式,第三
个是遍历的 limit hint。第一次遍历时,cursor 值为 0,然后将返回结果中第一个整数值作为
下一次遍历的 cursor。一直遍历到返回的 cursor 值为 0 时结束

从上面的过程可以看到虽然提供的 limit 是 1000,但是返回的结果只有 10 个左右。因
为这个 limit 不是限定返回结果的数量,而是限定服务器单次遍历的字典槽位数量(约等于)。

如果将 limit 设置为 10,你会发现返回结果是空的,但是游标值不为零,意味着遍历还没结
束。

SCAN命令的有SCAN,SSCAN,HSCAN,ZSCAN。
SCAN的话就是遍历所有的keys
其他的SCAN命令的话是SCAN选中的集合。
SCAN命令是增量的循环,每次调用只会返回一小部分的元素。所以不会有KEYS命令的坑。
SCAN命令返回的是一个游标,从0开始遍历,到0结束遍历。
 

字典的结构 

在 Redis 中所有的 key 都存储在一个很大的字典中,这个字典的结构和 Java 中的 
HashMap 一样,是一维数组 + 二维链表结构,第一维数组的大小总是 2^n(n>=0),扩容一
次数组大小空间加倍,也就是 n++

scan 指令返回的游标就是第一维数组的位置索引,我们将这个位置索引称为槽 (slot)。
如果不考虑字典的扩容缩容,直接按数组下标挨个遍历就行了。limit 参数就表示需要遍历的
槽位数,之所以返回的结果可能多可能少,是因为不是所有的槽位上都会挂接链表,有些槽
位可能是空的,还有些槽位上挂接的链表上的元素可能会有多个。每一次遍历都会将 limit 
数量的槽位上挂接的所有链表元素进行模式匹配过滤后,一次性返回给客户端

scan 遍历顺序

scan 的遍历顺序非常特别。它不是从第一维数组的第 0 位一直遍历到末尾,而是采用
了高位进位加法来遍历。之所以使用这样特殊的方式进行遍历,是考虑到字典的扩容和缩容
时避免槽位的遍历重复和遗漏
普通加法和高位进位加法的区别 
高位进位法从左边加,进位往右边移动,同普通加法正好相反。但是最终它们都会遍历
所有的槽位并且没有重复。

字典扩容 

Java 中的 HashMap 有扩容的概念,当 loadFactor 达到阈值时,需要重新分配一个新的 
2 倍大小的数组,然后将所有的元素全部 rehash 挂到新的数组下面。rehash 就是将元素的 
hash 值对数组长度进行取模运算,因为长度变了,所以每个元素挂接的槽位可能也发生了变
化。又因为数组的长度是 2^n 次方,所以取模运算等价于位与操作

渐进式 rehash 
Java 的 HashMap 在扩容时会一次性将旧数组下挂接的元素全部转移到新数组下面。如
果 HashMap 中元素特别多,线程就会出现卡顿现象。Redis 为了解决这个问题,它采用渐
进式 rehash

它会同时保留旧数组和新数组,然后在定时任务中以及后续对 hash 的指令操作中渐渐
地将旧数组中挂接的元素迁移到新数组上。这意味着要操作处于 rehash 中的字典,需要同
时访问新旧两个数组结构。如果在旧数组下面找不到元素,还需要去新数组下面去寻找。 
scan 也需要考虑这个问题,对与 rehash 中的字典,它需要同时扫描新旧槽位,然后将
结果融合后返回给客户端。 

更多的 scan 指令 

scan 指令是一系列指令,除了可以遍历所有的 key 之外,还可以对指定的容器集合进
行遍历。比如 zscan 遍历 zset 集合元素,hscan 遍历 hash 字典的元素、sscan 遍历 set 集
合的元素。 
它们的原理同 scan 都会类似的,因为 hash 底层就是字典,set 也是一个特殊的 
hash(所有的 value 指向同一个元素),zset 内部也使用了字典来存储所有的元素内容,所以
这里不再赘述。

大 key 扫描 

有时候会因为业务人员使用不当,在 Redis 实例中会形成很大的对象,比如一个很大的 
hash,一个很大的 zset 这都是经常出现的。这样的对象对 Redis 的集群数据迁移带来了很
大的问题,因为在集群环境下,如果某个 key 太大,会数据导致迁移卡顿。另外在内存分配
上,如果一个 key 太大,那么当它需要扩容时,会一次性申请更大的一块内存,这也会导致
卡顿。如果这个大 key 被删除,内存会一次性回收,卡顿现象会再一次产生。 
 
在平时的业务开发中,要尽量避免大 key 的产生。 
如果你观察到 Redis 的内存大起大落,这极有可能是因为大 key 导致的,这时候你就
需要定位出具体是那个 key,进一步定位出具体的业务来源,然后再改进相关业务代码设
计。 

那如何定位大 key 呢?
为了避免对线上 Redis 带来卡顿,这就要用到 scan 指令,对于扫描出来的每一个 
key,使用 type 指令获得 key 的类型,然后使用相应数据结构的 size 或者 len 方法来得到
它的大小,对于每一种类型,保留大小的前 N 名作为扫描结果展示出来。 
上面这样的过程需要编写脚本,比较繁琐,不过 Redis 官方已经在 redis-cli 指令中提供
了这样的扫描功能,我们可以直接拿来即用。 
redis-cli -h 127.0.0.1 -p 7001 –-bigkeys 
如果你担心这个指令会大幅抬升 Redis 的 ops 导致线上报警,还可以增加一个休眠参
数。 
redis-cli -h 127.0.0.1 -p 7001 –-bigkeys -i 0.1 
上面这个指令每隔 100 条 scan 指令就会休眠 0.1s,ops 就不会剧烈抬升,但是扫描的
时间会变长

原理 1:鞭辟入里 —— 线程 IO 模型 

  Redis 是个单线程程序!这点必须铭记。 
也许你会怀疑高并发的 Redis 中间件怎么可能是单线程。很抱歉,它就是单线程,你的
怀疑暴露了你基础知识的不足。莫要瞧不起单线程,除了 Redis 之外,Node.js 也是单线
程,Nginx 也是单线程,但是它们都是服务器高性能的典范

原理 2:交头接耳 —— 通信协议

Redis 将所有数据都放在内存,用一个单线程对外提供服务,单个节点在跑满一个 CPU 核心的情
况下可以达到了 10w/s 的超高 QPS。 

以上2个原理不知道讲的啥,看不懂!!!

原理 3:未雨绸缪 —— 持久化 


 Redis 的数据全部在内存里,如果突然宕机,数据就会全部丢失,因此必须有一种机制
来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的持久化机制。 
Redis 的持久化机制有两种,第一种是快照,第二种是 AOF 日志。快照是一次全量备
份,AOF 日志是连续的增量备份。快照是内存数据的二进制序列化形式,在存储上非常

凑,而 AOF 日志记录的是内存数据修改的指令记录文本。AOF 日志在长期的运行过程中会
变的无比庞大,数据库重启时需要加载 AOF 日志进行指令重放,这个时间就会无比漫长。
所以需要定期进行 AOF 重写,给 AOF 日志进行瘦身。



快照原理 

我们知道 Redis 是单线程程序,这个线程要同时负责多个客户端套接字的并发读写操作
和内存数据结构的逻辑读写。 
在服务线上请求的同时,Redis 还需要进行内存快照,内存快照要求 Redis 必须进行文
件 IO 操作,可文件 IO 操作是不能使用多路复用 API。 
这意味着单线程同时在服务线上的请求还要进行文件 IO 操作,文件 IO 操作会严重拖
垮服务器请求的性能。还有个重要的问题是为了不阻塞线上的业务,就需要边持久化边响应
客户端请求。持久化的同时,内存数据结构还在改变,比如一个大型的 hash 字典正在持久
化,结果一个请求过来把它给删掉了,还没持久化完呢,这尼玛要怎么搞? 
那该怎么办呢? 
Redis 使用操作系统的多进程 COW(Copy On Write) 机制来实现快照持久化,这个机制
很有意思,也很少人知道。多进程 COW 也是鉴定程序员知识广度的一个重要指标

fork(多进程)  

Redis 在持久化时会调用 glibc 的函数 fork 产生一个子进程,快照持久化完全交给子进
程来处理,父进程继续处理客户端请求。子进程刚刚产生时,它和父进程共享内存里面的代
码段和数据段。这时你可以将父子进程想像成一个连体婴儿,共享身体。这是 Linux 操作系
统的机制,为了节约内存资源,所以尽可能让它们共享起来。在进程分离的一瞬间,内存的
增长几乎没有明显变化

子进程做数据持久化,它不会修改现有的内存数据结构,它只是对数据结构进行遍历读
取,然后序列化写到磁盘中。但是父进程不一样,它必须持续服务客户端请求,然后对内存
数据结构进行不间断的修改。 
这个时候就会使用操作系统的 COW 机制来进行数据段页面的分离。数据段是由很多操
作系统的页面组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复
制一份分离出来,然后对这个复制的页面进行修改。这时子进程相应的页面是没有变化的,
还是进程产生时那一瞬间的数据子进程因为数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再
也不会改变,这也是为什么 Redis 的持久化叫「快照」的原因。接下来子进程就可以非常安
心的遍历数据了进行序列化写磁盘了。 

AOF 原理

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

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

Redis 会在收到客户端修改指令后,先进行参数校验,如果没问题,就立即将该指令文
本存储到 AOF 日志中,也就是先存到磁盘,然后再执行指令。这样即使遇到突发宕机,已
经存储到 AOF 日志的指令进行重放一下就可以恢复到宕机前的状态。 
Redis 在长期运行的过程中,AOF 的日志会越变越长。如果实例宕机重启,重放整个 
AOF 日志会非常耗时,导致长时间 Redis 无法对外提供服务。所以需要对 AOF 日志瘦

AOF 重写 

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

fsync

AOF 日志是以文件的形式存在的,当程序对 AOF 日志文件进行写操作时,实际上是将
内容写到了内核为文件描述符分配的一个内存缓存中,然后内核会异步将脏数据刷回到磁盘
的。 
这就意味着如果机器突然宕机,AOF 日志内容可能还没有来得及完全刷到磁盘中,这个
时候就会出现日志丢失。那该怎么办? 
Linux 的 glibc 提供了 fsync(int fd)函数可以将指定文件的内容强制从内核缓存刷到磁
盘。只要 Redis 进程实时调用 fsync 函数就可以保证 aof 日志不丢失。但是 fsync 是一个
磁盘 IO 操作,它很慢!如果 Redis 执行一条指令就要 fsync 一次,那么 Redis 高性能的
地位就不保了。 
所以在生产环境的服务器中,Redis 通常是每隔 1s 左右执行一次 fsync 操作,周期 1s 
是可以配置的。这是在数据安全性和性能之间做了一个折中,在保持高性能的同时,尽可能
使得数据少丢失

Redis 同样也提供了另外两种策略,一个是永不 fsync——让操作系统来决定合适同步磁
盘,很不安全,另一个是来一个指令就 fsync 一次——非常慢。但是在生产环境基本不会使
用,了解一下即可

运维

快照是通过开启子进程的方式进行的,它是一个比较耗资源的操作。 
    1、遍历整个内存,大块写磁盘会加重系统负载 
    2、AOF 的 fsync 是一个耗时的 IO 操作,它会降低 Redis 性能,同时也会增加系
统 IO 负担 

所以通常 Redis 的主节点是不会进行持久化操作,持久化操作主要在从节点进行。从节
点是备份节点,没有来自客户端请求的压力,它的操作系统资源往往比较充沛 ,

但是如果出现网络分区,从节点长期连不上主节点,就会出现数据不一致的问题,特别
是在网络分区出现的情况下又不小心主节点宕机了,那么数据就会丢失,所以在生产环境要
做好实时监控工作,保证网络畅通或者能快速修复。另外还应该再增加一个从节点以降低网
络分区的概率,只要有一个从节点数据同步正常,数据也就不会轻易丢失。

 Redis 4.0 混合持久化 

重启 Redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。我们通常
使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,这样在 Redis 实
例很大的情况下,启动需要花费很长的时间。 
Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。将 rdb 文
件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自
持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小

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

原理 4:雷厉风行 —— 管道 

Redis 的消息交互 

两个连续的写操作和两个连续的读操作总共只会花费一次网络来回,就好比连续的 write 
操作合并了,连续的 read 操作也合并了一样
这便是管道操作的本质,服务器根本没有任何区别对待,还是收到一条消息,执行一条
消息,回复一条消息的正常的流程。客户端通过对管道中的指令列表改变读写顺序就可以大
幅节省 IO 时间。管道中指令越多,效果越好

管道压力测试

接下来我们实践一下管道的力量。 
Redis 自带了一个压力测试工具 redis-benchmark,使用这个工具就可以进行管道测试

深入理解管道本质 

如上图

小结 
这就是管道的本质了,它并不是服务器的什么特性,而是客户端通过改变了读写的顺序
带来的性能的巨大提升

原理 5:同舟共济 —— 事务 

为了确保连续多个操作的原子性,一个成熟的数据库通常都会有事务支持,Redis 也不
例外。Redis 的事务使用非常简单,不同于关系数据库,我们无须理解那么多复杂的事务模
型,就可以直接使用。不过也正是因为这种简单性,它的事务模型很不严格,这要求我们不
能像使用关系数据库的事务一样来使用 Redis。

Redis 事务的基本使用

Redis 在形式上看起来也差不多,分别是 multi/exec/discard。multi 指示事务的开始,
exec 指示事务的执行,discard 指示事务的丢弃。 

上面的指令演示了一个完整的事务过程,所有的指令在 exec 之前不执行,而是缓存在
服务器的一个事务队列中,服务器一旦收到 exec 指令,才开执行整个事务队列,执行完毕
后一次性返回所有指令的运行结果。因为 Redis 的单线程特性,它不用担心自己在执行队列
的时候被其它指令打搅,可以保证他们能得到的「原子性」执行

 原子性

事务的原子性是指要么事务全部成功,要么全部失败,那么 Redis 事务执行是原子性的
么? 
下面我们来看一个特别的例子

上面的例子是事务执行到中间遇到失败了,因为我们不能对一个字符串进行数学运算,
事务在遇到指令执行失败后,后面的指令还继续执行,所以 poorman 的值能继续得到设置。 
到这里,你应该明白 Redis 的事务根本不能算「原子性」,而仅仅是满足了事务的「隔
离性」,隔离性中的串行化——当前执行的事务有着不被其它事务打断的权利

discard(丢弃)

Redis 为事务提供了一个 discard 指令,用于丢弃事务缓存队列中的所有指令,在 exec 
执行之前

 优化

上面的 Redis 事务在发送每个指令到事务缓存队列时都要经过一次网络读写,当一个事
务内部的指令较多时,需要的网络 IO 时间也会线性增长。所以通常 Redis 的客户端在执行
事务时都会结合 pipeline 一起使用,这样可以将多次 IO 操作压缩为单次 IO 操作

Watch 
考虑到一个业务场景,Redis 存储了我们的账户余额数据,它是一个整数。现在有两个
并发的客户端要对账户余额进行修改操作,这个修改不是一个简单的 incrby 指令,而是要对
余额乘以一个倍数。Redis 可没有提供 multiplyby 这样的指令。我们需要先取出余额然后在
内存里乘以倍数,再将结果写回 Redis。 
这就会出现并发问题,因为有多个客户端会并发进行操作。我们可以通过 Redis 的分布
式锁来避免冲突,这是一个很好的解决方案。分布式锁是一种悲观锁,那是不是可以使用乐
观锁的方式来解决冲突呢?

Redis 提供了这种 watch 的机制,它就是一种乐观锁。有了 watch 我们又多了一种可以
用来解决并发修改的方法。 watch 的使用方式如下:

watch 会在事务开始之前盯住 1 个或多个关键变量,当事务执行时,也就是服务器收到
了 exec 指令要顺序执行缓存的事务队列时,Redis 会检查关键变量自 watch 之后,是否被
修改了 (包括当前事务所在的客户端)。如果关键变量被人动过了,exec 指令就会返回 null 
回复告知客户端事务执行失败,这个时候客户端一般会选择重试

注意事项 
Redis 禁止在 multi 和 exec 之间执行 watch 指令,而必须在 multi 之前做好盯住关键
变量,否则会出错。 

为什么 Redis 不支持回滚 roll back

原理 6:小道消息 —— PubSub

前面我们讲了 Redis 消息队列的使用方法,但是没有提到 Redis 消息队列的不足之
处,那就是它不支持消息的多播机制

消息多播

消息多播允许生产者生产一次消息,中间件负责将消息复制到多个消息队列,每个消息
队列由相应的消费组进行消费。它是分布式系统常用的一种解耦方式,用于将多个消费组的
逻辑进行拆分。支持了消息多播,多个消费组的逻辑就可以放到不同的子系统中。 
如果是普通的消息队列,就得将多个不同的消费组逻辑串接起来放在一个子系统中,进
行连续消费。

PubSub

为了支持消息多播,Redis 不能再依赖于那 5 种基本数据类型了。它单独使用了一个模
块来支持消息多播,这个模块的名字叫着 PubSub,也就是 PublisherSubscriber,发布者订阅
者模型

客户端发起订阅命令后,Redis 会立即给予一个反馈消息通知订阅成功。因为有网络传
输延迟,在 subscribe 命令发出后,需要休眠一会,再通过 get\_message 才能拿到反馈消
息。客户端接下来执行发布命令,发布了一条消息。同样因为网络延迟,在 publish 命令发
出后,需要休眠一会,再通过 get\_message 才能拿到发布的消息。如果当前没有消息,
get\_message 会返回空,告知当前没有消息,所以它不是阻塞的

Redis PubSub 的生产者和消费者是不同的连接,也就是上面这个例子实际上使用了两个 
Redis 的连接。这是必须的,因为 Redis 不允许连接在 subscribe 等待消息时还要进行其它
的操作

这个应用看不下去了,当开个眼界吧 

原理 7:开源节流 —— 小对象压缩 

小对象压缩存储 (ziplist)(说的啥玩意,看不懂)

内存回收机制

Redis 并不总是可以将空闲内存立即归还给操作系统。 
如果当前 Redis 内存有 10G,当你删除了 1GB 的 key 后,再去观察内存,你会发现
内存变化不会太大。原因是操作系统回收内存是以页为单位,如果这个页上只要有一个 key 
还在使用,那么它就不能被回收。Redis 虽然删除了 1GB 的 key,但是这些 key 分散到了
很多页面中,每个页面都还有其它 key 存在,这就导致了内存不会立即被回收

Redis 虽然无法保证立即回收已经删除的 key 的内存,但是它会重用那些尚未回收的空
闲内存。这就好比电影院里虽然人走了,但是座位还在,下一波观众来了,直接坐就行。而
操作系统回收内存就好比把座位都给搬走了

内存分配算法 

内存分配是一个非常复杂的课题,需要适当的算法划分内存页,需要考虑内存碎片,需
要平衡性能和效率。 
Redis 为了保持自身结构的简单性,在内存分配这里直接做了甩手掌柜,将内存分配的
细节丢给了第三方内存分配库去实现。目前 Redis 可以使用 jemalloc(facebook) 库来管理内
存,也可以切换到 tcmalloc(google)。因为 jemalloc 相比 tcmalloc 的性能要稍好一些,所以
Redis 默认使用了 jemalloc

通过 info memory 指令可以看到 Redis 的 mem_allocator 使用了 jemalloc。

原理 8:有备无患 —— 主从同步 

很多企业都没有使用到 Redis 的集群,但是至少都做了主从。有了主从,当 master 挂
掉的时候,运维让从库过来接管,服务就可以继续,否则 master 需要经过数据恢复和重启
的过程,这就可能会拖很长的时间,影响线上业务的持续服务

在了解 Redis 的主从复制之前,让我们先来理解一下现代分布式系统的理论基石——
CAP 原理

CAP 原理

CAP 原理就好比分布式领域的牛顿定律,它是分布式存储的理论基石。自打 CAP 的论
文发表之后,分布式存储中间件犹如雨后春笋般一个一个涌现出来

 C - Consistent ,一致性 
 A - Availability ,可用性 
 P - Partition tolerance ,分区容忍性 

分布式系统的节点往往都是分布在不同的机器上进行网络隔离开的,这意味着必然会有
网络断开的风险,这个网络断开的场景的专业词汇叫着「网络分区」.在网络分区发生时,两个分布式节点之间无法进行通信,我们对一个节点进行的修改操
作将无法同步到另外一个节点,所以数据的「一致性」将无法满足,因为两个分布式节点的
数据不再保持一致。除非我们牺牲「可用性」,也就是暂停分布式节点服务,在网络分区发
生时,不再提供修改数据的功能,直到网络状况完全恢复正常再继续对外提供服务

一句话概括 CAP 原理就是——网络分区发生时,一致性和可用性两难全

 最终一致

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

主从同步 

Redis 同步支持主从同步和从从同步,从从同步功能是 Redis 后续版本增加的功能,为
了减轻主库的同步负担

增量同步 

Redis 同步的是指令流,主节点会将那些对自己的状态产生修改性影响的指令记录在本
地的内存 buffer 中,然后异步将 buffer 中的指令同步到从节点,从节点一边执行同步的指
令流来达到和主节点一样的状态,一遍向主节点反馈自己同步到哪里了 (偏移量)。Redis 的复制内存 buffer 是一个定长的环形数组,如果数组内容满了,就会从头开始覆
盖前面的内容。 

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

快照同步 

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

增加从节点 

当从节点刚刚加入到集群时,它必须先要进行一次快照同步,同步完成后再继续进行增
量同步。 

无盘复制

主节点在进行快照同步时,会进行很重的文件 IO 操作,特别是对于非 SSD 磁盘存储
时,快照会对系统的负载产生较大影响。特别是当系统正在进行 AOF 的 fsync 操作时如果
发生快照,fsync 将会被推迟执行,这就会严重影响主节点的服务效率。 
所以从 Redis 2.8.18 版开始支持无盘复制。所谓无盘复制是指主服务器直接通过套接字
将快照内容发送到从节点,生成快照是一个遍历的过程,主节点会一边遍历内存,一遍将序
列化的内容发送到从节点,从节点还是跟之前一样,先将接收到的内容存储到磁盘文件中,
再进行一次性加载

Wait 指令

Redis 的复制是异步进行的,wait 指令可以让异步复制变身同步复制,确保系统的强一
致性 (不严格)。wait 指令是 Redis3.0 版本以后才出现的。

wait 提供两个参数,第一个参数是从库的数量 N,第二个参数是时间 t,以毫秒为单
位。它表示等待 wait 指令之前的所有写操作同步到 N 个从库 (也就是确保 N 个从库的同
步没有滞后),最多等待时间 t。如果时间 t=0,表示无限等待直到 N 个从库同步完成达成
一致。 
假设此时出现了网络分区,wait 指令第二个参数时间 t=0,主从同步无法继续进行,
wait 指令会永远阻塞,Redis 服务器将丧失可用性

小结 
主从复制是 Redis 分布式的基础,Redis 的高可用离开了主从复制将无从进行。后面的
章节我们会开始讲解 Redis 的集群模式,这几种集群模式都依赖于本节所讲的主从复制。 

发布了33 篇原创文章 · 获赞 1 · 访问量 5509

猜你喜欢

转载自blog.csdn.net/ashylya/article/details/104754113