一、分布式支持
1、性能
Redis本身的QPS已经很高了,但是如果在一些并发量非常高的情况下,性能还是会受到影响。这个时候我们希望有更多的Redis服务来分摊压力,实现负载均衡。
2、高可用
如果只有一个Redis服务,一旦服务宕机,那么所有的客户端都无法访问,会对业务造成很大的影响。另一个,如果硬件发生故障,而单机的数据无法恢复的话,带来的影响也是灾难性的。
3、可扩展
因为Redis所有的数据都放在内存中,如果数据量大,很容易受到硬件的限制。升级硬件收效和成本比太低,所以我们需要有一种横向扩展的方法。
高性能、高可用、扩展性需要依赖两种关键的技术,一种是分片,一种是冗余。分片的意思是把所有的数据拆分到多个节点分散存储。冗余的意思是每个节点都有一个或者多个副本。那么,Redis必须要提供数据分片和主从复制的功能。副本有不同的角色,如果主节点发生故障,则把某个从节点改成主节点,访问新的主节点,实现高可用。
二、主从复制
1、主从配置
一)配置文件方式
在slave节点里添加master节点的IP即可配置成功
replicaof 127.0.0.1 6379
slave节点启动后,会自动连接到master节点
如果master节点改变,比如master节点宕机,选举出新的master,这个配置会被重写。
二)启动服务方式
可以在服务启动时,通过参数直接指定master节点。
./redis-server --slaveof 127.0.0.1 6379
三)运行中方式
一个正在运行中的节点可以通过命令变为master节点的slave节点
slaveof 127.0.0.1 6379
四)级联复制
一个从节点也可以是其他节点的主节点,形成级联复制的关系。
参看集群状态
redis> info replication
从节点是只读的,不能执行写操作。
在master节点写入之后,slave节点会自动从master节点同步数据。
五)去除主从关系
如果想要解除主从关系,可以直接去掉配置文件中的replicaof语句后重启,或者直接执行命令
redis> slaveof no one
此时从节点会变为自己的主节点,结束复制数据。
2、主从复制原理
Redis的主从复制分为两种,第一种是全量复制,即一个节点首次连接到master节点后,需要复制全部数据。第二种是增量复制,即已经连接到master节点的slave节点忽然断线或宕机时,缺失部分数据,需要同步过来。
一)全量复制
1)连接阶段
- slave节点启动时(或执行slaveof命令时),会在自己本地保存master节点的信息,包括master节点的host和ip
- slave节点内部有个定时任务,replicationCron,每隔一秒检查是否有新的master node要连接和复制。如果发现有master节点,就和master节点建立连接,如果连接成功,slave节点就为链接分配一个专门处理复制工作的文件事件处理器负责后续的复制工作。为了让master节点感知到slave节点的存活,slave节点定时给主节点发送ping请求。
2)数据同步阶段
- 如果是新加入的节点,那就需要全量复制。master通过bgsave命令在本地生成一份RDB快照。
- RDB快照生成后,会发送给slave节点。如果slave本来就有数据,则会清除旧数据,然后再使用RDB快照进行加载。
- master节点生成RDB文件时,master节点会把所有新的写命令缓存在内存中。在slave节点保存了RDB之后,再将新的写命令赋值给slave节点。这里的处理思路和AOF日志在进行日志文件压缩时的处理思路是一样的,先缓存命令,新文件生成后进行命令的同步。
3)命令传播阶段
master节点持续把写命令异步复给slave节点
4)总结
master节点先用RDB快照文件将数据传递给slave节点,然后将写命令发送给slave节点。
但在一般情况下,Redis不需要做读写分离,因为其吞吐量已经足够大,做成集群分片之后并发问题更少,所以不需要考虑主从延迟问题,但主从延迟依旧是不可避免地,只能通过提升网络性能改善。
二)增量复制
如果slave节点有一段时间断开了与master节点的连接,如果不想清空数据重新记录,就需要记录上次复制的偏移量。
slave通过master_repl_offest记录复制偏移量,获取偏移量命令:
redis> info replication
三)无盘复制
Redis 6.0的新特性,主从复制的无盘复制。
为了降低master节点的磁盘开销,Redis支持无盘复制,master节点生成的RDB文件不再保存到磁盘,它将直接被发送到slave节点。无盘复制适用于主节点磁盘性能紧张但网络性能宽裕的境况。
3、主从复制问题
Redis主从复制解决了数据备份容灾和部分性能问题,但没有解决高可用问题,如果master节点宕机,对外服务就会断掉,如果每次都手动更换节点配置就过于麻烦。
三、可用性保证(Sentinel哨兵机制)
高可用的前提是既要能实现主从切换,也要能通知客户端发生了主从切换,在RocketMQ中,NameServer为它提供了主从的切换帮助,我们可以借用它的部分思路来参考。
创建一个监控器来监控全部的Redis节点状态,如果某一个节点超过一段时间内没有给监控器发送心跳报文,就标记该节点离线,如果该节点是master节点,就把某一个slave节点变成master节点,应用每次都从监控器中取得master节点地址。
在Redis中,它实现了一个类似机制来进行对应控制,Sentinel(哨兵)
1、Sentinel哨兵原理
通常而言,Sentinel服务会有奇数个(src/redis-sentinel),可以通过sentinel的脚本启动,也可以用redis-server的脚本加入sentinel参数启动
./redis-sentinel../sentinel.conf
或
./redis-sentinel../sentinel.conf --sentinel
Sentinel的本质是一个运行在特殊模式下的Redis,Sentinel通过info命令得到被监听Redis机器的master,slave等信息
为了保证监控服务器的可用性,我们会对Sentinel做集群部署,Sentinel既监控所有的Redis节点,也互相监控,它们没有主从之分。
Sentinel是特殊的Redis节点,它本身具有发布订阅功能,哨兵上线时会给所有的Redis节点的名字为_sentinel_:hello的channel发送消息,每个哨兵都订阅了所有Redis节点名字为_sentinel_:hello的channel,所以哨兵之间可以互相监控,互相感知。
2、Sentinel哨兵监控下线
一)主观下线
Sentinel默认以每秒钟1次的频率向Redis服务节点发送PING命令,如果在指定时间内没有收到有效回复,Sentinel就会将该节点标记为下线。
但是这个下线并非是真正的节点下线,所以称为主观下线。
控制参数为:sentinel down-after-milliseconds
默认为30秒
二)客观下线
当一个节点主观下线,并不一定真的下线了,可能是网络出现波动,导致没有收到消息,所以这时第一个发现该节点下线的Sentinel节点会继续询问其他的Sentinel节点,确认这个节点是否下线,如果多数Sentinel都认为该节点已经下线,那么该节点就被认定为真正下线(客观下线)
如果客观下线的是master节点,就需要重新选举master节点。
3、Sentinel哨兵故障转移
Redis的选举与故障转移都交给Sentinel来进行,因此选举的第一步,是在Sentinel中选举出一个leader,然后由leader完成故障转移流程,Sentinel通过Raft算法进行选举。
一)Sentinel哨兵的Leader选举
1)传统Raft算法
Raft算法是一种共识算法,其核心思想是:先到先得,少数服从多数。
- 分布式环境中的节点有三种状态:follower,candidate,leader
- 一开始所有的节点都是follower状态,如果follower连接不到leader,它就会成为candidate。candidate请求其他节点的投票,其他的节点会投给它。如果它得到了大多数节点的投票,他就成为了主节点,这个过程就叫Leader Election
- 现在所有的写操作需要在leader节点上发生,leader会记录操作日志,没有同步到其他follower节点的日志,状态为uncommitted。等到超过半数的follower同步了这条记录,日志状态就会变成committed。leader会透支所有的follower日志已经committed,这个时候所有的节点就达成了一致。这个过程叫Log Replication
- 在Raft协议里,选举有两个超时时间。第一个叫election timeout,目的是为了放置同一时间大量节点参与选举,没一个节点在变成candidate之前需要随机等待一段时间,时间范围是150ms至300ms。第一个变成candidate的节点会先发起投票,它第一票投给自己,然后请求其他节点投票
- 如果还没有收到投票结果,又到了超时时间,需要重置超时时间。只要有大部分节点投给了一个节点,它就会成为leader。
- 成为leader之后,它会发消息让其他节点来同步数据,发消息的间隔是由heartbeat timeout来控制的。follower会回复同步数据的消息。
- 只要followers收到了同步数据的消息,代表leader没有挂掉,它们会清除heartbeat timeout的计时。
- 一旦followers在heartbeat timeout时间之内没有收到Append Rnteries消息,它就会认为leader挂了,开始让其他节点投票,成为新的leader。
- 必须有超过半数以上的节点投票,保证只有一个leader被选出来。
- 如果两个follower同时变成了candidate,就会出现分割投票,比如有两个节点同时变成candidate,而且各自有一个投票请求先达到了其他节点 ,加上它们给自己的投票,每个candidate手上有两票,但是,因为它们的election timeout不同,再发起新的一轮选举的时候,有一个节点获得了更多的投票,它被选举成leader。
2)Sentinel的Raft算法
Sentinel的选举和传统Raft算法略有不同
- master客观下线触发选举,而不是过了election timeout时间开始选举
- Leader不会自己成为Leader的消息发送给其他Sentinel,其他Sentinel等待Leader从Slave选举出Master后,检测到新的Master正常工作,就会去掉客观下线的标识,不去进去故障转移流程。
二)Master节点的故障转移
Redis的master节点选举规则有四条,分别是断开连接时长,优先级排序,复制数量和进程ID
- 如果与哨兵连接断开时间太久,超过了某个阈值,就直接失去选举权
- 如果拥有选举权,就会比较优先级,这个在配置文件中可以进行设置(replica-priority 100),数值越小优先级越高,默认为100
- 如果优先级相同,就看谁从master中复制的数据做多(复制偏移量最大),选最多的那个
- 如果复制偏移量相同,就选进程ID最小的那个。
三)Follower节点的生成
Redis选举出新的Master节点后,其他的节点会成为它的Follower节点,步骤分为两步。
- 选出Sentinel Leader之后,有Sectinel Leader向某个节点发送slaveof no one命令,让它成为独立节点
- 向其他节点发送slaveof x.x.x.x xxxx(Maser节点的ip与端口号),让其他节点成为Master节点的从节点,故障转移完成
3、Sentinel哨兵的功能总结
- 监控:Sentinel会不断检查主服务器和从服务器是否正常运行
- 通知:如果某一个被监控的节点出现问题,Sentinel会通过API发送通知
- 自动故障转移:如果主服务器发生故障,Sentinel可以启动故障转移过程,把某台从服务器升级为主服务器,并发送通知
- 配置管理:客户端连接到Sentinel,获取当前的Redis主服务器地址。
4、Sentinel哨兵的不足
- 主从切换的时候会丢失部分数据,因为只有一个Master节点
- 只能单点写入,没有水平扩容
- 如果数据量非常大,这个时候就要进行Redis数据分片,需要多个master-slave的group,把数据分布到不同的group中。
四、分布式方案
关于Redis数据分片,有三种可能方案
- 客户端实现相关逻辑,例如用取模或者一致性哈希对key进行分片,查询和修改都先判断key的路由
- 把分片处理的逻辑抽取出来,运行一个独立的代理服务,客户端连接到这个代理服务,代理服务做请求的转发。
- 服务端实现
1、客户端实现
在Jedis客户端中,支持分片功能,它是Spring Boot 2.x版本之前默认的Redis客户端,RedisTemplate就是对Jedis的封装
一)ShardedJedis
Jedis有几种连接池,其中一种支持分片。
例如:
Public class ShardedTest {
public static void mian (String[] args){
JedisPoolConfig poolConfig = new JedisPoolConfig();
//Redis服务器
JedisShardInfo shardInfo1 = new JedisShardInfo("127.0.0.1",6379);
JedisShardInfo shardInfo2 = new JedisShardInfo("127.0.0.2",6379);
//连接池
List<JedisShardInfo> infoList = Arrays.asList(shardInfo1,shardInfo2);
ShardedJedisPool jedisPool = new ShardedJedisPool(poolConfig,infoList);
ShardedJedis jedis = null;
try {
jedis = jedisPool.getResource();
for(int i=0;i<100;i++)[
jedis.set("Key"+i,""+i);
}
for(int i=0;i<100;i++)[
jedis.get("Key"+i);
}
}finally{
if(jedis != null){
jedis.close();
}
}
}
}
执行后,会发现两个Redis的key数量是非常接近的。
如果希望是数据分布相对均匀的话,首先可以考虑哈希后取模(因为Key不一定是整数,所以需要先计算哈希)
1、哈希后取模
例如,hash(key)%N,根据余数,,决定映射到哪一个节点。这种方式比较简单,属于静态的分片规则。但是一旦节点数量发生变化,由于N的数值发生变化,数据需要重新分布。
为了解决这个问题,可以采用一致性哈希算法
2、一致性哈希
把所有的哈希值空间组成一个虚拟圆环(哈希环),整个空间按顺时针方向组织,因为是环形空间,0和2^32-1是重叠的。
假设有四台机器需要哈希环来实现映射(分布数据),现根据机器命名或IP地址计算哈希值,然后分布到哈希环中。
现在有4条数据或者4个访问请求,针对key计算后,得到哈希环中的位置,沿着哈希环顺时针找到的第一个Node,就是数据存储的节点。
在这种情况下,新增加一个节点,只影响一部分数据的分布,减少一个节点,只影响相邻的下一个节点。
一致性哈希解决了动态增删节点时,所有数据都需要重新分布的问题,它只会影响到下一个相邻的节点,对其它节点没影响。
但是这样的一致性哈希算法有一个缺点,因为节点不一定是均匀地分布的,特别是在节点数比较小的情况下,所以数据不能得到均匀分布。解决这个问题的办法是引入虚拟节点(Virtual Node),一个实际节点设置数个虚拟节点后,按照原来的方式继续顺时针存储,虚拟节点存储的数据会存储在实际节点上,节点数提高后,数据分布会更加均匀。
一致性哈希在分布式系统,负载均衡,分库分表等场景中都有体现,是基础算法。
3、红黑树
在Jedis中,通过红黑树结构实现一致性哈希算法。
private void initialize(List<S> shards) {
//创建一个红黑树
nodes = newTreeMap<Long, S>();
//把所有的Redis节点放到红黑树中
for(int i=0;i!=shards.size();++i){
final S shardInfo = shards.get(i);
//为每一个Redis节点创建160个虚拟节点,放到红黑树中
if(shardInfo.getName()==null)for(int n=0;n<160*shardInfo.getWeight();n++){
nodes.put(this.algo.hash("SHARD-"+i+"-NODE-"+n),shardInfo);
}
else for(int n=0;n<160*shardInfo.getWeight();n++){
//对名字计算哈希(MurmurHash),名称格式SHARD-0-NODE-0
nodes.put(this.algo.hash(shardInfo.getName+"*"+shardInfo.getWeight()+n),shardInfo);
}
//添加到map中,键为ShardInfo,置为redis实例
resources.put(shardInfo,shardInfo.createResource());
}
}
当存取键值对时,计算键的哈希值,然后从红黑树上摘下比该值大的第一个节点上的JedisShardInfo,此即为需要存储的虚拟节点,随后从Resources取出Jedis。
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());
}
4、总结
使用ShardedJedis之类的客户端分片代码的优势是配质简单,不依赖于其他中间件,分区的逻辑可以自定义,比较灵活,。但是基于客户端的方案,不能实现动态的服务增减,每个客户端需要自行维护分片策略,存在重复代码。
2、Redis Cluster
Redis Cluster是在Redis 3.0版本推出的,用来解决分布式需求,同时可以实现高可用,采用去中心化架构,客户端可以连接到任意一个可用节点。
数据分片有三个关键问题需要解决:
- 数据怎么相对均匀地分片
- 客户端怎么访问到相应的节点和数据
- 重新分片的过程怎么保证正常服务
一)架构
Redis Cluster可以看成是由多个Redis实例组成的数据集合,客户端不需要关注数据的子集到底存储在哪个节点,只需要关注这个集合整体。
二)数据分布
1、虚拟插槽
Redis既没有用哈希取模,也没有用一致性哈希,而是用虚拟槽实现的。
Redis创建了16384个槽(slot),每个节点负责一定区间的slot。比如Node1负责0-3000,Node2负责3001-6000,Node3负责6001-9000……以此类推。
对象分布到Redis节点上时,对Key用CRC16算法再取模16384,得到一个slot的值,数据落到负责这个slot的Redis的节点上。
Redis的每个master节点都会维护自己负责的slot,用一个bit的序列实现,比如:序列的第0位是1,就代表第一个slot是它负责,序列的第1位是0,就代表第二个slot不归它负责。
查看key属于哪个slot的命令:
redis> cluster keyslot key
key和slot的关系是永远不变的,会变的只有slot和Redis节点的关系。
2、同节点计算
有些操作是不能跨越节点的,比如同一用户的基本信息和其他信息,最好是放置在同一个节点上。当有这种需要时,只需要在key的值里加入{hash tag}即可。Redis在计算槽编号的时候只会获取{}大括号之间的字符串进行槽编号计算,这样就算上面两种信息key不同,但只要大括号内的字符串相同,就可以计算出相同的槽,最后存储到同一节点。
3、客户端重定向
当客户端请求一个数据时,有可能请求的数据并不在当前节点,这时服务端就会返回MOVED,这个结果是根据key计算出来的slot不归当前请求节点,服务端返回MOVED告诉客户端应该去访问哪一个节点来获取数据,此时只有更换节点,才能获取到想要的数据。
这样的话。就需要连接两次,因此Jedis等客户端就会在本地维护一份slot——node的映射关系,大部分时候不需要重定向,所以叫smart jedis(需要客户端支持)
4、数据迁移
因为key和slot的关系永远不变,当新增了节点的时候,需要吧原有的slot分配给新的节点负责,并把相关数据迁移过来。
新增节点(新节点127.0.0.1:7124):
redis-cli --cluster add-node 127.0.0.1:7123 127.0.0.1:7124
新增的节点没有哈希槽,不能分布数据,因此在原来的任意一个节点上执行重分配命令:
redis-cli --cluster reshard 127.0.0.1:7123
Redis会自动进行数据迁移,将槽分配给新的节点。
当然也可以通过手动输入需要分配的哈希槽的数量和哈希槽的来源节点(all或者指定id)
一些槽命令:
- cluster addslots [slot……]:将一个或多个槽指派给当前节点
- cluster delslots [slot……]:移除一个或多个槽对当前节点的指派
- cluster flushslots :移除指派给当前节点的所有槽,让当前节点变成没有指派任何槽的节点。
- cluster setslot node <node_id> :将槽solt指派给node_id指定的节点,,如果槽已经指派给另一个节点,那么就让另一个节点删除该槽,再进行指派。
- cluster setslot migrating <node_id> :将本节点的槽slot迁移到node_id指定的节点中
- cluster setslot importing <node_id> :从node_id指定的节点中导入槽到本节点
- cluster setslot stable :取消对槽的导入(importing),或迁移(migrating)
5、高可用和主从切换原理
当slave发现自己的master变为Fail状态后,便会尝试进行Failover,希望成为新的master。由于宕机的master可能会有多个slave。从而引发选举,其过程如下:
- slave发觉自己的master变为Fail状态
- 将自己记录的集群currentEpoch加1,并广播FAILOVER_AUTH_REQUEST信息
- 其他节点收到该消息,只有master响应,判断请求者合法性,并发送FAILOVER_AUTH_ACK,对每一个epoch只发送一次ACK
- 尝试failover的slave收集FAILOVER_AUTH_ACK
- 超过半数可成为master
- 广播pong通知其他集群节点
Redis Cluster 技能实现主从的角色分配,也能实现主从切换,相当于集成Replication和Sentinel的功能。
三)总结
Redis Cluster的特点:
- 无中心结构
- 数据按照slot存储分布在多个节点,节点间数据共享,可动态调整数据分布。
- 可扩展性,线性扩展可到1000节点,节点可动态添加或删除,
- 高可用性,部分节点不可用时,集群仍可用,通过增加slave做standby数据副本,能够实现故障自动转移,节点之间通过gossip协议交换消息,用投票机制完成slave到master的角色转移。
- 降低运维成本,提高系统的可扩展性和可用性。