思路
Redis主从库的由来
随着数据量的增大,单台Redis无法很好的提供读写缓存服务了。于是就采用多台Redis完成更大的访问请求,那么多台Redis在提供服务时,数据肯定是要一致的。
Redis的处理方案是采用主从库模式,主从库之间采用的是读写分离方式,以保证数据副本的一致性。
读操作 :主从库都可以接收,因为读操作不影响主从库的数据一致问题
写操作 :如果在从库写,那么主库数据就不一致了。
如果不采用读写分离模式,那么我们就需要用其他方案解决数据一致性问题,比如加锁,多个Redis实例协商等等。这样的开销是非常大的,对于高性能的Redis来说显然是不能接受的。
如果采用读写分离模式,所有的写请求都打到主库上,主库再利用RDB文件同步给从库。达到数据的一致性!就可以省去一些不必要的开销了。
主从库数据如何实现一致
聊到Redis的主从库数据的一致性,我们可以先聊聊MySQL是如何实现主从库数据一致性的。
MySQL是借助binlog实现数据同步的,binlog主要有三种格式,statement,row,mixed。statement过于简单,row过于复杂数据量大,mixed中和了一下适合同步传输。
介绍了binlog三种格式,具体的流程是首先会在从库上执行 change master
设计好主从库之后,再执行 start slave
主库会发送从库binlog文件进行同步。
Redis这里也是一样的逻辑,通过 replicaof
实现从库认主。认主完成之后主库借助RDB文件进行数据同步操作。首先fork子线程进行生成RDB文件,生成RDB文件主要有两种形式,save
和 bgsave
- save:在主线程中执行,会导致阻塞;
- bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的默认配置。
一般我们都采用bgsave生成RDB文件,提升性能减少主线程阻塞!
生成完RDB就该进行数据同步了,接下来我们分析一下数据同步这个过程中都发生了哪些事情。
第一部分 肯定是主从库间建立连接,协商同步的过程,主要为了全量复制做准备。主从库建立连接之后,从库给主库发送了一个 psync ID -1
命令
- 这里的ID是简写,它是每个Redis实例启动时都会自动生成的一个随机ID也叫 runID,也是每个实例的唯一标识
- -1,他是同步的偏移量,也可以说是进度下标,第一次复制时,传-1就代表是全量同步,第二次之后就不是-1了,就是当前的复制下标了,比如从库已经同步到5000了,从库发送主库的时候就会从5000开始进行生成RDB文件进行增量同步。
第二部分 从库收到主库的RDB文件后,如果是第一次同步的话,会先清空当前从库的数据库然后再加载RDB文件。这是因为从库在通过 replicaof 命令开始和主库同步前,可能保存了其他数据。为了避免之前数据的影响,从库需要先把当前数据库清空。
第三部分 当主库在生成RDB期间,仍然有写请求,就会导致数据不一致,Redis这里的处理是开辟一个replication buffer 缓冲区,记录RDB文件生成后的主库写操作。第三部分就是把这部分少量的数据同步到从库上。
扩展1:除了主从同步,还有一种为了提升性能而诞生的一种同步是主从从模式。因为在主线程fork子线程后生成RDB文件是要消耗主线程资源的,如果存在多个从库的话,那么主库恐怕要一直fork和发送RDB。我们可以在部署主从集群时,手动选择一个内存资源配置较高的从库作为,从库与从库的数据同步源。然后让他们建立主从关系。
扩展2:一旦在传输过程冲断连了,可以通过复制下标进行重新数据同步。就不需要每次都走全量备份了。
高可用性体现在哪
Redis的高可用体现在哨兵机制,哨兵机制是等主从库挂了之后,通过哨兵机制可以自动切换选举出新的主库,然后进行数据同步,达到一致性。最终继续为用户提供服务。
哨兵主要负责的就是三个任务:监控、选主(选择主库)和通知。
监控: 哨兵会周期性的给所有的主从库发送ping命令,监测它们是否处于在线运行状态,
- 如果从库没有响应ping命令,哨兵就会把从库标记为下线状态。
- 如果主库没有响应ping命令,哨兵就会判断主库下线并且开始自动切换主库。
选主 :主库挂了之后,哨兵就需要在很多从库中,按照一定的规则选择出一个从库实例,把它作为新的主库。
通知 :主库重新诞生之后,哨兵就会通知从库,告诉它们新主库的信息。让他们执行replicaof
命令。重新建立之后开始数据同步流程。
一系列流程之后,主库出现了,从库也正常了。一切又回到了出故障之前的那个状态!
如何选定新主库
在上面高可用中,临时加了一个技术点,这个知识点就是在哨兵选主时,选择规则的介绍。
哨兵选主库时,一般都称为 "筛选+打分"
筛选:(网络波动)除了检查从库当前的在线状态还要判断它网络的连接状态。如果从库和主库响应过慢,并且超过了一定的阈值,那么肯定是不能选择该从库充当我们的主库的。因为一旦该从库选择主库,一旦在后续的写入操作,数据同步操作中网络波动大,或者直接断开连接了,我们还需要重新做一下选择,通知,同步等。这样性能是非常低效的!
打分:(择偶标准)主要有三点如下
- 优先级最高的从库得分高:用户可以通过
slave-priority
配置项,给不同的从库设置不同的优先级,比如不同的从库中的内存配置,CPU配置等 - 同步进度:一般选择一个从库为主库,如果我们从库的数据同步进度更接近与前主库,那么从库切换成主库之后,数据同步的时间消耗更低。性能会更好一些。
- ID 号小的从库得分高。(Redis的默认规定没啥好说的)。在优先级和复制进度都相同的情况下,ID 号最小的从库得分最高,会被选为新主库。
哨兵挂了,主从还能切换吗
答案是可以的。哨兵集群中的一个哨兵实例挂了,主从依然还是可以切换的,因为我们在配置哨兵信息的时,我们只需要设置主库的IP和端口,并没有配置其他的哨兵连接信息。
那么其他哨兵是如何知道彼此的地址的呢?
这应该就需要我们先了解一下 pub/sub机制的哨兵集群组成 。翻译一下分别是发布/订阅机制。
我们先讲一个对应的白话文故事,pub/sub机制就是几年前QQ非常火爆的QQ群功能,一个一个加好友交友也好,处理事情也好,都是比较麻烦的,如果说群主建立一个QQ群,然后拉自己的好友。拉进来之后,只要是这个群的人员都能收到任意好友发送的信息。这与Redis的pub/sub机制类似。
回到Redis中!哨兵只要和主库建立了连接,就可以在主库上发布消息了,同时也可以从主库订阅消息,获取其他哨兵发布的连接信息。当多个哨兵都在主库上发布了和订阅了信息后,就能知道彼此的IP地址和端口了 。
发布和订阅一直所说的频道信息,就类似于不同的群号,接收不同的好友信息一样。下面我们实践一下,必须是两个窗口或者多个Redis才能进行测试。
- 第一个窗口负责订阅一个频道叫huanshao,订阅之后就处于等待接收的状态了。
- 第二个窗口是用于发送消息的,往huanshao这个频道发送一个HelloWord然后第一个窗口自动就接收了
哨兵集群中每个哨兵都拿到了所需要的IP地址和端口号,哨兵除了要监测主库外还需要监测从库,因为主库挂了,要从从库中选举一个成为主库。那么从库的信息哨兵如何拿到呢?
可以通过哨兵告诉主库发送info命令来完成!主库执行了info命令就会把当前的从库信息返给哨兵。哨兵拿到了从库的IP地址和端口号一切就都好办了。
上述哨兵集群拿到了主从库的信息,在整个主从库切换这些不止是这些,还有重要的一步就是通知客户端修改主库信息。
和上述获取从库信息一样,通过不同的频道获取不同的消息,下面列举几个常用的频道事件
主库下线事件
- +sdown(实例进入主观下线状态)
- -sdown(实例退出主观下线状态)
- +odown(实例进入客观下线状态)
- -odown(实例退出下线状态)
从库重新配置事件
- +slave-reconf-sent(哨兵发送SLAVEOF命令重新配置从库)
- +slave-reconf-inprog(从库配置了新主库,但尚未进行同步)
- +slave-reconf-done(从库配置了新主库,且和新主库完成同步)
新主库切换
- +switch-master(主库地址发生变化)
知道了频道信息,就可以让客户端订阅相关信息,一旦哨兵监测出主库挂了,通过选举出新主库之后,以事件的方式通知客户端。就可以实现短暂宕机后的服务恢复了!
Redis订阅命令
SUBSCRIBE
订阅所有事件
PSUBSCRIBE *
马上结束了,大家再坚持一下! 简单总结一下,哨兵挂了之后,其他哨兵拿到了主从库的信息,同时通过频道事件的方式通知客户端重新绑定主库。那么选主时由谁来选?
一般应用哨兵我们都会采用哨兵集群,因为只通过1个哨兵实例的话,往往适得其反,如果当哨兵发送ping命令给主库时,那个时刻主库刚好网络不好,或者正常处理比较大的数据延误了给哨兵响应信息,那么哨兵就会认为当前主库挂了,就会给他设为主观/客观下线。然后就game over了。
于是引用多哨兵实例共同监控,这里我们设为哨兵A,哨兵B,哨兵C。
当哨兵A发现某个主库挂了,那么它会把这个主库设为主观下线,然后过段时间哨兵B也会给主库发送ping命令,当哨兵B也发现了这个主库挂了时,也会给他设为主观下线。在多个哨兵实例中如果有一半以上的哨兵都认为这个主库挂了。那么这个主库就真的挂了,会被设为客观下线。
判断出下线之后,多个实例哨兵就会先推选出一个执行leader。由其中一个哨兵来处理后续的通知相关操作。
这就类似于我们学生时代的小组组长一样,由一个小组6个人共同投票选举一个人,为这个小组的组长。如果选择同一个人的票数大于小组总人数。那么这个人就是组长,在Redis中这个人就是哨兵集群中的leader,由它来执行操作。
切片集群解决了什么
切片集群也叫作分片集群,就是启动多个Redis实例组成一个集群,然后按照一定的规则把收到的数据划分成多份,每一份用一个实例来保存数据。
如果我们生产环境有50G的数据,全部放入一台Redis实例的话,肯定是成本比较高,而且很多地方不好把控的。于是我们就把50个G的数据分成10份,每个Redis实例存5个G。
从性能上来分析,每个Redis只需要处理5个G的数据,也是非常快的
从硬件上来分析,每个Redis只需要一般配置就可以达到我们的需求。
从扩展上来分析,随着数据的增多,我们只需要不断加Redis实例就够了。也是方便扩展的
单机跟集群最难处理的点就是
- 分布式的一致性,
- 数据在多个实例上如何分布,
- 客户端如何得到自己想到的数据存在哪个实例上。
下面我们先从 数据如何分布上 进行介绍。我们可以采用Redis Cluster方案。
Redis Cluster 方案采用哈希槽,来处理数据和实例之间的映射关系。每个键值对都会根据它的 key,被映射到一个哈希槽中。主要分两步实现
- 首先根据键值对的 key,按照CRC16 算法计算一个 16 bit 的值;
- 然后,再用这个 16bit 值对 切片集群的哈希槽总数取模,得到模数,每个模数代表一个相应编号的哈希槽。
接下来介绍一下 客户端如何定位数据
Redis 实例会把自己的哈希槽信息发给和它相连接的其它实例,来完成哈希槽分配信息的扩散。当实例之间相互连接后,每个实例就有所有哈希槽的映射关系了。
客户端收到哈希槽信息后,会把哈希槽信息缓存在本地。当客户端请求键值对时,会先计算键所对应的哈希槽,然后就可以给相应的实例发送请求了。
哈希值随着数据的增多与减少并不是一成不变的,任何的增多与减少Redis都需要重新分配哈希槽。同时为了数据能均匀的分散在多个实例上,Redis也会把哈希槽在实例上重新分布一遍。
Redis的实例与实例之后可以通过相互传递消息获取最新的哈希槽分配信息,但是客户端无法感知,这就导致客户端的缓存数据与Redis的哈希槽会有不一致的情况。如何解决 ?
Redis Cluster 方案提供了一种 重定向机制,所谓的“重定向”,就是指,客户端给一个实例发送数据读写操作时,这个实例上并没有相应的数据时,Redis会给客户端发送一个MOVED命令,这个MOVED命令就包含了新实例的IP和端口。然后客户端要再给一个新实例发送操作命令就拿到了自己想要的数据了。
GET hello:key (error) MOVED 13320 172.16.19.5:6379
细节扩展
- Redis给客户端返回一个新实例信息,客户端再次请求时,同时也会修改本地的缓存,把当前的key更新到缓存中。
- 如果客户端请求的这个key刚好遇到了重新分配哈希槽途中,且数据还没有完全迁移完。就会返回ACK报错信息,这个命令的意思是,让这个实例允许执行客户端接下来发送的命令。然后,客户端再向这个实例发送 GET 命令,以读取数据。
GET hello:key (error) ASK 13320 172.16.19.5:6379 复制代码
ASK 命令表示两层含义:第一,表明数据还在迁移中;第二,ASK 命令把客户端所请求数据的最新实例地址返回给客户端,此时,客户端需要给Redis实例 发送 ASKING 命令,然后再发送操作命令。
和 MOVED 命令不同,ASK 命令并不会更新客户端缓存的哈希槽分配信息。所以如果客户端再次请求正在迁移的key,它还是会给实例 2 发送请求。这也就是说,ASK 命令的作用只是让客户端能给新实例发送一次请求,而不像 MOVED 命令那样,会更改本地缓存,让后续所有命令都发往新实例。
CAP原理
- C - Consistent ,一致性 ,访问所有的节点得到的数据应该是一样的。注意,这里的一致性指的是强一致性,也就是数据更新完,访问任何节点看到的数据完全一致,要和弱一致性,最终一致性区分开来。
- A - Availability ,可用性,所有的节点都保持高可用性。注意,这里的高可用还包括不能出现延迟,比如如果节点B由于等待数据同步而阻塞请求,那么节点B就不满足高可用性。也就是说,任何没有发生故障的服务必须在有限的时间内返回合理的结果集。
- P - Partition tolerance ,分区容忍性 这里的分区是指网络意义上的分区。由于网络是不可靠的,所有节点之间很可能出现无法通讯的情况,在节点不能通信时,要保证系统可以继续正常服务。
一个系统中不可能同时满足C,A,P三个条件,所以系统架构师在设计系统时,不要将精力浪费在如何设计能满足三者的完美分布式系统,而是应该进行取舍。由于网络的不可靠性质,大多数开源的分布式系统都会实现P,也就是分区容忍性,之后在C和A中做抉择。
接下来我们分三个场景分析:
- 在保证C和P的情况下:为了保证数据一致性,data1需要将数据复制给data2,即data1和data2需要进行通信。但是由于网络是不可靠的,我们系统有保证了分区容忍性,也就是说这个系统是可以容忍网络的不可靠的。这时候data2就不一定能及时的收到data1的数据复制消息,当有请求向data2访问number数据时,为了保证数据的一致性,data2只能阻塞等待数据真正同步完成后再返回,这时候就没办法保证高可用性了。
所以,在保证C和P的情况下,是无法同时保证A的。 - 在保证A和P的情况下:为了保证高可用性,data1和data2都有在有限时间内返回。同样由于网络的不可靠,在有限时间内,data2有可能还没收到data1发来的数据更新消息,这时候返回给客户端的可能是旧的数据,和访问data1的数据是不一致的,也就是违法了C。
也就是说,在保证A和P的情况下,是无法同时保证C的。 - 在保证A和C的情况下:如果要保证高可用和一致性,只有在网络情况良好且可靠的情况下才能实现。这样data1才能立即将更新消息发送给data2。但是我们都知道网络是不可靠的,是会存在丢包的情况的。所以要满足即时可靠更新,只有将data1和data2放到一个区内才可以,也就丧失了P这个保证。其实这时候整个系统也不能算是一个分布式系统了。
下述图片是CAP原理在各个系统中的应用
结尾
每个知识点都是自己整理浓缩表达出来的,部分有些不容易懂的地方请及时指出,我们一起共同进步!