复制

通过持久化的功能,Redis保证了即使在服务器重启的情况下也不会丢失(或少量丢失)数据。但是当数据存储在一台服务器时,当服务器的硬盘出现了故障,就会导致所有数据丢失。为了避免这种情况,我们通常的做法是将数据库复制多个副本部署在不同的服务器上,这样即使某一台出了故障,其他的还可以提供服务。

Redis为我们提供了复制(replication)功能,当一台数据库的数据更新之后,自动将更新的数据同步到其他数据库上。

在复制的概念中,数据库分为两类,一类是主数据库(Master),一类是从数据库(Slave)。
主数据库可以进行读写操作,当写操作导致数据变化时会自动将数据同步给从数据库。
而从数据库一般是只读的,并接收主数据库同步过来的数据。
一个主数据库可以拥有多个从数据库,而一个从数据库只能拥有一个主数据库。

从数据库不仅可以接收主数据库的同步数据,自己也可以同时作为主数据库存在,形成类似图的结构.

Redis中使用复制功能特别简单,只需要在从数据库的配置文件中加入"slaveof 主数据库地址 主数据库端口"即可。而主数据库不需要任何配置。

简单案例

(1)我们启动两个Redis实例,其中一个作为主数据库,一个作为从数据库。
首先我们不加任何参数启动Redis作为主数据库。
然后启动另一个Redis监听6380端口,并且加上slaveof作为从数据库。

reids-server  --port 6380  --slaveof  127.0.0.1 6379

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_主数据


(2)使用redis-cli分别连接到两个数据库,此时主数据库中的任何变化都会自动同步到从数据库中。

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_数据库_02


(3)我们使用INFO命令获取replication的相关信息。
可以看到6379数据库的角色为master(主数据库),并且已连接的从数据库为1.
6380数据库的角色为slave(从数据库)。

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_主数据_03


(4)我们操作验证一下

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_redis哨兵集群之间如何通讯_04


(5)默认情况下从数据库是只读的,如果直接修改从数据库的数据就会报错。

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_数据库_05

可以通过设置从数据库的配置文件中的slave-read-only为no使从数据库可写,但是尽量不要使用,因为没有意义。

除了通过配置文件和客户端来设置slaveof参数,还可以在运行时使用slaveof命令更换主数据库,如果当前数据库已经有了主数据库,那么就会停止和原来的主数据库同步而和我们新指定的主数据库同步。

原理

(1)复制初始化阶段
从库根据我们配置文件的的master节点ip和port向主库发起socket连接,主库收到socket连接之后将连接信息保存,此时连接建立;
当socket连接建立完成以后,从库向主库发送ping命令,以确认主库是否可用,此时的结果返回如果是PONG则代表主库可以用,否则可能出现超时或者主库此时在处理其他任务阻塞那么此时从库将断开socket连接,然后进行重试;
如果主库连接设置了密码,则从库需要设置masterauth参数,此时从库会发送auth命令,命令格式为“auth + 密码”进行密码验证,其中密码为masterauth参数配置的密码,需要注意的是如果主库设置了密码验证,从库未配置masterauth参数则报错,socket连接断开。
当身份验证完成以后,从数据库发送自己的监听端口,主库保存其端口信息,此时进入下一个阶段:数据同步阶段。

(2)数据同步阶段
主库和从库都确认对方信息以后,便可开始数据同步,此时从库向主库发送psync命令.主库收到该命令后判断是进行增量复制策略还是全量复制策略,然后根据策略进行数据的同步,当主库有新的写操作时候,此时进入复制第三阶段:命令传播阶段。

(3)命令传播阶段
当数据同步完成以后,在此后的时间里主从维护着心跳检查来确认对方是否在线,每隔一段时间(默认10秒,通过repl-ping-slave-period参数指定)主数据库向从数据库发送PING命令判断从数据库是否在线,而从数据库每秒1次向主节点发送REPLCONF ACK命令,命令格式为:REPLCONF ACK {offset},其中offset指从数据库保存的复制偏移量,作用一是汇报自己复制偏移量,主数据库会对比复制偏移量向从节点发送未同步的命令,作用二在于判断主数据库是否在线,从库接送命令并执行,最终实现与主库数据相同。

全量复制和增量复制

Redis2.6之前,在数据同步阶段。从数据库会向主数据库发送SYNC命令。同时主数据库接收到SYNC命令后会执行bgsave开始在后台保存快照(即RDB持久化的过程),并将保存快照期间接收到的命令缓存起来。当快照完成后,Redis会将快照文件和所有缓存的命令发送给从数据库。从数据库收到后,会载入快照文件并执行收到的缓存的命令。这叫做全量复制。

