问题:1~2亿条数据需要做缓存,请设计一个落地方案。
对于亿级数据的存储来说,单机肯定不可能实现,需要使用到分布式缓存,对于分布式缓存来说,redis集群是一个很好的方案。但是对于集群,又引入一个新的问题,数据需要存和取都在同一台机器,那么如何实现存和取都在同一台机器呢?
方案一:
哈希取余算法:
比如需要搭建3台redis集群,通过hash(key)/3来确定数据落在那台机器上,同样取数据也是通过hash(key)/3落在那台机器上取数据。
优点:粗暴简单,直接有效
缺点:一般来说,对于集群环境,都需要集群的扩缩,所以当机器数量变化时候(扩缩容时,或者某台机器挂了),取余数量为机器数量,这样就会导致 /机器数的改变,从而导致取数据落点机器错误,导致无法取出。
方案二:
一致性哈希算法:
为了在节点数目发生变化的时候尽可能少迁移数据,将所有的存储节点(节点例如通过ip地址hash)排列在首尾相接的hash环上,每个key在计算hash后会顺时针找到临近的存储节点存放。而当有节点加入或退出时仅仅影响的是hash环上顺时针相邻的后续节点。
优点:加入或者删除节点只影响相邻的节点,对其他节点无影响
缺点:因为这些节点在哈希环上是不均匀分布,所以在数据存储时候达不到均匀分布的效果,导致数据倾斜。
方案三:
哈希槽算法:
关于哈希槽,实质就是一个大小为[0,2^14-1]的数组,形成的hash slot空间。这样就解决了均匀分配的问题,在数据和节点之间加一层,把这层称为哈希槽(slot),用于管理数据和节点之间的关系,现在就相当于节点上放得是槽,数据放在槽里面。槽解决的是粒度问题,相当于把粒度变大了,便于数据的移动。哈希解决的是映射问题,使用key的哈希值来计算所在的槽,便于数据的分配。
一个集群只能有16384个槽,编号0-16383(0-2^14-1)。注:关于为什么是16384个,可以参考redis之父对于槽的回答https://github.com/redis/redis/issues/2576https://github.com/redis/redis/issues/2576
这些槽会分配给集群中的所有主节点,分配策略没有要求。可以指定哪些编号的槽分配给哪个主节点。集群会记录节点和槽的对应关系。解决了节点和槽的关系后,接下来就需要对key求哈希值,然后对16384取余,余数是几key就落入对应的槽里。slot = CRC16(key) % 16384。以槽为单位移动数据,因为槽的数目是固定的,处理起来比较容易,这样数据移动问题就解决了。
3主3从集群搭建
首先启动6个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
参数说明
随便进入一个redis容器,使用命令搭建集群环境
#你的ip+端口
redis-cli --cluster create 192.168.111.147:6381 192.168.111.147:6382 192.168.111.147:6383 192.168.111.147:6384 192.168.111.147:6385 192.168.111.147:6386 --cluster-replicas 1
注:这里笔者踩了两个坑,因为笔者用的是腾讯云,没有开发相关端口,导致这行命令连接超时,只需要开启端口后,再次执行这个命令即可进入下一步。
到了这一步只需要执行yes即可完成redis节点的3主3从搭配,但是这一步笔者又超时了,具体原因在于redis的集群搭建中,一个端口是客户端连接使用,即我们docker run的时候设置的端口,还有一个是redis节点间相互通信的内部总线端口,此端口比我们设置的大10000,所以再开发这些端口就成功了。
查看集群状态
集群搭建完毕后,尝试在集群中写入数据,发现有些可以写入有些不可以。(原因在于目前环境是集群环境,而我是通过 redis-cli -p 6381进入的第一台机器,因为第3台主机器存入的key值为0-5460,5461-10922,10923-16383,而当slot=CRC16(key)%16384不在这一台机器范围时,存入就失败)
进入redis cli端命令修改,增加-c 防止路由失效
查看集群情况
redis-cli --cluster check ip:端口
注意:当master 故障后,对应的slave会成为新的master。当故障的master恢复后,会成为新的slave。这里就不演示了
集群的扩容
新建两台redis容器,具体指定看上面的redis容器操作。然后将第一台redis容器加入集群后,检查集群情况
//第一个ip端口为新容器的,后一个为集群环境的
redis-cli --cluster add-node ip:端口 ip:端口
//检查集群情况
redis-cli --cluster check ip:端口
将新的容器加入集群后,发现槽位为0,所以需要分配槽位。redis-cli --cluster reshard IP地址:端口号
重新检查集群情况
redis-cli --cluster check 真实ip地址:6381
发现分配的槽位并非是重新平均分配,而是从之前的3个master上分配过来。这样就可以避免重新分配导致成本过高。槽位分配后,再将slave绑定到第四个master即可。然后检查集群状况
redis-cli --cluster add-node ip:新slave端口 ip:新master端口 --cluster-slave --cluster-master-id 新主机节点ID
redis-cli --cluster check ip:端口
绑定成功
集群的缩容
首先缩容涉及节点的删除,所以需要先删除从节点再删除主节点,当节点删除后,需要再重新分配槽位。
//第一步检查集群状态,获取6388的node id
redis-cli --cluster check ip:端口
//第二步删除6388 node节点
redis-cli --cluster del-node ip:端口 node id
//第三步再次检查集群状态,查看是否删除成功
redis-cli --cluster check ip:端口
只剩下7台机器,4个master,3个slave。然后将第四个master槽位分配给第一个master。
redis-cli --cluster reshard ip:端口
删除6387
redis-cli --cluster del-node ip:端口 6387节点ID
再次检查后,节点缩容为3主3从
至此docker之redis集群3主3从完结