面试题:在高并发的互联网公司中,有1亿条数据需要缓存,请问如何设计存储这批数据?
答:单台服务器肯定存储不了这么大的数据,一般是分布式存储,就像数据库的分库分表一样存储,那针对缓存redis如何分布式存储这么大的数据?
业界的做法一般有3种:
1、方法一:哈希取余分区
针对redis来说1亿条数据,一般是对应1亿个key value,我们把他分别存储在N个节点,如上图N=3,然后用户每次读写操作,根据节点N使用公式hash(key)%N计算出哈希值,用来决定数据映射到哪一个节点上
用来决定数据映射到哪一个节点上
这种方案的优缺点:
优点:简单粗暴,只要提前预估好数据量,然后规划好节点,例如:3台、30台、300台节点,就能保证未来一段时间内的数据支撑
缺点:节点扩容或收缩节点的时候就麻烦了,因为每次节点有变动数据节点映射关系需要重新计算,会导致数据的重新迁移
例如原先是3台,后面要新增到8台,要把所有的历史数据按hash(key)%8,重新洗一遍,非常麻烦
2、方法二:一致性哈希分区
在1997年,麻省理工学院的Karger等人提出了一致性哈希算法,为的就是解决分布式缓存的问题
一致性哈希算法基本原理大概需要3个步骤来解释:构造一致性哈希环、节点映射、路由规则
步骤1:构造一致性哈希环
一致性哈希算法中首先有一个哈希函数,哈希函数产生hash值,所有可能的哈希值构成一个哈希空间,哈希空间为[0, 2^32-1],这本来是一个“线性”的空间,但是在算法中通过恰当逻辑控制,使其首尾相衔接,也即是0=2^32-1,这样就构造一个逻辑上的环形空间
步骤2:节点映射
将集群中的各IP节点映射到环上的某一个位置
假设有四个节点Node A、B、C、D,经过ip地址的哈希函数计算(例如:hash(192.168.1.13)),它们的位置如下:
步骤3:路由规则
路由规则包括存储(setX)和取值(getX)规则
当需要存储一个<key-value>对时,首先计算键key的hash值:hash(key),这个hash值必然对应于一致性hash环上的某个位置,然后沿着这个值按顺时针找到第一个节点,并将该键值对存储在该节点上
例如有4个存储对象Object A、B、C、D,经过对key的哈希计算后,它们的位置如图
对于各个Object,它所真正的存储位置是按顺时针找到的第一个存储节点
例如Object A顺时针找到的第一个节点是Node A,所以Node A负责存储Object A,Object B存储在Node B
一致性哈希如何实现容错性和扩展性?
容错性
假设Node C节点挂掉了,Object C的存储丢失,如果要重新把数据补回来时,Object C就会顺时针找到的最新节点是Node D
也就是说Node C挂掉了,受影响仅仅包括Node B到Node C区间的数据,并且这些数据会转移到Node D进行存储
扩展性
假设现在数据量大了,需要增加一台节点 Node X,Node X位置在Node A到Node B之间,那么受到影响的仅仅是Node A到Node X间的数据,重新把A到X之间的数据洗到Node X上即可
优缺点
优点:与哈希取余分区相比,容错性和扩展性更灵活,例如Node C瘫痪,只影响Node B到Node C区间的数据,影响面小;
再例如增加一台节点Node X,只影响到Node A到Node B之间的数据,不会导致哈希取余全部数据重洗
缺点:数据倾斜不一致性
如果在分片的集群中,节点太少,并且分布不均,一致性哈希算法就会出现部分节点数据太多,部分节点数据太少。也就是说无法控制节点存储数据的分配。如图,大部分数据都在A上了,B的数据比较少
3、方法三:哈希槽分区
由于一致性哈希分区存在数据倾斜不一致性的问题,故引入了槽的概念
3-1、什么是哈希槽呢?
哈希槽其实就是一个数组,数组[0, 1, 2, ..., 2^14-1]形成hash slot空间
3-2、把哈希槽均匀分段,分配给redis节点
redis节点1,负责存储5461个哈希槽的数据,编号0号至5460号哈希槽
redis节点2,负责存储5462个哈希槽的数据,编号5461号至10922号哈希槽
redis节点3,负责存储5461个哈希槽的数据,编号10923号至16383号哈希槽
3-3、计算每条数据的slot空间位置
将数据key进行哈希取值,映射已经固定大小的hash slot空间上
例如:可以采用spring redis的API
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
例如:我们往redis设置3条数据:
redisTemplate.opsForValue.set("A", "agan1");
redisTemplate.opsForValue.set("B", "agan2");
redisTemplate.opsForValue.set("C", "agan3");
然后计算key,A、B、C的slot槽位置
io.lettuce.core.cluster.SlotHash.getSlot("A")=6373
io.lettuce.core.cluster.SlotHash.getSlot("B")=10374
io.lettuce.core.cluster.SlotHash.getSlot("C")=14503
故,
key A、B落在slot空间的5461至10922区间上,并最终存储在Node 2上
key C落在slot空间的10923至16383区间上,并最终存储在Node 3上
3-4、redis哈希槽分区的特点
1)解耦数据和节点之间的关系,例如:数据的读写只要计算出槽号就可以,节点的扩容和收缩只要重新均衡分配槽区间即可;故简化了节点扩容和收缩难度
2)节点自身维护槽的映射关系,不需要客户端(spring)或者代理服务维护槽分区和数据
3)支持节点、槽、键之间的映射查询,用于数据路由、在线伸缩等场景
数据和槽号是绑在一起的,spring通过槽号找到节点
========================================
一、spring与redis通信
1、建立TCP管道长连接
2、发送
3、结果返回
二、redis哈希槽分区
1、分析:哈希取余分区
2、分析:一致性哈希分区原理
3、分析:哈希槽分区原理
4、redis集群哈希槽部署
5、查看redis哈希槽的数据分布
6、redis集群哈希槽扩容
7、redis集群哈希槽收缩
8、spring集成redis集群哈希槽
三、建立TCP长连接,需要解决以下5个问题(常见的面试题):
1、redis集群3主3从,spring配置文件该配几个ip?配1个还是6个?
2、spring如何知道redis每个节点有多少个槽?
3、spring如何实现redis槽和节点的映射关系?
4、spring如何计算redis哈希槽?
5、有了哈希号,spring如何算出redis集群节点IP?
四、spring采用了netty与redis建立tcp连接后,就是如何进行数据网络通信?
spring与redis在通信方面,技术点采用了netty、lettuce、CompletionStage、commons-pool2技术来实现网络通信
最核心的技术是由于netty是异步通信,发出去和回来的是2条异步线程,为了解决这个难题,lettuce大量采用了CompletionStage把2条异步线程从异步改成同步
五、通信异常处理
1、spring与redis的TCP连接异常断开后,如何重连?
2、发送者Endpoint和CommandHandler的设计原理
3、redis断开重连,新channel如何加入发送者Endpoint?
4、redis的tcp异常断开后,未发送的数据如何存储?
5、如何把tcp异常断开的数据,再次发送出去?
6、什么是redis集群MOVED异常?
7、什么是redis集群ASK异常?
8、当redis集群主从切换、哈希槽迁移,spring如何知道?