学习目标:
弄懂什么是Redis哈希槽分区?
学习内容:
面试题:
1、在高并发的互联网公司中,有1亿条数据需要缓存,请问如何设计存储这批数据?
单台服务器肯定存储不了这么大的数据,一般是分布试存储,就像数据库分库分表一样存储,那么针对缓存redis如何分布式存储这么大的数据?
方案一:哈希取余分区
针对Redis来说1亿条数据,一般对应1亿个key-value,我们把它们分别存储在N个节点,如上图N=3,然后用户每次读写操作,根据节点N使用公式hash(key)%N计算出hash值,用来决定数据映射在哪个节点上。
这种方案的
优点:简单粗暴,只要提前预估好数据量,然后规划好节点,例如3台、30台、300台节点,就能保证未来一段时间内的数据支撑。
缺点:在扩容或收缩节点时比较麻烦,因为每次节点变动数据节点映射关系需要重新计算,导致数据重新迁移。例如由原来3台,扩展到8台,那么所有数据就得按照hash(key)%8重新洗一遍很麻烦。
方案二:一致性哈希分区
在1997年,麻省理工院Karger等人提出了一致性哈希算法,为的就是解决分布式缓存的问题。一致性哈希算法基本原理,大概需要3个步骤来解释:构造一致性哈希环、IP节点映射、路由规则。
步骤1:构造一致性哈希环
一致性哈希算法首先有一个哈希函数,哈希函数产生hash值,所有可能的hash值构成一个哈希空间,哈希空间为
[0,2^32-1],这本来是一个“线性”的空间,但是在算法中通过恰当逻辑控制,使其首尾相衔接,也即是0=2^32,这样就构造一个逻辑上的环形空间。
步骤2:节点映射
将集群中的各个IP节点映射到环上的某一个位置。
假设有四个节点Node A、B、C、D,经过ip地址.的哈希函数计算(例如:hash(192.168.1.13)),它们的位置如下图:
步骤3:路由规则
路由规则包括存储(setX)和取值(getX)规则。
当需要存储一个<key-value>对时,首先计算key的hash值:hash(key),这个值必然对应于一致性hash环上的某个位置,然后沿着这个值按顺时针找到第一个节点,并将该键值对存储在该节点上。
例如:有4个存储对象ObjectA、B、C、D,它们的位置如下图,对于各个Object,它所真正的存储位置是顺时针找到的第一个存储节点。
例如:ObjectA顺时针找到的第一个节点时NodeA,所以NodeA负责存储ObjectA。
一致性哈希如何实现容错性和扩张性
容错性
假设NodeC节点挂掉了,ObjectC的存储丢失,如果重新把数据补回来时,ObjectC就会顺时针找到最新的节点NodeD。也就是说NodeC挂掉了,受影响仅仅包括NodeB到NodeC区间的数据,并且这些数据会迁移到NodeD进行存储。
扩展性
假设现在数据量大了,需要增加一台节点NodeX。NodeX位于NodeA到NodeB之间,那么受到影响的仅仅是NodeA到NodeX间的数据,重新把A到X之间的数据洗到NodeX上即可。
优点:与哈希取余分区相比,容错性和扩展性更灵活,例如NodeC瘫痪,只影响NodeB到NodeC区间的数据,影响面小;再比如加一台节点NodeX,只影响NodeA到NodeX之间的数据,不会导致哈希取余全部重洗。
缺点:数据负载不均衡(或者说数据倾斜不一致)
若分片的集群中,节点太少,并且分布不匀,一致性哈希算法就会出现部分节点数据太多,部分节点数据太少。也就是说无法控制节点存储数据的分配。大部分数据都在A上了,B的数据比较少。如下图:
方案三:哈希槽分区
为什么会有哈希槽这个概念?
为了解决一致性哈希分区存在数据负载不均衡问题,而引入的概念。
什么是哈希槽?
哈希槽其实就是一个数组,数组[0,1,2,3,......2^14-1]形成hash slot空间,如图所示:
把哈希槽均匀分段,分配给redis节点:
1、redis节点1负责存储5461个哈希槽的数据,编号0号至5460号哈希槽。
2、redis节点2负责存储5462个哈希槽的数据,编号5461号至10922号哈希槽。
3、redis节点3负责存储5461个哈希槽的数据,编号10923号至16383号哈希槽。
如下图所示:
怎样计算每条数据的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","Object1");
redistemplate.opsForValue().set("B","Object2");
redistemplate.opsForValue().set("C","Object3");
然后计算keyA 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
故A、B、C存储分布如图所示:
Redis哈希槽分区的特点:
1、解耦数据和节点之间的关系,例如数据的读写只要计算出槽号就行了,节点扩容和收缩只要重新均匀分配槽区即可;故简化了节点扩容和收缩的难度。
2、节点自身维护槽的映射关系,不需要客户端(Spring)或者代理服务维护槽分区和数据。
3、支持节点、槽、键之间的映射查询,用于路由、在线伸缩等场景。
总结:
方案一二都是数据模型如下:
方案三的数据模型如下:
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。