目录
如何用Redis实现亿级数据缓存
目前主流的Redis设计方案有如下三种
哈希取余分区
一致性哈希算法
哈希槽分区
2亿条记录就是2亿个k,v,我们单机不行必须要分布式多机,假设有三台机器构成一个集群,用户每次读写操作都是根据公式:hash(key)%N个机器台数,计算出哈希值,用来决定数据映射到哪一个节点上。
比如说:0就是第一台,2就是第二台,3就是第三台,因为Redis里面的key是不会重复的。
优点:简单粗暴,直接有效,只需要预估好数据规划好节点,例如3台,8台,10台,就能保证一段时间的数据支撑。使用hash算法让固定的一部分请求落到同一台服务器上,这样每台服务器固定处理一部分请求(并维护这些请求的信息),起到负载均衡+分而治之的作用。
缺点:原来规划的节点,进行扩容或者缩容就比较麻烦,不管扩缩,每次数据变动导致节点有变动,映射关系需要重新进行计算,在服务器个数固定不变的时候没有问题,如果需要弹性伸缩或者故障宕机的情况下,原来的取模公式就会发生变化:即hash(key)/3会变成hash(key)/?。此时地址经过取余运算的结果将发生很大变化,根据公式获取的服务器也会变得不可控。某个Redis机器宕机了,由于台数发生了变化,会导致hash取余全部数据重新洗牌。
一致性哈希算法
设计目标是为了解决分布式缓存数据变动和映射问题,因为某个机器宕机了,分母数量改变了,自然取余就不行了。
提出一致性hash解决方案,目的是当服务器个数发生变化时,尽量减少影响客户端到服务器的映射关系。
一致性hash算法的三个步骤:
1. 算法构建一致性hash环
2. 服务器ip节点映射
3. key落到服务器的落键规则
1. 算法构建一致性hash环
一致性hash算法必然有个hash函数并按照算法产生的hash值,这个算法所有可能hash值会构成一个全量集,这个集合可以成为一个hash空间[0,2^32-1],这个是一个线性空间,但是在算法中,我们通过适当的逻辑控制将他收尾相连(0 = 2^32),这样让它逻辑上形成一个环形空间。
它也是按照取模的方式,前面笔记介绍节点取模法是对节点(服务器)的数量进行取模。而一致性hash算法是对2^32取模,简单来说,一致性hash算法将整个hash值空间组织成一个虚拟的圆环,如假设某hash函数H的值空间为0-2^32-1(即hash值是一个32位无符号整形),整个hash环如下,整个空间按照顺时针方向组织,圆环的正上方的点代表0,0点右侧的第一个点代表1,以此类推,2,3,4......直到2^32-1,也就是说0点左侧的第一个点代表2^32-1,0和2^32-1在零点中方向重合,我们把整个由2^32个点组成的圆环称为hash环。
2. 服务器ip节点映射
将集群中各个ip节点映射到环上的某个位置。
将各个服务器使用hash进行一个哈希,具体可以选择服务器的ip或者主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置。例如:4个几点nodeA,B,C,D,经过IP地址的哈希函数计算(hash(ip)),使用ip地址哈希后在环空间的位置如下:
3. key落到服务器的落键规则
当我们需要存储一个KV键值对时,首先计算key的hash值,hash(key),将这个key使用相同的函数hash计算出哈希值并确定此数据在环上的位置,从此位置沿着环顺时针“行走”,第一台遇到的服务器就是器应该定位到的服务器,并将该键值对存储在该节点上。
如我们有Object A,Object B,Object C,Object D四个数据对象,经过哈希计算后,在环空间上的位置如下:根据一致性hash算法,数据A会被定位Node A,B会定为Node B,C会定为Node C,D会定为Node D
优点:
1. 一致性哈希算法的容错性
假设Node C宕机了,可以看到此时对象A,B,D不会受到影响,只有C对象会重新定位到Node D上。一般的,在一致性hash算法中,如果一台服务器宕机了,则受影响的数据仅仅是次服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到哦的第一台服务器)之前的数据,其他不会受到影响,简单来说,就是C挂了,受到影响的只是B,C之间的数据,并且这些数据会转移到D进行存储。
2. 一致性哈希算法的扩展性
数据量增加了,需要增加一台节点Node X,X的位置在A和B之间,那受到影响的也就是A到X之间的数据,重新把A到X的数据录入到X上即可,不会导致hash取余全部数据重新洗牌。
缺点:一致性哈希算法的数据倾斜问题
hash环的数据倾斜问题
一致性hash算法在服务节点太少时,容器因为节点分布不均匀而造成数据倾斜(被缓存的对象大部分集中 缓存在某一台服务器上)问题,例如:系统中只有两台服务器
为了 在节点数目发生改变时尽可能少的迁移数据
将所有的存储节点排列在首尾相接的hash环上,每个key在计算hash后会顺时针找到临近的存储节点存放,而当所有节点加入或者退出时仅影响该节点hash环上顺时针相邻的后续节点。
优点:加入或者删除节点只影响哈希环中顺时针方向的相邻的节点,对其他节点无影响。
缺点:数据的分布和节点的位置有关,因为这些节点不是均匀的分布在哈希环上的,所以数据在镜像存储时达不到分布均匀的效果。
哈希槽分区
由于一致性哈希算法具有数据倾斜的问题,因此诞生了我们的哈希槽分区的思想
哈希槽分区就是一个数组,数组[0,2^14-1]形成hash slot空间。
能够解决均匀分配的问题,在数据和节点之间又加了一层,把这层称为哈希槽(slot),英语管理数据和节点之间的关系,现在就相当于节点上放的是槽,槽里方的是数据。
槽解决的是粒度问题,相当于把粒度变大了,这样便于数据移动。
(数据值管找到对应的槽)
哈希解决的是映射问题,使用key的哈希值来计算所在的槽,便于数据分配。
一个集群中只能有16384个槽,编号0-16383(0-2^14-1)。这些槽会分配给集群中的所有主节点,分配策略没有要求。可以指定哪些编号的槽分配给哪个主节点。集群会记录节点和槽的对应关系。解决了节点和槽的关系后,接下来就需要对key求哈希值,然后对16384取余,余数是几key就落入到对应的槽里。slot = CRC16(key) % 16384。以槽为单位移动数据,因为槽的数目是固定的,处理起来比较容易,这样数据移动问题就解决了。
Redis集群并没有使用一致性hash而是引入了哈希槽的概念。Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。但是为什么哈希槽的数量是16384(2^14)个呢?
CRC16算法产生的hash值有16bit,该算法可以产生2^16=65536个值。换句话说是分布在0-65536之间,那作者在做mod运算时候,为什么不mod65536,而选择mod16384?
博客连接:【原创】为什么Redis集群有16384个槽 - 孤独烟 - 博客园
哈希槽的计算
Redis集群中内置了16384个哈希槽,Redis会根据节点数量大致均等的将哈希槽映射到不同的节点。当需要在Redis集群中放置一个key-value时,Redis先对key使用crc16算法算出一个结果,然后把结果对16384求余数,这样每个key都会对应一个编号在0-16383之间的哈希槽,也就是映射到某个节点上。如下代码,key之A,B,在node2上,key之C在node3上
案例演示
三主三从Redis集群扩缩容配置案例
三主三从redis集群搭建
1. 新建6个docker容器实例
docker run -d --name redis-node-1 --net host --privileged=true -v /data/redis/share/redis-node-1:/data redis:6.0.8 --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:6.0.8 --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:6.0.8 --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:6.0.8 --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:6.0.8 --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:6.0.8 --cluster-enabled yes --appendonly yes --port 6386
[root@master1 ~]# docker ps|grep redis
92f5d7c3aff1 redis:6.0.8 "docker-entrypoint.s…" 8 seconds ago Up 8 seconds redis-node-6
5eae04073e39 redis:6.0.8 "docker-entrypoint.s…" 9 seconds ago Up 8 seconds redis-node-5
1604f1334b57 redis:6.0.8 "docker-entrypoint.s…" 9 seconds ago Up 9 seconds redis-node-4
9e37e565f5e0 redis:6.0.8 "docker-entrypoint.s…" 9 seconds ago Up 9 seconds redis-node-3
a1a26bba3c4d redis:6.0.8 "docker-entrypoint.s…" 9 seconds ago Up 9 seconds redis-node-2
c36eaf760886 redis:6.0.8 "docker-entrypoint.s…" 9 seconds ago Up 9 seconds redis-node-1
2. 为6台机器构建集群关系
这里我们以redis-node-1为切入口(当然以其他的机器也行,这里没有要求)
[root@master1 ~]# docker exec -it redis-node-1 bash
root@master1:/data#
然后构建主从关系
[root@master1 ~]# docker exec -it redis-node-1 bash
root@master1:/data# redis-cli --cluster create 192.168.64.150:6381 192.168.64.150:6382 192.168.64.150:6383 192.168.64.150:6384 192.168.64.150:6385 192.168.64.150:6386 --cluster-replicas 1
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 192.168.64.150:6385 to 192.168.64.150:6381
Adding replica 192.168.64.150:6386 to 192.168.64.150:6382
Adding replica 192.168.64.150:6384 to 192.168.64.150:6383
>>> Trying to optimize slaves allocation for anti-affinity
[WARNING] Some slaves are in the same host as their master
M: d1d4d2c6f8f5f97756d2d1ca4e0bbca15a69e0f2 192.168.64.150:6381
slots:[0-5460] (5461 slots) master
M: 0e83424efc6c9968e95be6f9b5b5b88ca7eb1420 192.168.64.150:6382
slots:[5461-10922] (5462 slots) master
M: 714f8fa6b51775a193b4190788d0ec2370bd3f1c 192.168.64.150:6383
slots:[10923-16383] (5461 slots) master
S: 2241baae7c0a5712e7087b26791f0648af259a0c 192.168.64.150:6384
replicates 714f8fa6b51775a193b4190788d0ec2370bd3f1c
S: 44eb9fc9245cfe4004e3f644c176b947a8faf080 192.168.64.150:6385
replicates d1d4d2c6f8f5f97756d2d1ca4e0bbca15a69e0f2
S: fc862ff3d9e0d8f4c1614dad487850544379cfbd 192.168.64.150:6386
replicates 0e83424efc6c9968e95be6f9b5b5b88ca7eb1420
Can I set the above configuration? (type 'yes' to accept): yes
.......
--cluster-replicas 1 表示为每个master创建一个salve节点
没有报错,即可完成。这样子就形成了如下的slot位分布
3. 查看集群状态
还是以redis-node-1为切入点,redis-cli -p表示连接到哪台redis机器上
root@master1:/data# redis-cli -p 6381
127.0.0.1:6381> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:6
cluster_my_epoch:1
cluster_stats_messages_ping_sent:368
cluster_stats_messages_pong_sent:371
cluster_stats_messages_sent:739
cluster_stats_messages_ping_received:366
cluster_stats_messages_pong_received:368
cluster_stats_messages_meet_received:5
cluster_stats_messages_received:739
127.0.0.1:6381>
127.0.0.1:6381> cluster nodes
714f8fa6b51775a193b4190788d0ec2370bd3f1c 192.168.64.150:6383@16383 master - 0 1641905260000 3 connected 10923-16383
44eb9fc9245cfe4004e3f644c176b947a8faf080 192.168.64.150:6385@16385 slave d1d4d2c6f8f5f97756d2d1ca4e0bbca15a69e0f2 0 1641905260902 1 connected
d1d4d2c6f8f5f97756d2d1ca4e0bbca15a69e0f2 192.168.64.150:6381@16381 myself,master - 0 1641905259000 1 connected 0-5460
2241baae7c0a5712e7087b26791f0648af259a0c 192.168.64.150:6384@16384 slave 714f8fa6b51775a193b4190788d0ec2370bd3f1c 0 1641905260000 3 connected
0e83424efc6c9968e95be6f9b5b5b88ca7eb1420 192.168.64.150:6382@16382 master - 0 1641905259000 2 connected 5461-10922
fc862ff3d9e0d8f4c1614dad487850544379cfbd 192.168.64.150:6386@16386 slave 0e83424efc6c9968e95be6f9b5b5b88ca7eb1420 0 1641905261907 2 connected
127.0.0.1:6381>
通过cluster nodes命令可以看到三主是三主,分别对应的三从是哪三从。
例如:
三个slave 是6385,6384,6386。
主从挂载是随机的,在这个集群中
从机6385挂在主机6381下
从机6384挂在主机6383下
从机6386挂在主机6382下
myself表示登录的是哪台,这里我们是以redis-node-1登录进来的。。。
主从容错切换迁移案例
1. 数据读写存储
我们现在往集群中写入数据,能不能在集群中其他的机器中读到数据,即数据共享。
root@master1:/data# redis-cli -p 6381
127.0.0.1:6381> keys *
(empty array)
127.0.0.1:6381>
目前的初始状态是空的数据。然后我们简单的存入一个数据
127.0.0.1:6381> set k1 v1
(error) MOVED 12706 192.168.64.150:6383
127.0.0.1:6381>
发现error了,原因如下:
我们知道我们在刚开始创建集群的时候,redis已经把我们的slot分配好了,如下所示
此时我们进入的是6381这个机器,执行set k1 v1,按照redis内部的算法,k1算出来的slot位是12706,此时已经超过6381这个机器的槽位范围了(0-5460),就报错了。验证如下,我们切换到6383这台机器上
root@master1:/data# redis-cli -p 6383
127.0.0.1:6383>
127.0.0.1:6383> set k1 v1
OK
127.0.0.1:6383>
当然,redis的做法不是这样的,不然我们每次还要算一下slot位。
单机的方式,上面的redis-cli -p 6383这种进入机器的方式是正确的,根据提示MOVED 12706 192.168.64.150:6383,我们也可以知道,单机环境下跳不到6383这台机器上。
但是我们这里是集群环境,正确的方式是:加上参数-c表示放置路由失效
root@master1:/data# redis-cli -p 6381 -c
192.168.64.150:6381> FLUSHALL #清空集群数据
OK
192.168.64.150:6383> KEYS *
(empty array)
127.0.0.1:6381> set k1 v1
-> Redirected to slot [12706] located at 192.168.64.150:6383
OK
192.168.64.150:6383>
192.168.64.150:6383> SET k2 v2
-> Redirected to slot [449] located at 192.168.64.150:6381
OK
192.168.64.150:6381>
我们发现set的数据算出的slot在哪个机器的槽范围内,就会自动跳转到哪台机器上去。
然后我们读取一下数据
root@master1:/data# redis-cli -p 6381 -c
127.0.0.1:6381>
127.0.0.1:6381> get k1
-> Redirected to slot [12706] located at 192.168.64.150:6383
"v1"
192.168.64.150:6383>
192.168.64.150:6383> get k2
-> Redirected to slot [449] located at 192.168.64.150:6381
"v2"
192.168.64.150:6381>
192.168.64.150:6381> get k3
(nil)
192.168.64.150:6381>
补充一个redis检查集群状态的命令
root@master1:/data# redis-cli --cluster check 192.168.64.150:6381
2. 容错切换迁移
假如我们的某个master机器宕机了,那么slave机器要能够补上来。
根据之前的笔记,我们主从关系如下,也可以通过cluster node命令查看
我们直接stop一台机器
然后进入到节点机器中去,使用cluster nodes命令查看node状态,还是六个节点,但是6381那台机器已经处于fail状态,而它的slave 6385变成了master,证明上位成功。
若此时,6381这台机器修复好了,这个时候6381跟6385的主从关系如何呢?其实很简单,6385还是master,恢复的机器6381变成了slave
3. 集群扩容案例
由于我们的三主三从集群已经把slot位分配好了,这个时候加入一台机器,那么slot的分配情况如何呢?答案是每个机器匀一点出来给新加入的机器
3.1 新建6387,6388两个节点
[root@master1 ~]# docker run -d --name redis-node-7 --net host --privileged=true -v /data/redis/share/redis-node-7:/data redis:6.0.8 --cluster-enabled yes --appendonly yes --port 6387
[root@master1 ~]# docker run -d --name redis-node-8 --net host --privileged=true -v /data/redis/share/redis-node-8:/data redis:6.0.8 --cluster-enabled yes --appendonly yes --port 6388
进入到redis-node-7里面将新增的6387节点作为master加入原集群中
[root@master1 ~]# docker exec -it redis-node-7 bash
root@master1:/data# redis-cli --cluster add-node 192.168.64.150:6387 192.168.64.150:6381
>>> Adding node 192.168.64.150:6387 to cluster 192.168.64.150:6381
>>> Performing Cluster Check (using node 192.168.64.150:6381)
S: d1d4d2c6f8f5f97756d2d1ca4e0bbca15a69e0f2 192.168.64.150:6381
slots: (0 slots) slave
replicates 44eb9fc9245cfe4004e3f644c176b947a8faf080
M: 44eb9fc9245cfe4004e3f644c176b947a8faf080 192.168.64.150:6385
slots:[0-5460] (5461 slots) master
1 additional replica(s)
M: 714f8fa6b51775a193b4190788d0ec2370bd3f1c 192.168.64.150:6383
slots:[10923-16383] (5461 slots) master
1 additional replica(s)
M: 0e83424efc6c9968e95be6f9b5b5b88ca7eb1420 192.168.64.150:6382
slots:[5461-10922] (5462 slots) master
1 additional replica(s)
S: 2241baae7c0a5712e7087b26791f0648af259a0c 192.168.64.150:6384
slots: (0 slots) slave
replicates 714f8fa6b51775a193b4190788d0ec2370bd3f1c
S: fc862ff3d9e0d8f4c1614dad487850544379cfbd 192.168.64.150:6386
slots: (0 slots) slave
replicates 0e83424efc6c9968e95be6f9b5b5b88ca7eb1420
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
>>> Send CLUSTER MEET to node 192.168.64.150:6387 to make it join the cluster.
[OK] New node added correctly.
root@master1:/data#
add-node参数接要加入的机器,再接一个“领路人”,我们这里以6381这台机器作为6387加入的领路人,相当于6387通过6381找到了要加入的集群。
此时通过redis-cli --cluster check命令可以查看到集群
但是6387的机器0 slots,0 slaves,表示暂时没有槽位和从机。
3.2 重新分配槽号
我们刚开始的三主三从的集群对于槽位的分配几乎是均等分配到三台master机器中,那么此时加入进来一台机器,这个槽位的分配又变成什么样的呢?是重新分配,还是集群中各自匀一点出来给新的机器呢?答案是后者,各自匀一点slot。
重新分配槽号的命令参数:reshard
redis-cli --cluster reshard 192.168.64.150:6381
第一个问题是在问我们要怎么重新分配,计算方式如下:
16384/4=4096
最后的情况如下:
为什么6387是3个新的区间,以前的还是连续的呢?
重新分配的成本太高,所以前三家各自匀出来一部分,从6381/6382/6383三个旧节点分别匀出1364个坑位给6387。(3*1364=4092,也就是约等于4096个槽位)
3.3 为主节点6387分配从节点6388
命令:redis-cli --cluster add-node ip:新slave端口 ip:新master端口 --cluster-salve --cluster-master-id 新主机节点ID
(新主机节点可以通过命令redis-cli --cluster check 192.168.64.150:6381)
再次检查集群节点状态
至此,我们集群扩容案例已经完成了。
4. 集群收缩案例
现在的集群情况如下
当峰流下去之后,我们要想缩减机器来减少资源的消耗,那么缩容,我们面临的问题就是slot位的分配问题,还有主从的删除先后顺序问题。
其实是先删除从机,然后把清出来的slot号重新分配,再删除主机,即可恢复三主三从。
如下操作:
4.1. 首先获取要删除的主从机的ID号
然后删除从节点
命令:redis-cli --cluster del-node ip:从机端口 从机ID
现在slave只剩下三个了
4.2 将要删除的master的槽号清空,重新分配
本次测试把slot清空,通过6382这台master作为切入点,把被删除的那个master的slot槽全部分给6385这台master(当然,我们也可以一部份分给6385这台master,一部份分给其他master)
root@master1:/data# redis-cli --cluster reshard 192.168.64.150:6382
............
How many slots do you want to move (from 1 to 16384)? 4096 #这里填被删除的slot槽数,若想分一部分,这里填相应的个数即可
What is the receiving node ID? 44eb9fc9245cfe4004e3f644c176b947a8faf080 #谁来接收被删除的master的slot号
..............
Source node #1: 0ccd6d451d50440ff247310e2f477e5e57b7ec15 #要删除的master的ID号
Source node #2: done
最后分配完成之后的情况如下:
4.3 删除要删除的那个master