JAVA面试题分享五百六十五:为啥Redis用哈希槽,不用一致性哈希?

首先,从使用hash取模数据分片开始说起

无论是哈希槽,还是一致性hash,都属于hash取模数据分片。

先从经典的hash取模数据分片说起

假如 Redis集群的节点数为3个,使用经典的hash取模算法进行数据分片,实际上就是一个节点一个数据分片,分为3片而已。

每次请求使用 hash(key) % 3 的方式计算对应的节点,或者进行 分片的路由。

经典哈希取模分片的问题和对策:

哈希取模分片有一个核心问题:对扩容不友好,扩容的时候数据迁移规模太大。

比如,把节点从3个扩展到4个, 具体如下:

原来的分片路由算法是:hash(key) % 3

现在的分片路由算法是:hash(key) % 4

分片路由算法调整之后,那么,大量的key需要进行节点的迁移。

换句话,即当增加或减少节点时,原来节点中的80%以上的数据,可能会进行迁移操作,对所有数据重新进行分布。

如何应对呢?

规避的措施之一:如果一定要采用哈希取模分片,建议使用多倍扩容的方式,这样只需要适移50%的数据。例如以前用3个节点保存数据,扩容为比以前多一倍的节点即6个节点来保存数据,这样移动50%的数据即可。

规避的措施之一:采用一致性hash分片方法。

图片

哈希取模分片优点:

  • 配置简单:对数据进行哈希,然后取余

哈希取模分片缺点:

  • 数据节点伸缩时,导致大量数据迁移

  • 迁移数量和添加节点数据有关,建议翻倍扩容

一致性hash算法

如果redis使用一致性hash算法进行数据分片,那么核心会涉及到的两个阶段:

  • 第一阶段,需要完成key到slot槽位之间的映射。

  • 第二阶段,需要完成slot槽位到 redis node节点之间的映射。

首先看第一阶段。

第一阶段,需要完成key到slot槽位之间的映射

第一阶段,使用了哈希取模的方式,不同的是:  对 2^32 这个固定的值进行取模运算。

注意,这里的取模的除数,是 2^32 , 相当于  2^32个槽位, 英文是 slot 。

通过这个槽位的计算,可以确定 key => slot 之间的映射关系。

第二阶段,需要完成slot槽位到 redis node节点之间的映射。

第二阶段,需要完成slot槽位到 redis node节点之间的映射。

如何完成 slot  槽位到node 节点之间的映射呢?

这里,需要 采用一种特殊的结构:Hash槽位环。

Hash槽位环

把一致哈希算法是对 2^32  slot 槽位虚拟成一个圆环,环上的对应 0~2^32 刻度,

如何完成 slot  槽位到node 节点之间的映射呢?

假设有4个redis 节点, 可以把 2^32  slot 槽位环分成4段, 每一个redis 节点负责存储一个slot分段

如何对每一个key进行node 路由呢?

第一步 进行slot槽位计算:每一个key进行hash运算,被哈希后的结果 2^32  取模,获得slot 槽位、

第二步 在hash槽位环上,按顺时针去找最近的redis节点,这个key将会被保存在这个节点上。

一致性哈希原理:

将所有的数据用hash取模, 映射到 2^32个槽位。

把2^32个槽位 当做一个环,把N个redis 节点瞬时间放置在槽位环上,从而把槽位环分成N段,每redis 节点负责一个分段。

当key在槽位环上路由的时候,按顺时针去找最近的redis节点,这个key将会被保存在这个节点上。

来看一致性哈希 三个经典场景:

经典场景1:Key入环

下图我们四个key(Key1/Key2/Key3)经过哈希计算,放入下面环中,第一步是进行hash计算,取模后得到slot槽位。

找到了slot槽位,相当于已经成功映射到哈希环上,

然后将槽位按顺时针方向,找到离自己最近的redis节点上,将value存储到那个节点上。

经典场景2:新增redis节点

现在,需要对redis 节点进行扩容, 在redis1 和 redis2之间,新增加点redis 5。

