1.如何设计亿级数据的缓存
2.哈希取余分区
3.一致性哈希算法分区
4.哈希槽分区
5.Redis集群哈希槽分区配置
6.哈希槽分区主从容错迁移
7.哈希槽分区主从扩容
8.哈希槽分区主从缩容
1.如何设计亿级数据的缓存
假设我们现在要设计一个存储案例,这个存储要能容纳1~2亿条数据,请问如何设计这个存储案例?
我们知道单台的redis可能无法存储这么多的数据量,这个时候我们就要使用redis集群进行分区存储,然而redis集群对于这么多的数据,一般有以下三种算法进行数据的保存:
1.1)哈希取余分区
1.2)一致性哈希算法分区
1.3)哈希槽分区
2.哈希取余分区
哈希取余分区存储是比较简单的一种操作,几乎看图就可以明白:
原理:
假设我们要存储两亿的数据,我们现在有三台reids构成一个集群,我们在存储/写入一个key的时候,用户每次读写操作都是根据公式:hash(key)%N个机器节点数,计算取出哈希值,用来决定数据映射在哪一个节点上。
优点:
实现简单,只需要预估好redis节点个数,就能保证一段时间的数据支撑。使用hash算法和取余能让数据落到对应的服务器上,这样每台服务器都能处理一部分请求,达到了负载均衡+分而治之的作用。
缺点:
因为一开始进行设计的时候,对redis的节点就进行了确定,进行扩缩容会比较麻烦,不管是扩容还是缩容,每次数据节点的变动,都会导致所有的数据需要重新计算映射,在服务器节点永远不发生变化的时候没有问题,如果要弹性扩缩容或故障停机的情况,取模公式就会发生变化。某个redis机器宕机了,由于台数数量变化,会导致hash取余全部数据重新洗牌。
3.一致性哈希算法分区
一致性哈希算法在1997年由麻省理工学院提出,设计目的是为了解决分布式缓存由于节点台数发生变化的时候,尽量减少影响客户端的数据到服务端的映射变化。
原理:
先要构建一致性哈希环:
它也是使用取模的方法,前面的redis集群哈希取余分区的取模方法是对节点(服务器)的数量进行取模。而一致性Hash算法是对2^32取模,将整个哈希值空间组织成一个虚拟的圆环,这个集合可以成为一个hash空间[0,2^32-1],这是一个线性空间,在算法中,我们会通过适当的逻辑将它首尾相连(0 = 2^32),这样就让它在逻辑上形成了一个环形空间,我们把这个由2^32个点组成的圆环称为Hash环。
然后是节点映射:
将集群中的节点映射到环上的某一个位置。
将各个服务器使用Hash进行一个哈希,具体可以选择服务器的IP或主机名作为关键字进行哈希,这样每台机器就能确定自己在哈希环上的位置。假如4个节点NodeAB、C、D,经过IP地址的哈希计算,使用IP地址哈希后在环空间的位置如下:
key到服务器的落键规则:
当我们要存储一个kv键值对的时候,首先要计算key的hash值,通过这个哈希值计算出这个key在环上的位置,从此位置沿着顺时针“行走”,第一台遇到的服务器就是其要保存key的服务器,并将该key保存在该节点上。
如我们有Object A、Object B、Object C、Object D四个数据对象,经过哈希计算后,在环空间上的位置如下:根据一致性Hash算法,数据A会被定为到Node A上,B被定为到Node B上,C被定为到Node C上,D被定为到Node D上。
优点:
一致性哈希算法的容错性佳:
假设现在的Node C宕机,我们可以看到ABD原有的数据不受影响,只有C对象被重新定位到Node D。一般在一致性hash算法中,某一台服务器不可用,受到影响的仅仅是hash环中的宕机这个节点与前一台服务器之间的数据,其它并不会受到影响,假设C挂了,受影响的是B、C之间的数据,并且这些数据会移动到D。
一致性哈希算法的扩展性佳:
假设数据量增加了,需要增加一个节点,这个节点增加在A和B之间,那受到影响的只是A到X之间的数据,重新把A到X的数据写到X上即可,不需要全部重新计算。
缺点:
一致性哈希算法有数据倾斜问题:
一致性哈希算法在服务器节点太少的时候,容易因为节点的分配不均匀而导致数据的倾斜,被缓存的key大部分都集中在一台服务器上:
4.哈希槽分区
通过前面的介绍,我们知道一致性hash算法有数据倾斜的问题,那么为了解决这种问题,我们有了hash槽分区算法。
原理:
哈希槽实质就是一个数组,数组[0,2^14 -1]形成hash slot空间。
它能解决均匀分配的问题,在数据和节点中又加入了一层,我们把这层称为哈希槽(slot),用于管理数据和节点之间的关系,现在就相当于节点上放的是槽,槽里放的是数据。
解决的问题:
槽解决的是粒度问题(可以移动数据),相当于把粒度变大了,这样便于数据移动。
哈希解决的是映射问题(也就是可以控制数据倾斜问题),使用key的哈希值来计算所在的槽,便于数据分配和调配。
redis一共有多少个hash槽?
一个集群只能有16382个槽(原因会在下面解释),这些槽会分配给集群中所有的主节点,分配策略没有要求,可以指定哪些编号的槽分配给哪些节点,集群会记录节点和槽的对应关系。
解决了节点和槽的关系以后,接下来就需要对key进行哈希求值,然后对16384取余,余数是几,key就落入对应的槽位。slot = CRC16(key) % 16384。以槽为单位移动数据,因为槽的数目是固定的,处理起来比较容易,这样数据移动问题就解决了。
为什么redis只能有16384个槽?
理论上CRC16算法可以得到2^16个数值,其数值范围在0-65535之间,也就是最多可以有65535个虚拟槽,取模运算key的时候,应该是CRC(key)%65535;但是却设计为crc16(key)%16384,原因是作者在设计的时候做了空间上的权衡,觉得节点最多不可能超过1000个,节点数量越多,节点间通信的成本越大(节点间通信的消息体内容越大,具体是消息头中携带的其他节点信息越大),为了保证节点之间通信效率,权衡之下所以采用了2^14个哈希槽。
哈希槽的计算方式:
上述提过,Redis集群中内置了16384个哈希槽,redis会根据节点数量,将大致均衡的哈希槽分配到对应的节点上。当需要在 Redis 集群中放置一个 key-value时,redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,也就是映射到某个节点上。如下代码,key之A 、B在Node2, key之C落在Node3上
5.redis集群哈希槽分区配置
在启动redis集群配置之前,我们先要把所有防火墙的端口打开,以便我们的操作系统连接:
firewall-cmd --zone=public --add-port=6381/tcp --permanent
firewall-cmd --zone=public --add-port=6382/tcp --permanent
firewall-cmd --zone=public --add-port=6383/tcp --permanent
firewall-cmd --zone=public --add-port=6384/tcp --permanent
firewall-cmd --zone=public --add-port=6385/tcp --permanent
firewall-cmd --zone=public --add-port=6386/tcp --permanent
firewall-cmd --reload
然后我们启动6个redis:
docker run -d --name redis-node-1 --net host --privileged=true -v /data/redis/share/redis-node-1:/data redis --cluster-enabled yes --appendonly yes --port 6381
docker run -d --name redis-node-2 --net host --privileged=true -v /data/redis/share/redis-node-2:/data redis --cluster-enabled yes --appendonly yes --port 6382
docker run -d --name redis-node-3 --net host --privileged=true -v /data/redis/share/redis-node-3:/data redis --cluster-enabled yes --appendonly yes --port 6383
docker run -d --name redis-node-4 --net host --privileged=true -v /data/redis/share/redis-node-4:/data redis --cluster-enabled yes --appendonly yes --port 6384
docker run -d --name redis-node-5 --net host --privileged=true -v /data/redis/share/redis-node-5:/data redis --cluster-enabled yes --appendonly yes --port 6385
docker run -d --name redis-node-6 --net host --privileged=true -v /data/redis/share/redis-node-6:/data redis --cluster-enabled yes --appendonly yes --port 6386
接下来我们进入容器redis-node-1,为6台机器构建集群关系:
先查一下自己的ip地址:
进入容器
docker exec -it redis-node-1 /bin/bash
构建主从关系:
redis-cli --cluster create 192.168.64.129:6381 192.168.64.129:6382 192.168.64.129:6383 192.168.64.129:6384 192.168.64.129:6385 192.168.64.129:6386 --cluster-replicas 1
配置成功!
我们进入6381看一下节点的状态:
我们往6381里写入数据看看:
我们会发现报错,那时因为我们没有开启集群模式读写,要加上参数-c
-c Enable cluster mode (follow -ASK and -MOVED redirections).
redis-cli -p 6381 -c
此时我们看出,设置两个值后,会自动帮我们计算并定位。
查看一下集群的状态:
redis-cli --cluster check 192.168.64.129:6381
6.哈希槽分区主从容错迁移
我们先把刚刚的6381停掉,看看真实主机的上位情况:
可以看出,6381停止后,6384成为了新的主节点。
我们启动原来的6381节点,看看6381节点的状态,是master,还是slave:
可以看出,再启动之后,之前的节点点就变成了slave。
7.哈希槽分区主从扩容
之前在讲哈希槽分区的理论的时候,没有说到哈希槽分区的主从扩缩容是什么样的,我们用实战来进行理解一下:
我们新建两个节点,加入集群之中,看看是什么效果:
创建两个节点:
docker run -d --name redis-node-7 --net host --privileged=true -v /data/redis/share/redis-node-7:/data redis --cluster-enabled yes --appendonly yes --port 6387
docker run -d --name redis-node-8 --net host --privileged=true -v /data/redis/share/redis-node-8:/data redis --cluster-enabled yes --appendonly yes --port 6388
进入容器6387内部:
docker exec -it redis-node-7 /bin/bash
将新增的6387节点作为master加入原集群:
redis-cli --cluster add-node 192.168.64.129:6387 192.168.64.129:6381
检查集群状态第1次:
redis-cli --cluster check 192.168.64.129:6381
我们发现,现在主节点有了四个,但是槽号还是没有分配,我们要给新加进来的主节点分配槽号。
重新分配槽号:
redis-cli --cluster reshard 192.168.64.129:6381
因为16384/4=4096,所以我们分配4096个槽号给新加进来的节点:
检查集群状态第2次:
redis-cli --cluster check 192.168.64.129:6381
我们可以看出,我们从三个主节点那里,各拿了一些槽号分给新的主节点,这就避免了整体重新hash。
为主节点6387分配从节点6388:
命令:redis-cli --cluster add-node ip:新slave端口 ip:新master端口 --cluster-slave --cluster-master-id 新主机节点ID
redis-cli --cluster add-node 192.168.64.129:6388 192.168.64.129:6387 --cluster-slave 00cluster-master-id 62539568294324e2c5aa35146de9b5a45c65d440
检查集群状态第3次:
redis-cli --cluster check 192.168.64.129:6381
成功完成四主四从的扩容!
可以看出,在扩容的时候,并没有整体进行重新分配,而是从之前的redis节点中,各拿了一些槽号到这个主节点,实现了扩容,可以看出这种方式,对影响的数据还是比较少的。
8.哈希槽分区主从缩容
检查集群状况第1次,查看6388的节点id:
redis-cli --cluster check 192.168.64.129:6381
将6388删除:
命令:redis-cli --cluster del-node ip:从机端口 从机6388节点ID
redis-cli --cluster del-node 192.168.64.129:6388 7066a848eaf5a46563da76fa669b169a07adc482
将6387的槽号清空,交给6384来接管:
检查集群状况第2次:
redis-cli --cluster check 192.168.64.129:6381
槽位已交出:
将6387删除:
redis-cli --cluster del-node 192.168.64.129:6387 62539568294324e2c5aa35146de9b5a45c65d440
检查集群情况第3次:
redis-cli --cluster check 192.168.64.129:6381
成功恢复三主三从!
我们可以看出哈希槽分区在扩缩容的时候,需要手动进行操作。要把被删除的那个节点的槽号交接给其中一个节点,也是只需要移动一部分数据,不用重新hash,这种手动操作,可以控制存储的大小,自由度很高,非常地方便。