但这时会出现一个问题,当主从数据库之间的连接断开重连后,会重新进行复制初始化,即使从数据库只是有几条命令没有收到,主数据库也必须要将数据库的所有数据重新传输给从数据库。这使得主从数据库断线重连后的数据恢复过程效率很低下。

Redis2.8版的一个重要改进就是断线重连能够支持有条件的增量数据传输。当从数据库重新连接上主数据库后,主数据库只需要将断线期间执行的命令传送给从数据库,从而大大提高Redis复制的实用性。

要了解增量复制的过程,我们先来看几个概念。
(1)offset(复制偏移量):

主库和从库分别各自维护一个复制偏移量(可以使用info replication查看),用于标识自己复制的情况,在主库中代表主库向从库传递的字节数,在从库中代表从库同步的字节数。每当主库向从库发送N个字节数据时,主库的offset增加N,从库每收到主库传来的N个字节数据时,从库的offset增加N。因此offset总是不断增大,这也是判断主从数据是否同步的标志,若主从的offset相同则表示数据同步量,不同则表示数据不同步。

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_服务器_06

(2)replication backlog buffer(复制积压缓冲区):

复制积压缓冲区是一个固定长度的FIFO队列,大小由配置参数repl-backlog-size指定,默认大小1MB。需要注意的是该缓冲区由master维护并且有且只有一个,所有slave共享此缓冲区,其作用在于备份最近主库发送给从库的数据。

在主从命令传播阶段,主库除了将写命令发送给从库外,还会发送一份到复制积压缓冲区,作为写命令的备份。除了存储最近的写命令,复制积压缓冲区中还存储了每个字节相应的复制偏移量(如下图),由于复制积压缓冲区固定大小先进先出的队列,所以它总是保存的是最近redis执行的命令。

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_redis哨兵集群之间如何通讯_07

(3)run_id(服务器运行的唯一ID)

每个redis实例在启动时候,都会随机生成一个长度为40的唯一字符串来标识当前运行的redis节点,查看此id可通过命令info server查看。

当主从复制在初次复制时,主库将自己的runid发送给从库,从库将这个runid保存起来,当断线重连时,从库会将这个runid发送给主库。主库根据runid判断能否进行部分复制:

如果从库保存的runid与主节点现在的runid相同,说明主从库之前同步过,主库会根据offset偏移量之后的数据判断是否执行部分复制,如果offset偏移量之后的数据仍然都在复制积压缓冲区里,则执行部分复制,否则执行全量复制;如果从库保存的runid与主库现在的runid不同,说明从库在断线前同步的redis节点并不是当前的主库,只能进行全量复制;

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_服务器_08


-如果从服务器以前没有复制过任何主服务器,或者之前执行过SLAVEOF no one命令,那么从服务器在开始一次新的复制时将向主服务器发送PSYNC ? -1命令,主动请求主服务器进行完整重同步(因为这时不可能执行部分重同步);

- 相反地,如果从服务器已经复制过某个主服务器,那么从服务器在开始一次新的复制时将向主服务器发送PSYNC <runid> <offset>命令:其中runid是上一次复制的主服务器的运行ID,而offset则是从服务器当前的复制偏移量,接收到这个命令的主服务器会通过这两个参数来判断应该对从服务器执行哪种同步操作,如何判断已经在介绍runid时进行详细说明。

根据情况,接收到PSYNC命令的主服务器会向从服务器返回以下三种回复的其中一种:

(1)如果主服务器返回+FULLRESYNC <runid>  <offset>回复,那么表示主服务器将与从服务器执行完整重同步操作:
	其中runid是这个主服务器的运行ID,从服务器会将这个ID保存起来,在下一次发送PSYNC命令时使用;
	而offset则是主服务器当前的复制偏移量,从服务器会将这个值作为自己的初始化偏移量; 

(2)如果主服务器返回+CONTINUE回复,那么表示主服务器将与从服务器执行部分同步操作,
	 从服务器只要等着主服务器将自己缺少的那部分数据发送过来就可以了; 

(3)如果主服务器返回-ERR回复,那么表示主服务器的版本低于Redis 2.8,它识别不了PSYNC命令,
    从服务器将向主服务器发送SYNC命令,并与主服务器执行完整同步操作。

由此可见psync也有不足之处,当从库重启以后runid发生变化,也就意味者从库还是会进行全量复制,而在实际的生产中进行从库的维护很多时候会进行重启,而正是有由于全量同步需要主库执行快照,以及数据传输会带不小的影响。因此在4.0版本,psync命令做了以下改进。

redis4.0新版本除了增加混合持久化,还优化了psync(以下称psync2)并实现即使redis实例重启的情况下也能实现部分同步,下面主要介绍psync2实现过程。psync2在psync1基础上新增两个复制id(可使用info replication 查看):

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_服务器_09

