背景 1~2亿条数据需要缓存,请问如何设计存储案例?上述问题阿里P6~P7工程案例和场景设计类必考题目,一般业界有三种解决方案^:2、一致性哈希算法分区3、哈希槽分区 1、哈希取余分
背景
1~2亿条数据需要缓存,请问如何设计存储案例? 上述问题阿里P6~P7工程案例和场景设计类必考题目,一般业界有三种解决方案^: 2、一致性哈希算法分区 3、哈希槽分区
1、哈希取余分区
2亿条记录就是2亿个k,v,我们单机不行,必须要分布式多机,假设有3太机器构成一个集群,用户每次读写操作都是根据公式:hash(key)%N个机器台数,计算出哈希值,用来决定数据映射到哪一个节点上 优点: 简单粗暴,直接有效,只需要预估好数据,规划好节点,例如3台、8台、10台,就能保证一段时间的数据支撑。使用Hash算法让固定的一部分请求落到同一台机器上,这样每一台服务器固定处理一部分请求(并维护这些请求信息),起到负载均衡+分而治之的作用。 缺点: 原来规划好的节点,进行扩容或者缩容就比较麻烦了,不管扩容还是缩容,每次数据变动导致节点有变动,映射关系就需要进行重新计算,在服务器个数固定不变时没有问题,如果需要弹性扩容或者故障停机的情况下,原来的取余公式就会发生变化:hash(key)/3会变成hash(key)/?。此时,经过某个redis机器宕了,由于台数数量发生变化,会导致hash取余全部数据重新洗牌。 我们来举个例子: 假设请求1的取余结果为【hash(请求1)/3】:第一台机器,我们把数据存到了第一台机器,如果第一台机器宕机时,我们要去第一台机器查询我们请求1的数据时,是查询不到的。 也就是说,当服务器数量发生改变时,所有缓存在一定时间内是失效的,当应用无法从缓存中获取数据时,则会频繁向后端服务器请求也无法存储。造成雪崩 为了解决这个问题,就拥有了一次性哈希算法2、一致性哈希算法分区
是什么: 一致性哈希算法在1997年由麻省理工学院中提出的,目的是为了解决分布式缓存数据变动和映射问题,某个单机坏了,分母数量变了,自然取余数就不ok了。 能干什么? 提出一致性哈希算法的解决方案。目的是当服务器个数发生变动的时候,尽量减少影响客户端到服务器的映射关系。 一致性哈希算法的具体步骤如下:(一)、算法构建一致性哈希环
一致性哈希算法必然有个hash函数,并按照算法产生哈希值,这个算法的所有可能哈希值会构成一个全量集,这个集合可以成为一个哈希空间[0,2^32-1],这是一个线性的空间,我们通过适当的逻辑控制将它的首尾相连(0=2^32),这样让它逻辑上形成了一个闭环空间。 哈希取模算法是对节点(服务器)的数量进行取模,而一致性哈希算法是对2^32进行取模,简单来说,一致性哈希算法将整个哈希值空间组成一个虚拟的圆环。 我们可以将2^32想象成一个圆,像钟表一样,钟表的圆可以理解成由60个圆点组成,而一致性哈希环我们可以理解为由2^32个远点组成,成功空间按照顺时针方向组织,圆环的正上方的点代表0,0点右侧的第一个点代表1,以此类推,2、3、4、5、6……直到2^32-1,0点与2^32重合,我们把这个由2^32个点组成的圆环称为hash环(二)、服务器IP节点映射
将集群上的各个点映射到环上的某一个位置 哈希算法:hash(服务器的IP) % 2^32 将各个服务器节点进行一个哈希计算,具体可以选择服务器的ip或主机名作为关键字进行哈希计算,这样每台机器就能确定其在哈希环上的位置。假如4个节点A、B、C、D,经过IP地址的hash计算,使用IP地址哈希后在环空间的位置如下:(三)、key落到服务器的落键规则
当我们需要存储一个k,v键值对的时候,首先计算key的hash值,根据计算出的哈希值确定此数据在哈希环上的位置,从此位置沿着顺时针行走,遇到的第一台服务器就是其应该定位到的服务器,并将该键值对存储在该节点上。 如下图,有1、2、3,三个请求,按照上面规则,1、3请求顺时针行走遇到的第一个服务是A服务,所以将1、3的请求数据存储在A服务器上,2请求按照顺时针行走遇到的第一个服务是B服务,所以将2的请求数据存储在B服务上。一致性哈希算法的优点
1、容错性 假设,我们上面例子的B服务宕机了,此时2请求进来时,沿着顺时针行走遇到的第一台服务是C服务器,所以2请求被重新定位到C服务器。此时A、B服务环中间的那部分取数会受到影响,而存数据不会受到影响,会定位到C服务。 2、扩展性 数据量增加了,需要增加一台节点X,X的位置在A和D之间,那受到查询影响的就是X和D之间的数据,而D到X之间的存数都落到X服务上即可,不会导致哈希取余所有数据重新洗牌。一致性哈希算法的缺点
hash环的数据倾斜问题 一致性哈希算法在服务节点太少时,容易因节点分布不均匀而造成数据倾斜(被缓存的对象一大部分集中在某一台服务上)问题3、哈希槽算法分区
(一)、为什么会出现
解决一致性哈希算法的数据倾斜问题 哈希槽实际上就是一个数组,数组[0,2^14-1]形成哈希槽空间(hash slot)(二)、能干什么
解决均匀分配问题,在数据和节点之间又加入了一层,把这层成为哈希槽(hash slot) ,用于管理数据和节点之间的关系,现在就相当于节点上放的是槽,槽里放的是数据 槽解决的是粒度的问题,相当于把粒度变大了,这样便于数据移动(三)、多少个hash槽
一个集群只能有16384个槽,编号0~16383(0-2^14-1)。这些槽会分配给集群中所有主节点,分配策略没有要求。可以指定哪些编号的槽分配给哪个主节点,集群会记录节点和槽的对应关系,解决了节点和槽的对应关系后,接下来就需要对key求哈希值,然后对16384取余,,余数是几,key就落入对应的槽里。slot=CRC16(key)%16384。以槽为单位移动数据,因为槽的数目是固定的,处理起来比较容易,这样数据移动的问题就解决了。(四)、哈希槽的计算
Redis集群中内置了16384个哈希槽,redis会根据节点数量大致均匀的将哈希槽映射到不同的节点。当需要在Redis集群中放置一个key-value时,redis先对key使用crc16算法算出一个结果,然后把结果对16384取余,这样每个key都会对应一个编号在0-16383之间的哈希槽,也就是映射到某个节点上。一、3主3从redis集群配置
docker run:创建并运行docker容器实例 --name redis-node-1:容器实例的名字 -d:后台守护进程运行 –net host:使用宿主机的ip和端口号,默认 --privileged=true:获取宿主机root用户权限,开启容器卷挂载 --cluster-enabled yes: 表示将 Redis 实例设置成集群节点而不是单机服务器 --appendonly yes:开启AOF持久化操作 --port 6386:redis端口号 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 返回以下结果,6个docker容器实例创建成功,此时,六台redis实例之间没有任何关系 进入容器redis-node-1并为6台机器构建集群关系: 1、进入容器: docker exec -it redis-node-1 /bin/bash 2、构建主从关系: redis-cli :进入到redis容器里 --cluster create:构建集群 --cluster-replicas 1:为每个master创建一个slave节点 注意使用自己的IP,进入docker容器后执行下面命令: redis-cli --cluster create 192.168.150.128:6381 192.168.150.128:6382 192.168.150.128:6383 192.168.150.128:6384 192.168.150.128:6385 192.168.150.128: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.150.128:6385 to 192.168.150.128:6381 Adding replica 192.168.150.128:6386 to 192.168.150.128:6382 Adding replica 192.168.150.128:6384 to 192.168.150.128:6383 将哈希槽分配到6个节点中,三主三从,master1~master3平均分配,以及主从对应关系 连接进入6381作为切入点,查看集群状态: 1、进入6381: redis-cli -p 6381 2、cluster info 3、cluster nodes 根据下图可以看出,本实例的主·从对应关系如下: Master Slaver 1 5 2 6 3 4二、主从容错切换迁移
1、数据读写存储
启动6机构成的集群,并通过exec进入
docker exec -it redis-node-1 /bin/bash redis-cli -p 6381对6381新增2个key
放入一个k1,v1后返回以下错误,数据存不进去,槽位号是12706,不在1服务上: 127.0.0.1:6381> set k1 v1 (error) MOVED 12706 192.168.150.130:6383 我们再来放一个k2,v2,结果如下,可以正常存放: 127.0.0.1:6381> set k2 v2 OK防止路由失效,加参数-c并新增2个key
以上数据无法放进去,我们来加一个参数解决这个问题: 防止路由失效,加参数-c后,并新增2个key -c代表集群环境连接 redis-cli -p 6381 -c 在此命令进入redis之后,发现可以正常插入数据,下图中,Redirected为重定向查看集群信息
redis-cli --cluster check 192.168.150.130:63812、容错切换迁移
(一)、主6381和从机切换,先停止主机6381
docker stop redis-node-1(二)、再次查看集群信息
随便进入一个节点: docker exec -it redis-node-2 bash redis-cli -p 6382 -c 查看集群信息如下: cluster nodes 可以看出6381已经宕机,6381之前对应的从机6385已经上位变成主机 并且查询之前存储的数据都可以查到
(三)、先还原之前的3主3从
先启动6381: docker start redis-node-1 我们启动节点1以后,发现,节点1成为了slaver,5节点还是master再停6385: docker stop redis-node-5 再启动6385: docker start redis-node-5 此时,我们恢复了最初的状态 Master Slaver 1 5 2 6 3 4
(四)、查看集群状态
root@192:/data# redis-cli --cluster check 192.168.150.130:63813、主从扩容案例(新增一主一从)
新建6387、6388两个节点+新建后启动 +查看是否是8个节点
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 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 执行完上面两个命令后,只是启动了2个redis容器实例,这两个容器实例和集群没有任何关系进入6387容器实例内部
docker exec -it redis-node-7 /bin/bash新增的6387节点(空槽号)作为master节点加入原集群
将新增的6387节点作为master节点加入集群 redis-cli --cluster add-node 自己实际的IP地址:6387 自己实际的IP地址:6381 redis-cli --cluster add-node 192.168.150.130:6387 192.168.150.130:6381 6387就是将要作为master的新增节点 6381就是原来集群节点里的领路人,相当于6387加入6381领头的组织检查集群情况第一次
redis-cli --cluster check 真实的ip地址:6381 redis-cli --cluster check 192.168.150.130:6381 此时我们的槽位分配情况为: 6381 :0~5460 6382: 5461~10922 6383:10923~16383重新分配槽号
redis-cli --cluster reshard ip地址:端口号 redis-cli --cluster reshard 192.168.150.130:6381检查集群情况第二次
重新分配槽后,槽位分配情况如下: 6381:1365~5460 6382:6827~10922 6383:12288~16383 6387: 0~1364;5461~6826;10923~12287 槽号分配说明: 为什么6387是3个新的区间,以前的旧节点还是连续槽位? 重新分配成本太高,所以前3家各匀出来一部分,从 6381/6382/6383三个旧节点分别匀出1364个坑位给新节点6387为主节点6387分配从节点6388
redis-cli --cluster add-node IP:从机端口 IP:主机端口 --cluster-slave --cluster-master-id 主机ID redis-cli --cluster add-node 192.168.150.130:6388 192.168.150.130:6387 --cluster-slave --cluster-master-id 1570427d5a692a5a3dfe45461470fd95aa407725检查集群情况第三次
检查的时候,我们去任意一个master节点都可以,下面我们进去6382 redis-cli --cluster check 192.168.150.130:63824、主从缩容案例
检查集群情况,获得6388的节点id
redis-cli --cluster check 192.168.150.130:6381 可获取6388ID: e6c0fcd4c7b39497efc1efeeeb370257344297d3从集群中将4号从节点6388删除
redis-cli --cluster del-node IP:从机端口 从机6388节点ID redis-cli --cluster del-node 192.168.150.130:6388 e6c0fcd4c7b39497efc1efeeeb370257344297d3将6387的槽位号清空,重新分配,本例将清出来的槽位号都给6381
这里的端口号6381代表四台master机器以6381为落脚点去操作集群, redis-cli --cluster reshard 192.168.150.130:6381检查集群情况第二次
redis-cli --cluster check 192.168.150.130:6381将6387删除
redis-cli --cluster del-node ip:主机4端口 节点ID redis-cli --cluster del-node 192.168.150.130:6387 1570427d5a692a5a3dfe45461470fd95aa407725检查情况第三次
redis-cli --cluster check 192.168.150.130:6381