replicaof [host ip] [port] //添加在每个slave节点中
从节点启动后,会自动连接到master节点,开始同步数据。如果master节点更改了,比如原来的master节点宕机了,选举了新的master节点,这个配置项就会被重写。
./redis-server --slaveof [host ip] [port]
slaveof [host ip] [port]
,使该Redis实例成为从节点。从节点也可以是其他节点的主节点,形成级联复制的关系。可以通过info replication
查看集群状态。从节点是只读的,不能执行写操作,执行写操作命令会报错。数据在主节点写入后,slave节点会自动从主节点同步数据。
取消主从关系:
replicaof
指令去除,此时从节点会变为主节点,不再复制数据。slaveof no one
断开连接,此时从节点会变为主节点,不再复制数据。Redis主从复制分为两类:
连接阶段:
replicationCron
,每隔一秒钟检查是否有新的主节点需要连接和复制。如果发现有主节点需要连接,就跟主节点建立连接。如果连接成功,从节点就让一个专门处理复制工作的文件事件处理器负责后续的复制工作。为了让主节点感知到从节点的存活,从节点会定时向主节点发送ping请求。建立连接以后,就可以同步数据了,这里也分成两个阶段。
数据同步阶段:
如果是新加入的从节点,那就需要全量复制,主节点通过bgsave命令在本地生成一份RDB快照,将RDB快照文件发给从节点(如果超时会重连,可以调大repl-timeout的值)。
如果从节点自己本来就有数据怎么办?
从节点首先需要清除自己的旧数据,然后使用RDB文件加载数据。
主节点生成RDB期间,接收到写命令怎么处理?
开始生成RDB文件时,主节点会把所有新的写命令缓存在内存中。在从节点保存了RDB之后,再将新的写命令复制给从节点。(与AOF重写rewrite期间接受到的命令的处理思路是一样的)
第一次全量同步完了,主从已经保持一致了,后面就是持续把接受到的命令发送给从节点。
命令传播阶段:
主节点持续把写命令,异步复制给从节点。一般情况下,我们不会使用Redis做读写分离,因为Redis的吞吐量已经够高了,做集群分片之后并发的问题更少,所以不需要考虑主从延迟的问题。只能通过优化网络来改善。
第二种情况是增量复制:
如果从节点有一段时间断开了与主节点的连接,是不是要把原来的数据全部清空,重新全量复制一遍?效率太低了。增量复制中从节点通过master_repl_offset
记录了偏移量,以便使用增量复制。
为了降低主节点磁盘开销,Redis从6.0开始支持无盘复制,主节点生成的RDB文件不再保存到磁盘,而是直接通过网络发送给从节点。无盘复制适用于主节点所在机械磁盘性能较差但是网络宽带比较富裕的场景。
主从复制并没有解决高可用的问题。在一主一从或者一主多从的情况下,如果服务器挂了,对外提供的服务就不可用了,单点问题没有得到解决。如果每次都是手动把之前的从服务器切换成主服务器,然后再把剩余节点设置为它的从节点,这就比较费事费力,还会造成一定时间的服务不可用。
如何实现高可用呢?通过运行监控服务器来保证服务的可用性。如果主节点超过一定时间没有给监控服务器发送心跳报文,就把主节点标记为下线,然后把某一个从节点变成主节点,应用每一次都是从这个监控服务器拿到主节点的地址。2.8版本后,提供了一个稳定版本的Sentinel,用来解决高可用的问题。
我们会启动奇数个数的Sentinel的服务,启动方式:
通过Sentinel的脚本启动:
./redis-sentinel ../sentinel.conf
使用redis-server的脚本加Sentinel参数启动:
./redis-server ../sentinel.conf --sentinel
Sentinel本质上只是一个运行在特殊模式之下的Redis。Sentinel通过info命令得到被监听Redis机器的主从节点等信息。为了保证监控服务器的可用性,我们会对Sentinel做集群的部署。Sentinel既监控了所有的服务,Sentinel之间也有互相监控。Sentinel本身没有主从之分,地位是平等的,只有Redis服务节点有主从之分。
Sentinel是如何直到其他Sentinel节点的存在的?
Sentinel是一个特殊状态的Redis节点,它也具有订阅发布功能。Sentinel上线时,给所有的Redis节点的名字为:_sentinel_:hello
的channel发送消息。每个哨兵都订阅了所有Redis节点名字为_sentinel_:hello
的channel,所以可以互相感知对方的存在,而进行监控。
Sentinel是如何知道主节点宕机了的?
Sentinel默认每秒一次向Redis服务节点发送ping命令,如果在指定的时间内没有收到有效回复,Sentinel会将该服务器标记为下线。(主观下线)。由参数:# sentinel conf
控制。默认是30秒。但是,只有你发现主节点下线,并不代表主节点真的下线了,也有可能是自己的网络问题。所以这个时候第一个发现主节点下线的Sentinel节点会继续询问其他的Sentinel节点,确认这个节点是否下线。如果quorum数量的Sentinel节点都认为主节点下线,主节点在被真正确认下线(客观下线)。
quorum:确认客观下线的最少的哨兵数量,通过配置项进行设定
确定主节点下线之后,就需要重新选举主节点。Sentinel集群此时便开始故障转移,从从节点中选举一个节点作为主节点。
由Sentinel完成。如果需要从redis集群中选举一个节点作为主节点,首先需要从Sentinel集群中选举一个Sentinel节点作为Leader。Sentinel使用与原生略有区别的Raft算法完成。
原生-Raft:
核心思想:先到先得,少数服从多数。Spring cloud 注册中心解决方案Consul也使用到了Raft协议。
文字描述:
Sentinel的Raft协议在此基础之上,略有不同:
Leader确定以后,开始对redis其余节点进行故障转移:
对于所有的从节点,按照以下顺序来进行选举主节点:
slave_priority 100
数值越小优先级越高。至此,主节点选举完毕。
为什么Sentinel集群至少是3个?
如果Sentinel集群中只有2个节点,那么Leader最低票数至少为2,当该集群中有一个节点故障后,仅剩的一个节点便永远无法成为Leader。
确定了主节点之后,对将要成为主节点的从节点发送slaveof no one
让它成为独立节点,并对其他从节点发送slaveof [master ip] [master port]
,让他们成为这个主节点的从节点,故障转移完成。
主从切换的过程中会丢失数据,因为只有一个主节点。只能单点写,没有解决水平扩容的问题。如果数据量十分大,就需要对Redis进行数据分片。
实现Redis数据分片,有三种解决方案:
Jedis客户端中,支持分片功能。RedisTemplate就是对Jedis的封装。
ShardedJedis:
Jedis有几种连接池,其中有一种支持分片。
这里通过这个连接池分别连接到两个Redis服务。插入一百条数据后,发现一台服务器上有44个key,另外一台为56个key。
ShardedJedis是如何做到的?
如果希望数据分布相对均匀,首先可以考虑哈希后取模,因为key不一定是整数,所以先计算哈希。例如:hash(key)%N,根据余数,决定映射到哪个节点。这种方式比较简单,属于静态的分片规则,但是一旦节点数量发生变化(新增或者减少),由于取模的N发生变化,数据就需要重新分布。为了解决这个问题,又使用了一致性哈希算法。
ShardedJedis实际上就是使用一致性哈希算法。原理是:
把所有的哈希值空间组织成一个虚拟的圆环(哈希环),整个空间按照顺时针方向组织。因为是圆环,所以0和2^32-1是重叠的。
假如有四台机器要哈希环来实现映射(分布数据),我们就先根据机器的名称或者IP计算哈希值,然后分布到圆环中。对key计算后,得到它在哈希环中对应的位置。沿着哈希环顺时针找到第一个Node,就是数据存储的节点。
一致性哈希解决了动态增减节点时,所有数据都需要重新分布的问题,它只会影响到下一个相邻的节点,对其他节点没有影响。但是也存在一个缺点:
因为节点不一定是均匀分布的,特别是在节点数量比较少的情况下,所以数据不能得到均匀分布,为了解决这个问题,我们需要引入虚拟节点。
那么,节点是怎么实现的?
jedis实例被放到了一颗红黑树TreeMap()中,当存取键值对时,计算键的哈希值,然后从红黑树上摘下比该值大的第一个节点上的JedisShardInfo ,随后从resources取出Jedis。在jedis.getShard(“k”+i).getClient()
获取到真正的客户端。
public R getShard(String key) { return resources.get(getShardInfo(key)); } public S getShardInfo(byte[] key) { // 获取比当前key的哈希值要大的红黑树的子集 SortedMap<Long, S> tail = nodes.tailMap(algo.hash(key)); if (tail.isEmpty()) { // 没有比它大的了,直接从nodes中取出 return nodes.get(nodes.firstKey()); } // 返回第一个比它大的JedisShardInfo return tail.get(tail.firstKey()); }
使用ShardedJedis之类的客户端分片代码的优势是配置简单,不依赖于其他中间件,分区的逻辑自定义,比较灵活。但是居于客户端的方案,不能实现动态的服务增减,每个客户端需要自行维护分片策略,存在重复代码。
典型的代理分区的方案有Twitter开源的Twemproxy和国内的豌豆荚开源的Codis。
优点:比较稳定,可用性高
缺点:
是一个代理中间件,使用Go进行开发,跟数据库分库分表中间件的MyCat的工作层次是一样的,
客户端连接Codis和连接Redis没有区别。
**分片原理: **
Codis把所有的key分成了N个槽,每个槽对应一个分组,一个分组对应一个或一组Redis实例。Codis对key进行CRC32运算,得到一个32位的数字,然后模以N,得到余数,这个就是key对应的槽,槽后面就是Redis的实例,
如果需要解决单点问题,Codis也需要做集群部署,多个Codis节点需要运行一个Zookeeper或etcd/本地文件。
新增节点: 可以为节点指定特定的槽位,Codis也提供了自动均衡策略。
获取数据: 在Redis中的各个实例里获取到符合的key,然后再汇总到Codis中。
在Redis 3.0版本正式推出,用来解决分布式的需求,同时也可以实现高可用。跟Codis不一样,它是去中心化的,客户端可以连接到任意一个可用节点。
可以看作是由多个Redis实例组成的数据集合。客户端不需要关注数据的子集到底存在哪个节点,只需要关注这个集合整体。
使用虚拟槽来实现。Redis创建了16383个槽,每个节点负责一定区间的slot。
对象分布到Redis节点上时,对key用CRC16算法计算再%16384,得到一个slot的值,数据落到负责这个slot的Redis节点上。
为什么slot是16384个?
CRC16
算法产生的hash值有16bit,该算法可以产生2^16-=65536个值。换句话说,值是分布在0~65535之间。那作者在做mod
运算的时候,为什么不mod
65536,而选择mod
16384?很幸运的是,这个问题,作者是给出了回答的!
The reason is:
So 16k was in the right range to ensure enough slots per master with a max of 1000 maters, but a small enough number to propagate the slot configuration as a raw bitmap easily. Note that in small clusters the bitmap would be hard to compress because when N is small the bitmap would have slots/N bits set that is a large percentage of bits set.
转换理解以下就是:
myslots[CLUSTER_SLOTS/8]
。当槽位为65536时,这块的大小是:65536÷8÷1024=8kb
可以通过指令查看key属于哪个slot:cluster keyslot [key]
如何让相关的数据落到同一个节点上?
有些操作时不可以跨节点执行的。比如:multi key
解决方案:在key里加入{hash tag}即可,Redis在计算编号的时候会只获取{}之间的字符串进行槽号计算,这样由于上面两个不同的键,{}里面的字符串是相同的,因此他们可以被计算出相同的槽。
客户端连接到哪一台服务器?访问的数据不在当前节点上,怎么办?
那么我们就需要让客户端重定向。
如果我操作指令:set redis 1
返回(error) MOVED 13724 127.0.0.1:7293
。则表示根据key计算出来的slot不归现在的端口管理,需要切换到7293端口去操作。这个时候,我们需要更换端口:redis-cli -p 7293
操作,才会返回OK。这样客户端需要连接两次。
Jedis等客户端会在本地维护一份slot-node的映射关系,所以大部分时候都不需要重定向。
如果新增或下线了主节点,数据应该怎么迁移(重新分配)?
因为key-slot的关系永远不会变,所以当新增了节点的时候,把原来的slot分配给新的节点负责,并且把相关的数据迁移过来。
redis-cli --cluster add-node 127.0.0.1:7291 127.0.0.1:7297 //添加一个新节点(新增一个7297) redis-cli --cluster reshard 127.0.0.1:7291 //新增的节点没有哈希槽,不能分布数据,在原来的任意一个节点执行,使得被分配的原节点重新分配slot //输入需要分配的哈希槽的数量,和哈希槽的来源节点(可以输入all或者id)
只有主节点可以写,如果一个主节点挂了,从节点怎么变成主节点?
当从节点发现自己的主节点变为FAIL状态时,便尝试进行Failover,以期成为新的主节点。由于挂掉的主节点可能会有多个从节点,从而存在多个从节点竞争成为主节点的过程:
currentEpoch: 可以当做记录集群状态变更的递增版本号.集群节点创建时,不管是 master 还是 slave,都置 currentEpoch 为 0。当前节点接收到来自其他节点的包时,如果发送者的 currentEpoch(消息头部会包含发送者的 currentEpoch)大于当前节点的currentEpoch,那么当前节点会更新 currentEpoch 为发送者的 currentEpoch。因此,集群中所有节点的 currentEpoch 最终会达成一致,相当于对集群状态的认知达成了一致 。
作用是: 当集群的状态发生改变,某个节点为了执行一些动作需要寻求其他节点的同意时,就会增加 currentEpoch 的值。当 slave A 发现其所属的 master 下线时,就会试图发起故障转移流程。首先就是增加 currentEpoch 的值,这个增加后的 currentEpoch 是所有集群节点中最大的。然后slave A 向所有节点发起拉票请求,请求其他 master 投票给自己,使自己能成为新的 master。其他节点收到包后,发现发送者的 currentEpoch 比自己的 currentEpoch 大,就会更新自己的 currentEpoch,并在尚未投票的情况下,投票给 slave A,表示同意使其成为新的 master。