master_replid: 复制id1(后文简称:replid1),一个长度为41个字节(40个随机串+’0’)的字符串,
			   每个redis实例都有,和runid没有直接关联,但和runid生成规则相同。
			   当实例变为从实例后,自己的replid1会被主实例的replid1覆盖。

master_replid2:复制id2(后文简称:replid2),默认初始化为全0,用于存储上次主实例的replid1。

在4.0之前的版本,redis复制信息完全丢失,所以每个实例重启后只能进行全量复制,到了4.0版本,仍然可以使用部分同步,其实现过程:

第一步:存储复制信息

redis在关闭时,通过shutdown save,都会调用rdbSaveInfoAuxFields函数,把当前实例的repl-id和repl-offset保存到RDB文件中,当前的RDB存储的数据内容和复制信息是一致性的可通过redis-check-rdb命令查看。

第二步:重启后加载RDB文件中的复制信息

redis加载RDB文件,会专门处理文件中辅助字段(AUX fields)信息,把其中repl_id和repl_offset加载到实例中,分别赋给master_replid和master_repl_offset两个变量值,特别注意当从库开启了AOF持久化,redis加载顺序发生变化优先加载AOF文件,但是由于aof文件中没有复制信息,所以导致重启后从实例依旧使用全量复制!

第三步:向主库上报复制信息,判断是否进行部分同步

从实例向主库上报master_replid和master_repl_offset+1;从实例同时满足以下两条件,就可以部分重新同步,否则执行全量同步:

从实例上报master_replid串,与主实例的master_replid1或replid2有一个相等,用于判断主从未发生改变;
从实例上报的master_repl_offset+1字节,还存在于主实例的复制积压缓冲区中,用于判断从库丢失部分是否在复制缓冲区中;

psync2除了解决redis重启使用部分同步外,还为解决在主库故障时候从库切换为主库时候使用部分同步机制。redis从库默认开启复制积压缓冲区功能,以便从库故障切换变化master后,其他落后该从库可以从缓冲区中获取缺少的命令。该过程的实现通过两组replid、offset替换原来的master runid和offset变量实现:

第一组:master_replid和master_repl_offset:如果redis是主实例,则表示为自己的replid和复制偏移量; 
		如果redis是从实例,则表示为自己主实例的replid1和同步主实例的复制偏移量。

第二组:master_replid2和second_repl_offset:无论主从,都表示自己上次主实例repid1和复制偏移量;
		用于兄弟实例或级联复制,主库故障切换psync。

判断是否使用部分复制条件:如果从库提供的master_replid与master的replid不同,且与master的replid2不同,或同步速度快于master; 就必须进行全量复制,否则执行部分复制。

以下常见的主从切换都可以使用部分复制:

一主一从发生切换,A->B 切换变成 B->A ;
一主多从发生切换,兄弟节点变成父子节点时;
级别复制发生切换, A->B->C 切换变成 B->C->A;

用一句redis开发者话来说psync2,尽管它不是非常完美,但是已经非常实用。

乐观复制

Redis采用了乐观复制的复制策略,容忍在一定时间内主从数据库的内容是不同的,但是两者的数据会最终同步。
具体来说,Redis在主从数据库之间复制数据的过程本身是异步的,这意味着,主数据库执行完客户端请求的命令后会立即将命令在主数据库的执行结果返回给客户端,并异步地将命令同步给从数据库,而不会等待从数据库接收到该命令后再返回给客户端。这一特性保证了启用复制后主数据库地性能不会受到影响
但另一方面也会产生一个主从数据库数据不一致地时间差,当主数据库执行了一条写命令后,主数据库的数据已经发生的变动,然而在主数据库将该命令传送给从数据库之前,如果两个数据库之间的网络连接断开了,此时两者之间的数据是不一致的。从这个角度来看,主数据库是无法得知某个命令最终同步给了多少个从数据库的,
不过Redis提供了两个配置选项来限制只有当数据至少同步给指定数量的从数据时,主数据库才是可写的
min-slaves-to-write 3
min-slaves-max-lag 10
上面的配置中,min-slaves-to-write表示只有当3个或3个以上的从数据库连接到主数据库时,主数据库才是可写的,否则会返回错误.
min-slaves-max-lag表示允许从数据库最长失去连接的时间,如果从数据库最后与主数据库联系(即发送REPLCONF ACK命令)的时间小于这个值,即认为从数据库还在保持与主数据库的连接.
举个例子,上面的配置,主数据库假设与3个从数据库相连,其中一个从数据库上一次与主数据库联系是9秒前,这时主数据库可以正常接受写入.
一旦一秒过后这台数据库依旧没有活动,则主数据库则认为目前连接的从数据库只有2个,从而拒绝写入.
这一特性默认是关闭的,在分布式系统中,打开并合理配置该选项后可以降低主从架构中因为网络分区导致的数据不一致的问题.

