![](https://i-blog.csdnimg.cn/blog_migrate/f09a47ea930779a750d370789038095a.jpeg)
1. 为什么 Redis 需要支持分布式?
1.1 为什么 Redis 需要支持分布式?
1.1.1 性能
Redis 本身的 QPS 已经很高了,但是如果在一些并发量非常高的情况下,性能还是会收到影响。这个时候我们希望由更多的 Redis 服务来分摊压力,实现负载均衡。
1.1.2 高可用
第二个是可用性和安全的问题。如果只有一个 Redis 服务,一旦服务宕机,那么所有的客户端都无法访问,会对业务造成很大的影响。另一个,如果硬件发生故障,而单机的数据无法恢复的话,带来的影响也是灾难性的。
1.1.3 可扩展
第三个是处于存储的考虑。因为 Redis 所有的数据都放在内存中,如果数据量大,很容易收到硬件的限制。升级硬件(scale up)收效和成本比太低,所以我们需要有一种横向扩展的方法。
高性能、高可用、扩展性需要依赖两种关键的计数,一种是分片,一种是冗余。分片的意思是把所有的数据拆分到多个节点分散存储。冗余的意思是每个节点都有一个或者多个副本。那么,Redis 必须要提供数据分片和主从复制的功能。副本有不同的角色,如果主节点发生故障,则把某个从节点改成主节点,访问新的主节点,实现高可用。
下面我们讨论的内容就是围绕着三点,第一个是主从复制怎么实现。第二个是主从自动切换怎么实现。第三个是数据分片怎么实现。
2. 主从复制(replication)
跟 Kafka、RocketMQ、MySQL、Zookeeper 一样,Redis 支持集群的架构,集群的节点有主节点和从节点之分。主节点叫 master,从节点叫 slave。slave 会通过复制的计数,自动同步 master 的数据。
2.1 主从复制配置
Redis 的主从复制配置非常地简单,只需要在配置文件里面添加一行配置就可以了。
例如一主多从,186 是主节点,在每个 slave 节点的 redis.conf 配置文件增加一行:
replicaof 192.168.44.186 6379
从节点启动之后,就会自动连接到 master 节点,开始同步数据。
如果 master 节点变了,比如原来的 mater 宕机,选举出了新的 mater,这个配置会被重写。
还有一种方式,就是在启动服务时通过参数直接指定 master 节点:
./redis-server --slaveof 192.168.44.186 6379
一个正在运行中的节点,可以变成其他节点的从节点吗?
可以,这就是第三种方式,在客户端直接执行 slaveof IP port,使该 Redis 实例成为从节点。
127.0.0.1:6379> slaveof 192.168.44.186 6379
一个从节点也可以是其他节点的主节点,形成级联复制的关系。
查看集群状态:
127.0.0.1:6379> info replication
从节点使制度的,不能执行写操作。执行写命令会报错:
(error)READONLY You can’t write against a read only replica.
在主节点写入后,slave 会自动从 master 同步数据。
有一天小弟觉得跟着这个大哥没前途,想单飞,自己做大哥,怎么办?
把配置文件里面的 replica of 去掉重启,或者直接断开复制:
127.0.0.1:6379> slaveof no one
此时从节点会变成自己的主节点,不再复制数据。
下面我们来研究一下主从复制到底怎么实现的。MySQL 的主从复制原理我们是清楚的,依赖 binlog 文件,然后还有几个线程。Redis 是怎么实现的呢?
2.2 主从复制原理
Redis 的主从复制分为两类,一种叫全量复制,就是一个节点第一次连接到 master 节点,需要全部的数据。第二种叫做增量复制,比如以前已经连接到 master 节点,但是中间网络断开,或者 slave 节点宕机了,确立一部分的数据。
2.2.1 连接阶段
1.slave 节点启动时(或者执行 slaveof 命令时),会在自己本地保存 master 节点的信息,包括 master node 的 host 和 ip。
2.slave 节点内部有个定时任务 replicationCron,每隔 1 秒钟检查是否有新的 master node 要连接和复制。源码 replication.c 3132 行。
如果发现有 master 节点,就跟 master 节点建立连接,如果连接成功,从节点就为连接建立一个专门处理复制工作的文件事件处理器负责后续的复制工作。为了让主节点感知到 slave 节点的存货,slave 节点定时会给主节点发送 ping 请求。
建立连接之后,就可以同步数据了,这里也分成两个阶段。
2.2.2 数据同步阶段
如果是新加入的 slave 节点,那就需要全量复制。master 通过 bgsave 命令在本地生成一份 RDB 快照,将 RDB 快照文件发送给 slave 节点(如果超时会重连,可以调大 repl-timeout 的值)。
问题:如果 slave 节点自己本来有数据怎么办?
slave 节点首先需要清除自己的旧数据,然后用 RDB 文件加载数据。
问题:master 节点生成 RDB 期间,收到的命令怎么处理?
开始生成 RDB 文件时,master 会把所有新的写命令缓存在内存中。在 slave 节点保存了 RDB 之后,再将新的写命令复制给 slave 节点。(跟 AOF 重写 rewrite 期间连接收到的命令的处理思路是一样的)
第一次全量同步完了,主从已经保持一致了,后面就是持续把接收到的命令发送给 slave 节点。
2.2.3 命令传播阶段
4.master node 持续把写命令,异步赋值给 slave node。
总结起来非常的简单,前面用 RDB 文件,后面把命令发送给 slave 节点,就实现了主从复制。
注意:一般情况下我们不会用 Redis 做读写分离,因为 Redis 的吞吐量已经够高了,做集群分片之后并发的问题更少,所以不需要考虑主从延迟的问题。
跟 MySQL 一样,主从之间复制延迟是不可避免的,只能通过优化网络来改善。
第二种情况就是增量复制了。
如果从节点有一段时间断开了与主节点的连接,是不是把原来的数据全部清空,重新再全脸复制一遍?这样效率太低了吧。
如果可以增量复制,怎么知道上次复制到哪里?
slave 通过 master_repl_offset 记录的偏移量:
127.0.0.1:6379> info replication
第一节课我们讲到了 6.0 的一个新特性,主从复制的无盘复制(从 2.8.18 版本开始支持无盘复制)。
repl-diskless-sync=no
为了降低主节点磁盘开销,Redis 支持无盘复制,master 生成的 RDB 文件不保存到磁盘而是直接通过网络发送给从节点。无盘复制适用于主节点所在机器磁盘性能较差,但网络带宽比较充裕的场景。
2.3 主从复制的不足
Redis 主从复制解决了数据备份和一部分性能的问题,但是没有解决高可用的问题。
在一主一从或者一主多从的情况下,如果主服务器挂了,对外提供的服务就不可用了,单点问题没有得到解决。跟 MySQL 一样。
如果每次都是手动把之前的服务器切换成主服务器,然后再把剩余节点设置为它的从节点,这个比较费时费力,还会造成一定时间的服务不可用。
3. 可用性保证之 Sentinel
3.1 Sentinel 原理
怎么实现高可用呢?第一个对于服务端来说,能够实现主从自动切换;第二个,对于客户端来说,如果发生了主从切换,它需要获取最新的 master 节点。
这个怎么实现呢?这里面应该要有一个管理 Redis 节点存货状态的角色,而且具备路由功能。比如 RocketMQ 是怎么实现的?(NameServer)
我们的思路:
创建一台监控服务器来监控所有 Redis 服务节点的状态,比如,master 节点超过一定时间没有给监控服务器发送心跳保温,就把 master 标记为下线,然后把某一个 slave 变成 master。应用每一次都是这个监控服务器拿到 master 的地址。
问题是:如果监控服务器本身出问题了怎么办?那我们就拿不到 master 的地址了。应用也没有办法访问。
那我们再创建一个监控服务器,来监控监控服务器…似乎陷入死循环了,这个问题怎么解决?这个问题先放着。
Redis 的高可用时通过哨兵 Sentinel 来保证的。它的思路就是通过运行监控服务器来保证服务的可用性。
从 Redis 2.8 版本起,提供了一个稳定版本的 Sentinel(哨兵),用来解决高可用的问题。
我们会启动奇数个的 Sentinel 的服务(通过 src/redis-sentiel)。
可以用 sentinel 的脚本启动,也可以用 redis-server 的脚本加 sentinel 参数启动:
$ ./redis-sentinel ../sentinel.conf
或者:
$ ./redis-server ../sentinel.conf --sentinel
它本质上只是一个运行在特殊模式下的Redis。Sentinel 通过 info 命令得到被监听 Redis 机器的 master,slave 等信息。
为了保证监控服务器的可用性,我们会对 Sentinel 做集群的部署。Sentinel 即监控所有的 Redis 服务,Sentinel 之间也相互监控。
注意:Sentinel 本身没有主从之分,地位是平等的,只有 Redis 服务节点有主从之分。
这里就有个问题了,Sentinel 唯一的联系,就是他们监控相同的 master,那一个 Sentinel 节点是怎么知道其他的 Sentinel 节点存在的呢?
因为 Sentinel 是一个特殊状态的 Redis 节点,它也有发布订阅的功能。
哨兵上线时,给所有的 Redis 节点(master/slave)的名字为 _sentinel_:hello 的 channel 发送消息。
每个哨兵都订阅了所有 Redis 节点名字为 _sentinel_:hello 的 channel,所以能互相感知对方的存在,从而进行监控。
Sentinel 和 Sentinel 集群是怎么运行的我们知道了,下面来看看它是怎么干活的。 它最大的作用就是管理 Redis 节点服务状态,还有切换主从。
3.1.1 服务下线
第一个问题,Sentinel 是怎么知道 master 节点挂了。
Sentinel 默认以每秒钟 1 次的频率向 Redis 服务节点发送 PING 命令。如果在指定时间内没有收到有效回复,Sentinel 会将该服务器标记为下线(主观下线)。
由这个参数控制:
# sentinel.conf
sentinel down-after-milliseconds <master-name> <milliseconds>
默认是 30 秒。
但是,只有你发现 master 下线,并不代表 master 真的下线了。也有可能是你自己的网络出问题了。所以,这个时候第一个发现 master 下线的 Sentinel 节点会继续询问其他的 Sentinel 节点,确认这个节点是否下线,如果多数 Sentinel 节点都认为 master下线,master 才真正认为被下线(客观下线)。
确定 master 下线之后,就需要重新选举 master。
好了,问题又来了,选举这件事情谁来做呢?Redis 节点自己选举?还是谁?
这里我们回想一下,Kafka 副本的选举是怎么做的?RocketMQ 副本的选举是怎么做的?
- Kafka 会有在 Broker 里面选一个 Controller 出来。
- RocketMQ 用 Dledger 技术选举(基于 Raft 协议)。
3.1.2 故障转移
Redis 的选举和故障转移都是由 Sentinel 完成的。问题又来了,既然有这么多的 Sentinel 节点,由谁来做故障转移的事情呢?
故障转移流程的第一步就是在 Sentinel 集群选择一个 Leader,由 Leader 完成故障转移流程。Sentinel 通过 Raft 算法,实现 Sentinel 选举。
Raft 算法
我们前面说过,只要有了多个副本,就必然要面对副本一致性的问题。如果要所有的节点达成一致,必然要通过复制的方式实现。但是这么多节点,以哪个节点的数据为准呢?所以必须选出一个 Leader。
所以数据保持一致需要两个步骤:领导选举,数据复制。数据复制我们前面讲过了。这里关注一下选举的实现。
Raft 是一个共识算法(consensus algorithm)。比如比特币之类的加密货币,就需要共识算法。Spring Cloud 的注册中心解决方案 Consul 也用到了 Raft 协议。
Raft 的核心思想:先到先得,少数服从多数。
先说一下,Sentinel 的 Raft 实现跟原生的算法是有所去别的,但是大体思想一致。
Raft 算法演示:http://thesecretlivesofdata.com/raft/
文字描述:
1.分布式环境中的节点有三个状态:Follower、Candidate(虚线外框)、Leader(实线外框)。
2.一开始所有的节点都是 Follower 状态。如果 Follower 连接不到 Leader(Leader 挂了),它就会成为 Candidate。Candidate 请求其他节点的投票,其他的节点会投给它。如果它得到了大多数节点的投票,它就成为了主节点。这个过程就叫做 Leader Election。
3.现在所有的写操作需要在 Leader 节点上发生。Leader 会记录操作日志。没有同步到其他 Follower 节点的日志,状态时 uncommitted。等到超过半数的 Follower 同步了这条记录,日志状态就会变成 committed。Leader 会通知所有的 Follower 日志已经 committed,这个时候所有的节点就达成了一致。这个过程叫 Log Replication。
4.在 Raft 协议里面,选举的时候有两个超时时间。第一个叫 election timeout。也就是说,为了防止同一时间大量节点参与选举,每个节点在变成 Candidate 之前需要随机等待一段时间,时间范围是 150ms 和 300ms 之间。第一个变成 Candidate 的节点会先发起投票。它会先投给自己,然后请求其他节点投票(Request Vote)。
5.如果还没有收到投票结果,又到了超时时间,需要充值超时时间。只要有大部分节点投给了一个节点,它就会变成 Leader。
6.成为 Leader 之后,它会发消息让来同步数据(Append Entries),发消息的间隔是由 heartbeat timeout 来控制的。Followers 会回复同步数据的消息。
7.只要 Foloowers 收到了同步数据的消息,代表 Leader 没挂,他们就会清除 heartbeat timeout 的计时。
9.但是一旦 Followers 在 heartbeat timeout 时间之内没有收到 Append Entries 消息,它就会认为 Leader 挂了,开始让其他节点投票,成为新的 Leader。
10.如果两个 Follower 同时变成了 Candidate,就会出现分割投票。比如有两个节点同时变成 Candidate,而且各自有一个投票请求先达到了其他的节点。加上他们给自己的投票,每个 Candidate 手上有 2 票。但是,因为他们的 election timeout 不同,在发起新的一轮选举的时候,有一个节点收到了更多的投票,所以它变成了 Leader。
总结:
Sentinel 的 Raft 算法和 Raft 论文略有不同:
1.master 客观下线触发选举,而不是过了 election timeout 时间开始选举。
2.Leader 并不会把自己成为 Leader 的消息发给其他 Sentinel。其他 Sentinel 等待 Leader 从 slave 选出 master 后,检测到新的 master 正常工作后,就会去掉客观下线的标识,从而不需要进入故障转移流程。
OK,到了这一步,我们还只是从所有的 Sentinel 节点里面选出来了一个 Leader 而已,也就是所谓选举委员会主席。下面才是真正的选举。
故障转移
Redis 的 master 节点的选举规则又是什么样的呢?
对于所有的 slave 节点,一共有四个因素影响选举的结果,分别是断开连接时长、优先级排序、复制数量、进程 id。
1.如果与哨兵连接断开的比较久,超过了某个阈值,就直接失去了选举权。
2.如果拥有选举权,那就看谁的优先级高,这个在配置文件里可以设置(replica-priority 100),数值越小优先级越高。
3.如果优先级相同,就看谁从 master 中复制的数据最多(复制偏移量最大),选最多的哪个。
4.如果复制数量也相同,就选择进程 id 最小的那个。
master 节点确定之后,又怎么让其他的节点变成它的从节点呢?
我们前面学了两个命令可以用起来:
1.选出 Sentinel Leader 之后,由 Sentinel Leader 向某个节点发送 slaveof no one 命令,让它成为独立节点。
2.然后向其他节点发送 slaveof x.x.x.x xxxx(本机 IP 端口),让它们成为这个节点的从节点,故障转移完成。
3.2 Sentinel 的功能总结
监控:Sentinel 会不断检查主服务器和从服务器是否正常运行。
通知:如果某一个被监控的实例出现问题,Sentinel 可以通过 API 发出通知。
自动故障转移(failover):如果主服务器发生故障,Sentinel 可以启动故障转移过程。把某台服务器升级为主服务器,并发出通知。
配置管理:客户端连接到 Sentinel,获取当前的 Redis 主服务器的地址。
3.3 Sentinel 实战
3.3.1 Sentinel 配置
为了保证 Sentinel 的高可用,Sentinel 也需要做集群部署,集群中至少需要三个 Sentinel 实例(推荐奇数个,防止脑裂)。
以 Redis 安装路径 /usr/local/soft/redis-6.0.9 为例。
在 204 和 205 的 src/redis.conf 配置文件中添加:
replicaof 192.168.44.186 6379
在 203、204、205 创建 sentinel 配置文件(安装后根目录下默认有 sentinel.conf)
$ cd /usr/local/soft/redis-6.0.9
$ mkdir logs
$ mkdir rdbs
$ mkdir sentinel-tmp
$ vim sentinel.conf
三台服务器内容相同:
daemonize yes
port 26379
protected-mode no
dir "/usr/local/soft/redis-6.0.9/sentinel-tmp"
sentinel monitor redis-master 192.168.44.186 6379 2
sentinel down-after-milliseconds redis-master 30000
sentinel failover-timeout redis-master 180000
sentinel parallel-syncs redis-master 1
上面出现了 4 个 ‘redis-master’,这个名称要统一,并且使用客户端(比如 Jedis)连接的时候要是用这个名字。
3.3.2 Sentinel 验证
启动 Redis 服务和 Sentinel:
$ cd /usr/local/soft/redis-6.0.9/src
# 启动 Redis 节点
$ ./redis-server ../redis.conf
# 启动 Sentinel 节点
$ ./redis-sentinel ../sentinel.conf
# 或者
$ ./redis-server ../sentinel.conf --sentinel
查看集群状态:
127.0.0.1:6379> info replication
模拟 master 宕机,在主节点执行:
127.0.0.1:6379> shutdown
某个节点会被选为新的 master,只有一个 slave 节点。
注意看 sentinel.conf 里面的 redis-master 被修改了!
模拟原 master 恢复,启动 redis-server。master 又有两个 slave 了。
slave 宕机和恢复省略。
3.3.3 Sentinel 连接使用
Jedis 连接 Sentinel(gupao-jedis: JedisSentinelTest.java)
master name 来自于 sentinel.conf 的配置
private static JedisSentinelPool createJedisPool() {
String masterName = "reds-master";
Set<String> sentinels = new HashSet<String>();
sentinels.add("192.168.44.186:26379");
sentinels.add("192.168.44.187:26379");
sentinels.add("192.168.44.188:26379");
pool = new JedisSentinelPool(masterName, sentinels);
return pool;
}
Spring Boot 连接 Sentinel(springboot-redis: RedisAppTest.java)
spring.redis.sentinel.master=redis-master
spring.redis.sentinel.nodes=192.168.44.186:26379,192.168.44.187:26379,192.168.44.188:26379
无论是 Jedis 还是 Spring Boot(2.x 版本默认是 Lettuce),都只需要配置全部哨兵的地址,由哨兵返回当前的 master 节点地址。
3.4 哨兵机制的不足
主从切换过程中会丢失数据,因为只有一个 master。
只能单点写,没有解决水平扩容的问题。
如果数据量非常大,这个时候就要对 Redis 的数据进行分片了。
这个时候我们需要多个 master-slave 的 group,把数据分布到不同的 group 中。
问题来了,数据怎么分片?分片之后,怎么实现路由?
4. Redis 分布式方案
数据源的动态选择?分库分表的分片策略?问题是这段代码逻辑在哪里实现?
如果要实现 Redis 数据的分片,我们有三种方案:
第一种是在客户端实现相关的逻辑,例如用取模或者一致性哈希对 key 进行分片,查询和修改都先判断 key 的路由。
第二种是把做分批那处理的逻辑抽取出来,运行一个独立的代理服务,客户端连接到这个代理服务,代理服务做请求的转发。
第三种就是基于服务端的实现。
4.1 客户端 Sharding
在我们用的非常多的 Jedis 客户端中,支持分片功能。它是 Spring Boot 2.x 版本之前默认的 Redis 客户端,RedisTemplate 就是对 Jedis 的封装。
4.1.1 ShardedJedis
Jedis 有几种连接池,其中有一种支持分片。
比如这里一个连接到 186、一个连接到 Windows 的Redis 服务。
public class ShardingTest {
public static void main(String[] args) {
JedisPoolConfig poolConfig = new JedisPoolConfig();
// Redis 服务器
JedisStardInfo shardInfo1 = new JedisShardInfo("127.0.0.1", 6379);
JedisShardInfo shardInfo2 = new JedisShardInfo("192.168.44.186", 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("k" + i, "" + i);
}
for (int i = 0; i < 100; i++) {
System.out.println(jedis.get("k" + i));
}
} finally {
if (jedis != null) {
jedis.close();
}
}
}
}
通过 dbsize 命令发现,一台服务器有 44 个 key,一台服务器有 56 个 key。
ShardedJedis 是怎么做到的呢?
如果是希望数据分布相对均匀的话,我们首先可以考虑哈希后取模(因为 key 不一定是整数,所以先计算哈希)。
哈希后取模
例如,hash(key)%N,根据余数,决定映射到哪一个节点。这张方式比较简单,属于静态的分片规则。但是一旦节点数量发生变化(增加或者减少),由于取模的 N 发生变化,数据需要重新分布。
为了解决这个问题,我们又有了一致性哈希算法。ShardedJedis 实际上用的就是一致性哈希算法。
一致性哈希
一致性哈希的原理:
把所有的哈希值空间组织成一个虚拟的环形(哈希环),整个空间按顺时针方向组织。因为是环形空间,0 和 2^32-1 是重叠的。
假设我们有四台机器要哈希环来实现映射(分布数据),我们先根据机器的名称或者 IP 计算哈希值,然后分不到哈希环中(红色圆圈)。
现在有 4 条数据或者 4 个访问请求,对 key 计算后,得到哈希环中的位置(绿色圆圈)。沿哈希环顺时针找到的第一个 Node,就是数据存储的节点。
在这种情况下,新增了一个 Node5 节点,只影响一部分数据的分布。
删除了一个节点 Node4,只影响相邻的一个节点。
一致性哈希解决了动态增减节点时,所有数据都需要重新分布的问题,它只会影响到下一个相邻的节点,对其他节点没有影响。
但是这样的一致性哈希算法有一个缺点,因为节点不一定是均匀分布的,特别是在节点数比较少的情况下,所以数据不能得到均匀分布。解决这个问题的办法是引入虚拟节点(Virtual Node)。
比如:2 个节点,5 条数据,只有 1 条分布到 Node2,4 条分布到 Node1,不均匀。
Node1 设置了两个虚拟节点,Node2 也设置了两个虚拟节点(虚线圆圈)。
这时候有 3 条数据分布到 Node1,1 条数据分布到 Node2。
一致性哈希在分布式系统中,负载均衡、分库分表等场景中都有应用,跟 LRU 一样,是一个很基础的算法。
那这样一个一致性哈希算法,到底怎么实现呢?哈希环是一个什么数据结构?虚拟节点又怎么实现?
redis.clients.util.Sharded.initialize(),jedis 实例被放到了一棵红黑树 TreeMap 中。
private void initialize(List<S> shards) {
// 创建一个红黑树
nodes = new TreeMap<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());
}
使用 ShardedJedis 之类的客户端分片代码的优势是配置简单,不依赖于其他中间件,分区的逻辑可以自定义,比较灵活,但是基于客户端的方案,不能实现动态的服务增减,每个客户端需要自行维护分片策略,存在重复代码。
第二种思路就是把分片的代码抽取出来,做成一个公共服务,所有的客户端都连接到这个代理层。由代理层来实现请求和转发。
4.2 代理 Proxy
典型的代理分区方案有 Twitter 开源的 Twemproxy 和国内的豌豆荚开源的 Codis。
4.2.1 Twemproxy
Twemproxy 的有点:比较稳定,可用性高。
不足:
1.出现故障不能自动转移,结构复杂,需要借助其他组件(LVS/HAProxy + Keepalived)实现 HA。
2.扩缩容需要修改配置,不能实现平滑地扩缩容(需要重新分布数据)。
4.2.2 Codis
Codis 是一个代理中间件,豌豆荚公司用 Go 语言开发的(快三年没有更新了)。跟数据库分库分表中间件的 Mycat 的工作层次是一样的。
功能:客户端连接 Codis 跟连接 Redis 没有区别。
分片原理:Codis 把所有的 key 分成了 N 个槽(例如1024),每个槽对应一个分组,一个分组对应于一个或者一组 Redis 的实例。Codis 对 key 进行 CRC32 运算,得到一个 32 位的数字,然后模以 N (槽的个数),得到余数,这个就是 key 对应的槽,槽后面就是 Redis 的实例(跟 Mycat 的 先哈希后范围的算法思想类似)。例如 4 个槽:
Codis 的曹位映射关系是保存在 Proxy 中的,如果要解决单点的问题,Codis 也要做集群部署,多个 Codis 节点怎么同步槽和实例的关系呢?需要运行一个 Zookeeper(或者 etcd/本地文件)。
在新增节点的时候,可以为节点指定特定的槽位。Codis 也提供了自动均衡策略。
Codis 不支持事务,其他的一些命令也不支持。
获取数据原理(mget):在 Redis 中的各个实例里获取到符合的 key,然后再汇总到 Codis 中。
Codis 是第三方提供的分布式解决方案,在官网的集群功能稳定之前,Codis 也得到了大量的应用。下面我们来看下官方提供的分布式方案。
4.3 Redis Cluster
Redis Cluster 是在 Redis 3.0 的版本正式推出的,用来解决分布式的需求,同时也可以实现高可用。跟 Codis 不一样,它是去中心化的,客户端可以连接到任意一个可用节点。
数据分片有几个关键的问题需要解决:
- 数据怎么相对均匀地分片
- 客户端怎么访问到响应的节点和数据
- 重新分片的过程,怎么保证正常服务
4.3.1 架构
Redis Cluster 可以看成是由多个 Redis 实例组成的数据集合。客户端不需要关注数据的子集到底存储在哪个节点,只需要关注这个集合整体。
以 3 主 3 从为例,节点之间两两交互,共享数据分片、节点状态等信息。
4.3.2 搭建
安装步骤在第一节预习资料里面
问题:Cluster 解决分片的问题,数据怎么分布?
4.3.3 数据分布
Redis 即没有用哈希取模,也没有用一致性哈希,而是用虚拟槽来实现的。
Redis 创建了 16384 个槽(slot),每个节点负责一定区间的 slot。比如 Node1 负责 0-5460,Node2 负责 5461-10922,Node3 负责 10923-16383.
对象分布到 Redis 节点上时,对 key 用 CRC16 算法计算再%16384,得到一个 slot 的值,数据落到负责这个 slot 的 Redis 节点上。
Redis 的每个 master 节点都会维护自己负责的 slot。用一个 bit 序列实现,比如:序列的第 0 位是 1,就代表第一个 slot 是它负责;序列的第 1 位是 0,代表第二个 slot 不归它负责。
$ redis-cli -p 7291
$ redis-cli -p 7292
$ redis-cli -p 7293
查看 key 属于哪个 slot:
redis> cluster keyslot key1
注意:key 与 slot 的关系是永远不会变的,会变的只有 slot 和 Redis 节点的关系。
问题:怎么让相关的数据落到同一个节点上?
比如有些 multi key 操作是不能跨节点的,例如用户 2673 的基本信息和金融信息。
在 key 里面加入 {hash tag} 即可。Redis 在计算槽编号的时候只会获取 {} 之间的字符串进行槽编号计算,这样由于上面两个不同的键,{} 里面的字符串是先通的,因此他们可以被计算出相同的槽。
user{2673}base=…
user{2673}fin=…
127.0.0.1:7293> set a{qs}a 1
OK
127.0.0.1:7293> set a{qs}b 1
OK
127.0.0.1:7293> set a{qs}c 1
OK
127.0.0.1:7293> set a{qs}d 1
OK
127.0.0.1:7293> set a{qs}e 1
OK
问题:客户端连接到哪一台服务器?访问的数据不在当前节点上,怎么办?
4.3.4 客户端重定向
比如在 7291 端口的 Redis 的 redis-cli 客户端操作:
127.0.0.1:7291> set qs 1
(error) Moved 13724 127.0.0.1:7293
服务端返回 MOVED,也就是根据 key 计算出来的 slot 不归 7291 端口管理,而是归 7293 端口管理,服务端返回 MOVED 告诉客户端去 7293 端口操作。
这个时候更换端口,用 redis-cli -p 7293 操作,才会返回 OK。或者用 ./redis-cli -c -p port 的命令。
这样客户端需要连接两次。Jedis 等客户端会在本地维护一份 slot——node 的映射关系,大部分时候不需要重定向,所以叫做 smart jedis(需要客户端支持)。
问题:新增或下线了 master 节点,数据怎么迁移(重新分配)?
4.3.5 数据迁移
因为 key 和 slot 的关系是永远不会变的,当新增了节点的时候,需要把原有的 slot 分配给新的节点负责,并且把相关的数据迁移过来。
添加新节点(新增一个 7297):
$ redis-cli --cluster add-node 129.0.0.1:7291 127.0.0.1:7297
新增的节点没有哈希槽,不能分布数据,在原来的任意一节点上执行:
$ redis-cli --cluster reshard 127.0.0.1:7291
输入需要分配的哈希槽的数量(比如 500),和哈希槽的来源节点(可以输入 all 或者 id)。
问题:只有主节点可以写,一个主节点挂了,从节点怎么办成主节点?
4.3.6 高可用和主从切换原理
当 slave 发现自己的 master 变为 FAIL 状态时,便尝试进行 Failover,以期成为新的 master。由于挂掉的 master 可能会有多个 slave,从而存在多个 slave 竞争成为 master 节点的过程,其过程如下:
- 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 的功能。
4.3.7 总结:
Redis Cluster 特点:
- 无中心架构。
- 数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布。
- 可扩展性,可先行扩展到 1000 个节点(官方推荐不超过 1000 个),节点可动态添加或删除。
- 高可用性,部分节点不可用时,集群仍可用。通过增加 slave 做 standby 数据副本,能够实现故障自动 failover,节点之间通过 gossip 协议交换状态信息,用投票机制完成 slave 到 master 角色提升。
- 降低运维成本,提高系统的扩展性和可用性。
整理完毕,完结撒花~ 🌻