具体的操作是:在hash槽位环上,把redis 5节点放置进去

添加了新节点之后,对所有的redis 2上的数据,进行重新的检查。

如果redis 2上的数据,顺时针方式最近的新节点不是redis 2而是 redis 5的话,需要进行迁移,迁移到redis 5。

比如,上图的key2,需要从redis 2迁移 redis 5。

而其他节点上的数据,不受影响。比如redis1、redis3、redis4上的数据不受影响。上图中,key1和key3不受影响

经典场景3:删除redis节点

假设,删除hash环上的节点redis 2

那么存储在redis 2节点上的key2,将会重新映射找到离它最近的节点redis3,如下图

另外,key1、key3不受影响。

经典哈希取模与一致性hash的对比:

前面讲到,假设Redis 集群使用经典哈希取模分片, 缺点是在数据节点伸缩时,导致大量数据迁移:

  • 最少50%的数据要迁移,这个是在翻倍扩容场景

  • 一般有80%以上的数据要迁移。

假设Redis 集群使用一致性哈希取模分片, 通过上面的一致性哈希取模新增节点、一致性哈希取模删除节点的分析之后, 可以得到:

  • 一致性hash在伸缩的时候, 需要迁移的数据不到25%(假设4个节点)。

  • 和普通hash取模分片相比, 一致性哈希取模分片需要 迁移的数据规模缩小2倍以上。

一致性hash的数据不平衡(数据倾斜)问题

标准的一致性hash,存在一个大的问题:数据不平衡(数据倾斜)问题。

回顾一下,一致性hash算法的两个阶段:

  • 第一阶段,需要完成key到slot槽位之间的映射。

  • 第二阶段,需要完成slot槽位到 redis node节点之间的映射。

在这个两阶段中,数据不平衡(数据倾斜)问题的来源在第二阶段:

  • 第一个阶段,hash算法是均匀的。

  • 第二个阶段,如果某个节点宕机,那么就会出现节点的不平衡。

迁移之后,发生了 严重的数据倾斜,或者不平衡。Redis 3上4个key,而redis 1、redis 4上只有1个key。

这样,redis2 上的数量很多,此时会导致节点压力陡增。

旱涝不均。

那如何解决这个旱涝不均问题呢?答案是通过 虚拟节点

什么是 虚拟节点?

虚拟节点 可以理解为逻辑节点,不是物理节点。 假设在hash环上,引入 32 个虚拟 reids节点。

如何找到物理节点呢? 办法是增加一次映射:虚拟节点到物理节点的映射。

假设加上一层  32 个虚拟 redis节点到 4个  redis 物理节点映射。一种非常简单的map参考映射方案

假设物理节点 redis 3被移除,那么,把redis 3负责的逻辑节点,二次分配到其他三个物理节点就行了

无论如何,通过虚拟节点,就会大大减少了 一致性hash 算法的数据倾斜/数据不平衡。

一致性hash的简易实现

可以使用TreeMap 来实现一致性hash,原因有二:

  • TreeMap的key是有序,

  • 使用TreeMap的ceilingEntry(K k) 方法,可以返回大于或等于给定参数K的键, 这就是映射到的节点。

TreeMap是一个小顶堆,默认是根据key的自然排序来组织(比如integer的大小,String的字典排序)。底层是根据红黑树的数据结构构建的。

这里使用TreeMap的ceilingEntry(K key) 方法,该方法用来返回与该键至少大于或等于给定键,如果不存在这样的键的键 - 值映射,则返回null相关联。

一致性hash的简易实现,参考代码如下:

package com.th.treemap;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;

public class ConsistentHash {
    /**
     * 假设我们一共初始化有8个节点(可以是ip, 就理解为ip吧);
     * 把 1024个虚拟节点跟 8个资源节点相对应
     */
    public static Map<Integer, String> nodeMap = new HashMap<>();
    public static int V_redisS = 1024; // 假设我们的环
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

之乎者也·

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值