读写分离与一致性

通过复制可以实现读写分离,以提高服务器的负载能力.
在常见的场景中(如电子商务网站),读的频率大于写,当单机的Redis无法应付大量的读请求时
可以通过复制功能建立多个从数据库节点,主数据库只进行写操作,而从数据库负责读操作.
这种一主多从的结构很适合读多写少的场景.

从数据库的持久化

为了提高性能,可以通过复制功能建立一个(或若干个)从数据库,并在从数据库中启用持久化,同时在主数据库禁用持久化.
当从数据库崩溃重启后主数据库会自动将数据同步过来,所以无需担心数据丢失.
然而当主数据库崩溃时,情况就比较复杂.
手工通过从数据库恢复主数据库数据时,需要严格按照以下两步进行

(1)在从数据库中使用SLAVEOF NO ONE命令将从数据库提升成主数据库继续服务
	
	(2)启动之前崩溃的主数据库,然后使用SLAVEOF命令将其设置成新的主数据库的从数据库,即可将数据同步回来.

当开启复制且主数据库关闭持久化功能时,一定不要使用进程管理工具令主数据库崩溃后自动重启.
同样当主数据库所在的服务器因故关闭时,也要避免直接重新启动.
因为当主数据库重新启动后,因为没有开启持久化功能,所有数据库中的数据都被清空,
这时从数据库依然会从主数据库中接收数据,使得所有从数据库也被清空,导致从数据库的持久化失去意义.

无论哪种情况,手工维护从数据库或主数据库的重启以及数据恢复都比较麻烦,好在Redis提供了一种自动化方案哨兵来实现这一过程,无需我们手动操作,我们将在后面学习,这里只是抛砖引玉提出这一问题。

无硬盘复制
我们前面介绍Redis复制的工作原理时介绍了复制是基于RDB方式的持久化实现的,即主数据库端在后台保存RDB快照,从数据库端则接收并载入快照文件.
优点是可以显著地简化逻辑,但是缺点也很明显

(1)当主数据库禁用RDB快照时,如果执行了复制初始化操作,Redis依然会生成RDB快照,所以下次启动后主数据库会以该快照恢复数据.
	因为复制发生地时间不能确定,这使得恢复地数据可能是任何时间点的
	
	(2)因为复制初始化时需要在硬盘中创建RDB快照文件,所以如果硬盘性能很慢时这一过程会对性能产生影响.

从2.8版本开始,Redis引入了无硬盘复制选项,开启该选项后,Redis在与从数据库进行复制初始化时将不会将快照内容存储到硬盘上,而是直接通过网络发送给从数据库,避免了硬盘的性能瓶颈.

可以在配置文件中开启此功能
	repl-diskless-sync yes

哨兵

哨兵的作用就是监控Redis系统的运行情况。主要功能有以下两个:

  • 监控主数据库和从数据库是否正常运行
  • 主数据库出现故障时自动将从数据库转换为主数据库

哨兵是一个独立的进程,因为单个哨兵本身就可能出故障,可以使用多个哨兵保证系统的稳健来持续监控Redis。当有多个哨兵时,哨兵不仅监控主数据库和从数据库,哨兵之间也会相互监控

入门案例

我们通过一个一主二从的环境来演示一下哨兵的实际运用。

(1)首先启动三个Redis实例,一个作为主数据库,两个作为从数据库

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_服务器_10


redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_服务器_11


redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_数据库_12

(2)接下来建立一个配置文件sentinel.conf ,来配置哨兵
内容为 sentinel monitor mumaster 127.0.0.1 6379 1
mymaster表示要监控的主数据库的名字,可以自己随便自定义。
后面是主数据库的地址和端口
最后的1表示最低通过票数,后面会详解。

配置哨兵时,只需要配置其监控主数据库即可,哨兵会自动发现主数据库的所有从数据库

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_redis哨兵集群之间如何通讯_13

(3)启动哨兵,需要将配置文件传递给哨兵

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_redis哨兵集群之间如何通讯_14

(4)我们杀掉6379的redis实例

+sdown表示哨兵主观认为主数据库停止服务了。
+odown表示哨兵主观认为主数据库停止服务了。
然后哨兵开始进行故障恢复,即挑选一个从数据库升级为主数据库。
+try-failover表示哨兵开始进行故障恢复
+failover-end表示哨兵完成故障恢复。
其间的进行的操作比较复杂,我们放在后面详细讲解。
+switch -master 表示主数据库从6379转换为6380.
+slave则列出了主数据库的两个从数据库6381和6379,可见哨兵并没有完全清除停止服务的redis信息,这是因为服务可能在某个时间恢复。

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_数据库_15

查看6380和6381的信息

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_服务器_16


redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_主数据_17

(5)我们再重启6379数据库

哨兵会监测到这一变化。

-sdown表示哨兵已经恢复服务了。

+convert-to-slave 表示将6379设置为6380的从数据库

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_主数据_18

实现原理

哨兵启动时,会从配置文件中读取信息,找到需要监控的主数据库。启动后,哨兵会与要监控的主数据库建立两条连接,这两个连接的建立方式与普通的Redis客户端一样。其中一条连接用来订阅主数据库的_sentinel_:hello频道以获取其他同样监控该数据库的哨兵节点的信息。另外哨兵也需要定期向主数据库发送info等命令来获取主数据库本身的信息,因为一个连接进入订阅模式不能执行其他命令,所以哨兵会使用另一条连接来发送这些命令。

和主数据库的连接建立完成后,哨兵会定时执行以下操作
(1)每十秒哨兵会向主数据库和从数据库发送INFO命令
(2)每两秒哨兵会向主数据库和从数据库的_sentinel_:hello频道发送自己的信息
(3)每一秒哨兵会向主数据库、从数据库和其他哨兵节点发送PING命令

首先,发送INFO命令使得哨兵可以获得当前数据库的相关信息从而实现新节点的自动发现。我们前面的配置文件里只添加了主数据库的信息,哨兵正是借助INFO命令来获取该主数据库所有的从数据库的信息。启动后,哨兵发送INFO命令获取从数据库信息,然后与每个从数据库建立两个连接。然后,哨兵会每十秒定时的向所有已知的主从数据库发送INFO命令获取信息的更新及执行相关操作,例如建立新增从数据库的监控等等。

接下来哨兵会向主数据库和从数据库的_sentinel_:hello频道发送信息来和同样监控该数据库的哨兵分享自己的信息,发送的消息内容为

<哨兵的地址>,<哨兵的端口>,<哨兵的运行id>,<哨兵的配置版本>,<主数据库的名字>,<主数据库的地址>,<主数据库的端口>,<主数据库的配置信息>,

当其他哨兵收到消息后,会判断发消息的哨兵是不是新的哨兵,如果是则将其加入已发现的哨兵列表中并建立一个连接(哨兵与哨兵之间只会建立一个连接用来发送PING命令)。

哨兵会定时监控这些数据库和其他哨兵服务有没有停止,这是通过发送PING命令实现的,发送的时间间隔由哨兵配置文件中这个参数决定。

当down-after-milliseconds小于一秒时,哨兵会按照指定的时间间隔按时发送PING命令。
当down-after-milliseconds大于一秒时,哨兵会每隔一秒发送PING命令。

//每600毫秒发送一次PING命令
sentinel down-after-milliseconds   mymaster 600
//每1秒发送一次PING命令
sentinel down-after-milliseconds   mymaster 60000

当超过down-after-milliseconds指定时间后,如果被PING的数据库或哨兵未进行回复,则哨兵认为其主观下线。主观下线表示从当前哨兵进程来看,该节点已经下线。如果该节点是主数据库,则哨兵会进一步判断是否需要对其进行故障恢复:
哨兵会发送sentinel is-master-down-by-addr命令询问其他哨兵节点是否也认为其主观下线,如果达到指定数量时(这个数量就是我们配置文件中的最后那个参数),该哨兵就会认为该节点客观下线,并选举领头的哨兵节点对主从系统进行故障恢复。

故障恢复需要由领头哨兵来完成,以保证同一时间只有一个哨兵进行故障恢复。选举领头哨兵使用了Raft算法,具体过程如下:
(1)发现主数据库客观下线的哨兵节点向其他每个哨兵节点发送命令,要求对方选择自己成为领头哨兵。
(2)如果该哨兵节点发现有超过半数并且超过我们所配置的参数值的哨兵节点同意选择自己,则该哨兵节点称为领头哨兵。
(3)当有多个哨兵节点同时参选领头节点选举,有可能出现没用节点当选。此时每个参选节点将等待一个随机事件重新进行选举,直到选举成功。

领头哨兵选举成功后,则会开始进行故障恢复。
首先领头哨兵会从停止的主数据库下的从数据库中选择一个担任新的主数据库,挑选算法如下:
(1)所有在线的从数据库中,选择优先级最高的从数据库库,优先级可以通过slave-priority参数来设置
(2)如果有多个最高优先级的从数据库,则复制的命令偏移量越大(即数据越完整)越优先。
(3)如果以上条件都一样,则选择运行id小的从数据库。
选出从数据库后,领头哨兵会向其发送SLAVEOF NO ONE使其升级为主数据库,而后哨兵会向其他数据库发送SLAVEOF命令使其成为新主数据库的从数据库。最后一步更新内部记录,将已经停止服务的旧主数据库作为新主数据库的从库,当恢复服务时以从数据库的身份运行。

哨兵部署原则

不是强制的,但是为了系统的稳定性我们一般遵守这个原则。
(1)为每个数据库部署一个哨兵
(2)使每个哨兵与其对应的数据库节点网络情况相同或相近
(3)设置选举通过的个数值为N/2+1(N为哨兵节点数量)

集群

当Redis中的数据量过多时,我们就需要对其进行分区处理。
以前我们可以通过客户端分片,由客户端决定每个键交由哪个数据库节点储存,下次客户端读取该键时直接到该节点读取。但是在客户端分片后,如果想增加更多的节点,就需要对数据进行手工迁移,相对比较复杂。

我们可以采用预分片在一定程度上来避免这种问题,在节点部署初期,提前考虑好日后的数据存储规模,建立足够多的节点,因为Redis非常轻量,前期数据不是很多的时候,甚至两台服务器就可以运行大量节点,当以后数据量增多,只需要将某些节点迁移到其他服务器即可。

Redis为我们提供了集群(Cluster)功能,可以帮助我们很好的解决问题。

每个Redis集群中的节点都需要打开两个TCP连接。一个连接用于正常的给Client提供服务,比如6379,还有一个额外的端口(通过在这个端口号上加10000)作为数据端口,比如16379。第二个端口(本例中就是16379)用于集群总线,这是一个用二进制协议的点对点通信信道。这个集群总线(Cluster bus)用于节点的失败侦测、配置更新、故障转移授权,等等。客户端从来都不应该尝试和这些集群总线端口通信,它们只应该和正常的Redis命令端口进行通信。注意,确保在你的防火墙中开放着两个端口,否则,Redis集群节点之间将无法通信。

命令端口和集群总线端口的偏移量一定是10000。

也就是,要想集群正常工作,集群中的每个节点需要做到以下两点:
(1)正常的客户端通信端口(通常是6379)必须对所有的客户端都开放,换言之,所有的客户端都可以访问
(2)集群总线端口(客户端通信端口 + 10000)必须对集群中的其它节点开放,换言之,其它任意节点都可以访问

配置集群

接下来我们配置一个三主三从的集群。
使用集群前,首先需要打开主数据库配置文件中的cluster-enabled选项,从数据库不要打开,否则启动时会报错。
集群会将当前节点记录的集群状态持久化到存储在指定文件中,可以通过cluster-config-file来修改,每个数据库必须对应不同的文件,否则会启动失败,所以我们需要在我们六个数据库的配置文件中将cluster-config-file更改成不同的名字。

(1)首先我们启动六个数据库
可以通过INFO cluster 来判断集群是否正常启用,cluster_enabled为1表示正常启用

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_服务器_19

(2)Redis提供了一个辅助工具redis-trib.rb,它有一个功能就是可以帮我们将节点加入到同个集群中,因为该工具是ruby编写,所以我们在运行前需要安装Ruby。redis-trib.rb依赖于gem包redis,在安装完Ruby之后,通过gem install redis来安装该工具。
然后我们需要去到redis-trib.rb的目录下去执行,该文件存放在redis的src目录下。

执行以下命令,不要忘记./ 
create命令用来创建集群
replicas  1 表示每个主数据库拥有一个从节点,所以整个集群有三主三从

./redis-trib.rb  create --replicas  1  127.0.0.1:6379  127.0.0.1:6380   127.0.0.1:6381   127.0.0.1:6382  127.0.0.1:6383   127.0.0.1:6383

执行这个命令的时候我碰到了很多问题,首先我发现在节点上执行命令时会出现Hash slot not served的错误
这时候我们只需要对出错的节点执行修复命令

./redis-trib.rb fix 127.0.0.1:6379

执行完命令后,会输出如下内容,内容是节点的具体分配方案。

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_redis哨兵集群之间如何通讯_20

如果没问题输入yes开始创建集群,出现以下内容即表示创建成功

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_数据库_21

这一步我遇到了ERR Slot 0 is already busy (Redis::CommandError)的错误
我们需要在每个节点上执行flushall和cluster reset命令进行解决。

集群创建的过程

首先redis-trib.rb会以客户端的形式尝试连接所有的节点,并发送PING命令以确定节点能够正常服务。如果有任何一个节点无法连接,则创建失败。同时发送INFO命令获取每个节点的运行id以及是否开启集群功能(即cluster_enabled为1)

准备就绪后,集群会向每个节点发送CLUSTER MEET命令,格式为CLUSTER MEET ip port,这个命令用来告诉当前节点指定ip和port上在运行的节点也是集群的一部分,从而使六个节点归入一个集群。

然后redis-trib.rb会分配主从节点,分配的原则是尽量保证每个主数据库运行在不同的ip上,同时每个主数据库和从数据库均不运行在同一ip地址上,以保证系统的容灾能力。分配结果如图中所示:

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_服务器_22

分配完成后,会为每个主数据库分配插槽,分配插槽的过程其实就是分配哪些键归哪个节点所负责。之后对每个要成为从数据库的节点发送CLUSTER REPLICATE 主数据库的运行id 来将当前节点转换为从数据库并进行复制操作。

增加节点

前面讲redis-trib.rb是使用CLUSTER MEET命令来使每个节点认识集群中的其他节点的,当我们需要加入新的节点时,也需要使用该命令。只需要向新节点发送

CLUSTER MEET ip port

ip和port可以是集群中的任一节点,新节点收到该命令后,会与指定ip和port的的节点进行握手,握手成功后,指定节点会使用Gossip协议将新节点的信息通知给每一个集群中的节点,非常方便。

插槽的分配

新的节点加入集群后有两种选择,要么使用CLUSTER REPLICATE命令复制每个主数据库来以从数据库的形式运行,要么向集群申请分配插槽(slot)以主数据库的形式运行。

在一个集群中,所有的键会被分配给16384个插槽,每个主数据库会负责处理其中的一部分插槽。具体的分配情况如下。

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_服务器_23


虽然redis-trib.rb初始化集群时分配给每个节点的插槽是连续的,但Redis并没有对其作任何限制,我们可以将任意的插槽分配给任意节点。

我们先来介绍一个键与插槽的对应关系。Redis将每个键的键名的有效部分使用CRC16算法计算出散列值,然后取对16384的余数,这样使得每个键都可以分配到16384个插槽中。
键名的有效部分是指:
(1)如果键名包含{符号,且在{符号后面存在对应的},并且{}之间至少有一个字符,则有效部分是指{}之间的内容。
(2)如果不满足上条规则,则整个键名都是有效部分。
例如:“hello.world”的有效部分就是hello.world。{user}:name的有效部分就是user。

如果命令涉及多个键,例如MGET,只有当所有键都位于同一个节点时Redis才能正常运行,否则报错。
利用键的分配规则,我们可以将所有相关的键的有效部分设置成相同的使得相关键都能分配到同一个节点。

如何将指定插槽分配给指定节点?有以下几种情况
(1)插槽之前没有被分配过,现在想分配给指定节点。
(2)插槽之前被分配过,现在想移动到指定节点。

第一种情况我们使用CLUSTER ADDSLOTS命令来实现,redis-trib.rb就是通过该命令在创建集群时为节点分配插槽的。

CLUSTER  ADDSLOTS   slots1 slots2..... slotsn

例如想将100和101插槽分配给指定节点,只需要在该节点执行以下命令即可。

CLUSTER  ADDSLOTS   100 101

如果该插槽已经被分配过,则会报错。
可以通过命令CLUSTER SLOTS来查看插槽的分配情况。

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_数据库_24


返回的结果中第一条和第二条表示插槽的开始号码和结束号码,后面的值则为负责这些插槽的数据库,主数据库始终在第一位

对于第二种插槽之前被分配过,现在想移动到指定节点的情况,redis-trib.rb提供了一个比较方便的方式对插槽进行迁移。
我们来通过例子演示一下。
(1)首先执行如下命令

redis-trib.rb  reshard  127.0.0.1:6380

reshard告诉Redis要进行重新分片,127.0.0.1:6380为集群中的任意一个节点。redis-trib.rb会自动获取集群信息。
执行完以后,首先会询问想要迁移多少个插槽

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_数据库_25


我们只迁移一个来看看就行,所以输入1.

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_服务器_26


接下来会询问要把插槽迁移到哪个节点,我们可以通过CLUSTER NODES获取我们想要迁移到的节点的运行id。

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_主数据_27


最后一步询问从哪个节点移出插槽,我们输入对应节点的运行id,最后输入done即可完成迁移。

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_服务器_28

我们输入CLUSTER SLOTS再来查看插槽的分配情况

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_主数据_29


我们可以看到多了一条记录,5461插槽由6381节点负责。

那么redis-trib.rb实现重新分片的原理是什么呢?我们如何进行手工重新分片
在要移动的插槽号的节点上使用如下命令(在其他节点好像不管用)

CLUSTER  SETSLOT  插槽号  NODE 新节点的运行id

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_主数据_30

然后这样迁移插槽的前提是插槽中没有任何键,因为该命令不会将插槽中的键给一起迁移,这就造成了客户端在指定节点找不到未迁移的键,对客户端来说,这些键“丢失了”。
为此需要手工获取插槽中存在哪些键,然后将每个键迁移到新节点才行。

手工获取插槽中存在哪些键的命令是

CLUSTER  GETKEYSINSLOT  插槽号  要返回的键的数量

然后对每个键使用MIGRATE命令将其迁移到目标节点

MIGRATE  目标节点地址 目标节点端口 键名  数据库号码  超时时间  [copy]  [replace]

其中copy选项表示不将键从当前数据库删除。replace表示如果目标节点存在同名键,则覆盖。
因为集群模式只能使用0号数据库,所以数据库号码始终是0.

这样我们就知道了如何迁移的方法,但是有一个问题,如果需要迁移的数据量很大,整个过程则需要相当长的时间,那么我们应该在什么时候执行CLUSTER SETSLOT来完成插槽的交接呢?
如果在未迁移完成前执行,那么客户端就会从新的节点读取,自然可能会读不到。
如果在迁移完成后执行,然后有些键已经迁移过去,这时在旧的节点上获取也可能读不到。那么redis-trib.rb是如何解决的呢?
Redis提供了两个命令来实现在集群不下线的情况下迁移数据。

CLUSTER  SETSLOT  插槽号 MIGRATING  新节点的运行id
CLUSTER  SETCLOT  插槽号  IMPORTING  原节点的运行id

进行迁移时,假设要把0号插槽从A迁移到B,此时redis-trib.rb会依次执行如下操作:
(1)在B执行CLUSTER SETCLOT 0 IMPORTING A
(2)在A执行CLUSTER SETSLOT 0 MIGRATING B
(3)执行CLUSTER GETKEYSINSLOT 0获取0号插槽的键
(4)对每个键执行MIGRATE命令完成从A到B的迁移
(5)执行CLUSTER SETSLOT 0 NODE B 完成迁移

可以看到redis-trib.rb首先做了前两步操作,这个就是为了解决我们上面提出的问题。执行完前两步之后,当客户端向A请求插槽0中的键时,如果键还未迁移,则正常执行。如果已经迁移,则返回一个ASK跳转请求,告诉客户端这个键在B里,客户端收到ASK跳转请求后,首先向B发送ASKING命令,然后重新发送刚才的命令。
相反,当客户端向B请求插槽0中的键时,如果前面执行了ASKING命令,则返回键值,否则MOVED跳转请求,重定向到A。

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_主数据_31

redis哨兵集群之间如何通讯 redis哨兵和集群模式对比_服务器_32

获取与插槽对应的节点

我们前面可以通过命令知道哪些插槽由哪些节点管理,那么我们如何获取某个键在哪个节点呢?
实际上,当客户端向集群中的任意一个节点发送命令后,该节点会判断相应的键是否在当前节点中,如果在则正常处理,如果不在,则返回一个MOVE重定向请求,告诉客户端这个键目前由哪个节点负责,然后客户端再重新发送一次请求到目标节点。

故障恢复

在一个集群中,每个节点都会定期向其他节点发送PING命令,并通过有没有收到回复来判断目标节点是否已经下线。
以我们的例子来讲,集群中的每个节点每隔一秒钟就会随机选择五个节点,然后选择其中最久没有响应的节点发送PING命令。
如果一定时间内目标节点没有回复,则发送命令的节点会认为其疑似下线, 如果要使在整个集群中的所有节点认为该节点下线,需要一定数量的节点认为该节点疑似下线才可以,具体过程为:
(1)一旦节点A认为节点B疑似下线,就会在集群中传播此消息,所有其他节点收到消息后会记录下来。
(2)当集群中的某一节点C收到半数以上的节点认为B疑似下线的信息时,就会将B标记为下线,并向集群中的其他节点传递信息。

在集群中,如果一个主数据库下线,那么就会出现部分插槽无法写入,如果该主数据库拥有至少一个从数据库,集群就进行故障恢复将其中的一个从数据库设置为主数据库,选择哪个从数据库和选举领头哨兵一样,都是基于Raft算法。
(1)发送其所在的主数据库下线的从数据库向集群中的每个节点发送请求,要求对方选择自己为主数据库。
(2)如果收到请求的节点没有选过其他人,则会同意
(3)如果从数据库发现由一半的节点同意,则其成为主数据库
(4)当有多个数据库节点参选,则可能会出现没有任何节点当选的可能。会挑选时间进行下一轮,直到选举成功。

当从数据库当选为主数据库后,会通过SLAVEOF NO ONE将自己转换为主数据库,并将主数据库的插槽交由自己负责。

如果一个至少负责一个插槽的主数据库下线并且没有从数据库可以进行故障恢复,则整个集群会默认进入下线状态无法继续工作,如果想集群能够正常工作,则需要修改参数。

cluster-require-full-coverage    no