前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >一文讲透一致性哈希的原理和实现

一文讲透一致性哈希的原理和实现

作者头像
玖柒的小窝
发布于 2021-12-02 16:55:44
发布于 2021-12-02 16:55:44
58200
代码可运行
举报
文章被收录于专栏:各类技术文章~各类技术文章~
运行总次数:0
代码可运行

为什么需要一致性哈希

首先介绍一下什么是哈希

Hash,一般翻译做散列,或音译为哈希,是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。

在分布式缓存服务中,经常需要对服务进行节点添加和删除操作,我们希望的是节点添加和删除操作尽量减少数据-节点之间的映射关系更新。

假如我们使用的是哈希取模( hash(key)%nodes ) 算法作为路由策略:

哈希取模的缺点在于如果有节点的删除和添加操作,对 hash(key)%nodes 结果影响范围太大了,造成大量的请求无法命中从而导致缓存数据被重新加载。

基于上面的缺点提出了一种新的算法:一致性哈希。一致性哈希可以实现节点删除和添加只会影响一小部分数据的映射关系,由于这个特性哈希算法也常常用于各种均衡器中实现系统流量的平滑迁移。

一致性哈希工作原理

首先对节点进行哈希计算,哈希值通常在 2^32-1 范围内。然后将 2^32-1 这个区间首尾连接抽象成一个环并将节点的哈希值映射到环上,当我们要查询 key 的目标节点时,同样的我们对 key 进行哈希计算,然后顺时针查找到的第一个节点就是目标节点。

根据原理我们分析一下节点添加和删除对数据范围的影响。

  1. 节点添加

只会影响新增节点与前一个节点(新增节点逆时针查找的第一个节点)之间的数据。

  1. 节点删除

只会影响删除节点与前一个节点(删除节点逆时针查找的第一个节点)之间的数据。

这样就完了吗?还没有,试想一下假如环上的节点数量非常少,那么非常有可能造成数据分布不平衡,本质上是环上的区间分布粒度太粗。

怎么解决呢?不是粒度太粗吗?那就加入更多的节点,这就引出了一致性哈希的虚拟节点概念,虚拟节点的作用在于让环上的节点区间分布粒度变细。

一个真实节点对应多个虚拟节点,将虚拟节点的哈希值映射到环上,查询 key 的目标节点我们先查询虚拟节点再找到真实节点即可。

代码实现

基于上面的一致性哈希原理,我们可以提炼出一致性哈希的核心功能:

  1. 添加节点
  2. 删除节点
  3. 查询节点

我们来定义一下接口:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ConsistentHash interface {
    Add(node Node)
    Get(key Node) Node
    Remove(node Node)
}
复制代码

现实中不同的节点服务能力因硬件差异可能各不相同,于是我们希望在添加节点时可以指定权重。反应到一致性哈希当中所谓的权重意思就是我们希望 key 的目标节点命中概率比例,一个真实节点的虚拟节点数量多则意味着被命中概率高。

在接口定义中我们可以增加两个方法:支持指定虚拟节点数量添加节点,支持按权重添加。本质上最终都会反应到虚拟节点的数量不同导致概率分布差异。

指定权重时:实际虚拟节点数量 = 配置的虚拟节点 * weight/100

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ConsistentHash interface {
    Add(node Node)
    AddWithReplicas(node Node, replicas int)
    AddWithWeight(node Node, weight int)
    Get(key Node) Node
    Remove(node Node)
}
复制代码

接下来考虑几个工程实现的问题:

  1. 虚拟节点如何存储? 很简单,用列表(切片)存储即可。
  2. 虚拟节点 - 真实节点关系存储 map 即可。
  3. 顺时针查询第一个虚拟节点如何实现 让虚拟节点列表保持有序,二分查找第一个比 hash(key) 大的 index,list[index] 即可。
  4. 虚拟节点哈希时会有很小的概率出现冲突,如何处理呢? 冲突时意味着这一个虚拟节点会对应多个真实节点,map 中 value 存储真实节点数组,查询 key 的目标节点时对 nodes 取模。
  5. 如何生成虚拟节点 基于虚拟节点数量配置 replicas,循环 replicas 次依次追加 i 字节 进行哈希计算。

go-zero 源码解析

core/hash/consistenthash.go

详细注释可查看:github.com/Ouyangan/go…

花了一天时间把 go-zero 源码一致性哈希源码看完,写的真好啊,各种细节都考虑到了。

go-zero 使用的哈希函数MurmurHash3GitHubgithub.com/spaolacci/m…

go-zero 并没有进行接口定义,没啥关系,直接看结构体 ConsistentHash

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// Func defines the hash method.
// 哈希函数
Func func(data []byte) uint64

// A ConsistentHash is a ring hash implementation.
// 一致性哈希
ConsistentHash struct {
    // 哈希函数
    hashFunc Func
    // 确定node的虚拟节点数量
    replicas int
    // 虚拟节点列表
    keys []uint64
    // 虚拟节点到物理节点的映射
    ring map[uint64][]interface{}
    // 物理节点映射,快速判断是否存在node
    nodes map[string]lang.PlaceholderType
    // 读写锁
    lock sync.RWMutex
}
复制代码

key 和虚拟节点的哈希计算

在进行哈希前要先将 key 转换成 string

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 可以理解为确定node字符串值的序列化方法
// 在遇到哈希冲突时需要重新对key进行哈希计算
// 为了减少冲突的概率前面追加了一个质数prime来减小冲突的概率
func innerRepr(v interface{}) string {
   return fmt.Sprintf("%d:%v", prime, v)
}

// 可以理解为确定node字符串值的序列化方法
// 如果让node强制实现String()会不会更好一些?
func repr(node interface{}) string {
   return mapping.Repr(node)
}
复制代码

这里 mapping.Repr 里会判断 fmt.Stringer 接口,如果符合,就会调用其 String 方法。go-zero 代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// Repr returns the string representation of v.
func Repr(v interface{}) string {
    if v == nil {
        return ""
    }

    // if func (v *Type) String() string, we can't use Elem()
    switch vt := v.(type) {
    case fmt.Stringer:
        return vt.String()
    }

    val := reflect.ValueOf(v)
    if val.Kind() == reflect.Ptr && !val.IsNil() {
        val = val.Elem()
    }

    return reprOfValue(val)
}
复制代码

添加节点

最终调用的是 指定虚拟节点添加节点方法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 扩容操作,增加物理节点
func (h *ConsistentHash) Add(node interface{}) {
    h.AddWithReplicas(node, h.replicas)
}
复制代码
添加节点 - 指定权重

最终调用的同样是 指定虚拟节点添加节点方法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 按权重添加节点
// 通过权重来计算方法因子,最终控制虚拟节点的数量
// 权重越高,虚拟节点数量越多
func (h *ConsistentHash) AddWithWeight(node interface{}, weight int) {
    replicas := h.replicas * weight / TopWeight
    h.AddWithReplicas(node, replicas)
}
复制代码
添加节点 - 指定虚拟节点数量
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 扩容操作,增加物理节点
func (h *ConsistentHash) AddWithReplicas(node interface{}, replicas int) {
    // 支持可重复添加
    // 先执行删除操作
    h.Remove(node)
    // 不能超过放大因子上限
    if replicas > h.replicas {
        replicas = h.replicas
    }
    // node key
    nodeRepr := repr(node)
    h.lock.Lock()
    defer h.lock.Unlock()
    // 添加node map映射
    h.addNode(nodeRepr)
    for i := 0; i < replicas; i++ {
        // 创建虚拟节点
        hash := h.hashFunc([]byte(nodeRepr + strconv.Itoa(i)))
        // 添加虚拟节点
        h.keys = append(h.keys, hash)
        // 映射虚拟节点-真实节点
        // 注意hashFunc可能会出现哈希冲突,所以采用的是追加操作
        // 虚拟节点-真实节点的映射对应的其实是个数组
        // 一个虚拟节点可能对应多个真实节点,当然概率非常小
        h.ring[hash] = append(h.ring[hash], node)
    }
    // 排序
    // 后面会使用二分查找虚拟节点
    sort.Slice(h.keys, func(i, j int) bool {
        return h.keys[i] < h.keys[j]
    })
}
复制代码

删除节点

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 删除物理节点
func (h *ConsistentHash) Remove(node interface{}) {
    // 节点的string
    nodeRepr := repr(node)
    // 并发安全
    h.lock.Lock()
    defer h.lock.Unlock()
    // 节点不存在
    if !h.containsNode(nodeRepr) {
        return
    }
    // 移除虚拟节点映射
    for i := 0; i < h.replicas; i++ {
        // 计算哈希值
        hash := h.hashFunc([]byte(nodeRepr + strconv.Itoa(i)))
        // 二分查找到第一个虚拟节点
        index := sort.Search(len(h.keys), func(i int) bool {
            return h.keys[i] >= hash
        })
        // 切片删除对应的元素
        if index < len(h.keys) && h.keys[index] == hash {
            // 定位到切片index之前的元素
            // 将index之后的元素(index+1)前移覆盖index
            h.keys = append(h.keys[:index], h.keys[index+1:]...)
        }
        // 虚拟节点删除映射
        h.removeRingNode(hash, nodeRepr)
    }
    // 删除真实节点
    h.removeNode(nodeRepr)
}

// 删除虚拟-真实节点映射关系
// hash - 虚拟节点
// nodeRepr - 真实节点
func (h *ConsistentHash) removeRingNode(hash uint64, nodeRepr string) {
    // map使用时应该校验一下
    if nodes, ok := h.ring[hash]; ok {
        // 新建一个空的切片,容量与nodes保持一致
        newNodes := nodes[:0]
        // 遍历nodes
        for _, x := range nodes {
            // 如果序列化值不相同,x是其他节点
            // 不能删除
            if repr(x) != nodeRepr {
                newNodes = append(newNodes, x)
            }
        }
        // 剩余节点不为空则重新绑定映射关系
        if len(newNodes) > 0 {
            h.ring[hash] = newNodes
        } else {
            // 否则删除即可
            delete(h.ring, hash)
        }
    }
}
复制代码

查询节点

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 根据v顺时针找到最近的虚拟节点
// 再通过虚拟节点映射找到真实节点
func (h *ConsistentHash) Get(v interface{}) (interface{}, bool) {
    h.lock.RLock()
    defer h.lock.RUnlock()
    // 当前没有物理节点
    if len(h.ring) == 0 {
        return nil, false
    }
    // 计算哈希值
    hash := h.hashFunc([]byte(repr(v)))
    // 二分查找
    // 因为每次添加节点后虚拟节点都会重新排序
    // 所以查询到的第一个节点就是我们的目标节点
    // 取余则可以实现环形列表效果,顺时针查找节点
    index := sort.Search(len(h.keys), func(i int) bool {
        return h.keys[i] >= hash
    }) % len(h.keys)
    // 虚拟节点->物理节点映射
    nodes := h.ring[h.keys[index]]
    switch len(nodes) {
    // 不存在真实节点
    case 0:
        return nil, false
    // 只有一个真实节点,直接返回
    case 1:
        return nodes[0], true
    // 存在多个真实节点意味这出现哈希冲突
    default:
        // 此时我们对v重新进行哈希计算
        // 对nodes长度取余得到一个新的index
        innerIndex := h.hashFunc([]byte(innerRepr(v)))
        pos := int(innerIndex % uint64(len(nodes)))
        return nodes[pos], true
    }
}
复制代码

项目地址

github.com/zeromicro/g…

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
一致性哈希算法的理解与实践
一致哈希是一种特殊的哈希算法。在使用一致哈希算法后,哈希表槽位数(大小)的改变平均只需要对 K/n个关键字重新映射,其中K是关键字的数量, n是槽位数量。然而在传统的哈希表中,添加或删除一个槽位的几乎需要对所有关键字进行重新映射。
搜云库技术团队
2019/10/17
1.2K0
解密一致性哈希算法:实现高可用和负载均衡的秘诀
在构建大规模分布式系统时,如何分布数据成为一个复杂的问题。一致性哈希算法是一项引人注目的技术,它能够在高负载和故障时保持数据一致性。在这篇博客中,我们将揭开一致性哈希算法的神秘面纱,分享如何利用这一黑科技来构建可靠的分布式系统。
一只牛博
2025/05/30
950
解密一致性哈希算法:实现高可用和负载均衡的秘诀
一文搞懂一致性hash的原理和实现
在 go-zero 的分布式缓存系统分享里,Kevin 重点讲到过一致性hash的原理和分布式缓存中的实践。本文来详细讲讲一致性hash的原理和在 go-zero 中的实现。
不会飞的小鸟
2021/07/20
3980
微服务细剖:一致性hash的原理和实现,面试划重点
所以本质来讲:我们需要一个可以将输入值“压缩”并转成更小的值,这个值通常状况下是唯一、格式极其紧凑的,比如uint64:
Java_老男孩
2021/07/23
7050
一致性哈希算法原理及代码实现「建议收藏」
在介绍一致性哈希之前,首先来看看集群部署可能发生的问题:比如说我现在有5台 Redis 服务器,正常运行了很久,很不巧有一天A服务器崩溃了,这个时候还有4台服务器,系统还可以正常运行,原来发送到A服务器的请求我们肯定要想办法进行重定向吧,如果说我们使用一般的哈希函数进行分配,无疑是 hash(key) % num,不过因为 num 现在变成了 num-1,那么很有可能所有的请求都会发生改变打到不同的服务器上,原来发送到B的请求重新处理之后可能发送到了C服务器了。
全栈程序员站长
2022/09/18
4250
一致性哈希算法原理及代码实现「建议收藏」
一致性哈希初认识
一致哈希算法简单得令人难以置信,但却非常强大,但直到我坐下来亲自研究它之前,我都不太明白它到底是什么。在介绍一致性散列之前,你需要先了解我们要解决的问题:
Lemon黄
2023/11/20
1610
一致性哈希初认识
Dubbo 的负载均衡策略:一致性哈希策略
简单描述一下一致性哈希,如下图所示。假设有 8 ,组成下面 A - H 的 8 个虚拟节点,A - H 哈希值分别是递增的,当有一个请求进来,通过计算参数的哈希值,比如该值刚好是 1 的位置,那么选中比 1 的值大的最小的那个实例,则结果该请求选中 C 虚拟节点;假如请求的参数的哈希值是在 2 的位置,那么找不到比 2 的值大的最小的实例,则选中所有实例中最小的一个,也就是 A。
LieBrother
2019/04/02
7750
Dubbo 的负载均衡策略:一致性哈希策略
一文搞懂一致性hash的原理和实现
在 go-zero 的分布式缓存系统分享里,Kevin 重点讲到过一致性hash的原理和分布式缓存中的实践。本文来详细讲讲一致性hash的原理和在 go-zero 中的实现。
梦醒人间
2021/11/30
3830
一文搞懂一致性hash的原理和实现
什么是一致性哈希算法
原文:http://www.cnblogs.com/hapjin/p/4737207.html
Bug开发工程师
2018/07/23
5310
什么是一致性哈希算法
分布式系统设计理论之一致性哈希
例如系统需要构建分布式缓存,多个节点分别部署而形成的一个分布式集群,当有请求到来时进行负载均衡,具体的负载均衡方式就是将节点的ID(唯一标识)进行哈希值的取余运算,得到结果的机器就是进行请求处理的机器。
闫同学
2023/11/01
2250
一致性 Hash 算法原理总结
作者:kylinkzhang,腾讯 CSIG 后台开发工程师 一致性 Hash 算法是解决分布式缓存等问题的一种算法,本文介绍了一致性 Hash 算法的原理,并给出了一种实现和实际运用的案例; 一致性 Hash 算法背景 考虑这么一种场景: 我们有三台缓存服务器编号node0、node1、node2,现在有 3000 万个key,希望可以将这些个 key 均匀的缓存到三台机器上,你会想到什么方案呢? 我们可能首先想到的方案是:取模算法hash(key)% N,即:对 key 进行 hash 运算后取模,N
腾讯技术工程官方号
2022/03/14
1.7K0
一致性哈希的分析与实现
哈希函数,想必大家都不陌生。通过哈希函数我们可以将数据映射成一个数字(哈希值),然后可用于将数据打乱。例如,在HashMap中则是通过哈希函数使得每个桶中的数据尽量均匀。那一致性哈希又是什么?它是用于解决什么问题?本文将从普通的哈希函数说起,看看普通哈希函数存在的问题,然后再看一致性哈希是如何解决,一步步进行分析,并结合代码实现来讲解。
Java技术江湖
2020/07/23
4760
一致性哈希的分析与实现
图解一致性哈希算法,看这一篇就够了!
近段时间一直在总结分布式系统架构常见的算法。前面我们介绍过布隆过滤器算法。接下来介绍一个非常重要、也非常实用的算法:一致性哈希算法。通过介绍一致性哈希算法的原理并给出了一种实现和实际运用的案例,带大家真正理解一致性哈希算法。
章为忠学架构
2023/03/23
7.4K2
图解一致性哈希算法,看这一篇就够了!
分布式基石算法 一致性hash
分布式系统中, 一致性hash无处不在,CDN,KV,负载均衡等地方都有它的影子,是分布式系统的基石算法之一。一致性hash 有以下几个优点。
萝卜要努力
2025/03/07
1270
分布式基石算法 一致性hash
漫画:什么是一致性哈希?
按Mysql单表存储500万条记录来算,暂时不必分库,单库30个分表是比较合适的水平分表方案。
用户5927304
2019/07/31
6720
一致性hash原理与实现
一、背景介绍 memcached的分布式 memcached虽然称为“分布式”缓存服务器,但服务器端并没有“分布式”功能。服务器端内存存储功能,其实现非常简单。至于memcached的分布式,则是完全由客户端程序库实现的。这种分布式是memcached的最大特点。分布的原则是由client端的api来决定的,api根据存储用的key以及已知的服务器列表,根据key的hash计算将指定的key存储到对应的服务器列表上。 memcached的分布式是什么意思? 这里多次使用了“分布式”这个词,但并未做详细解释。
小小科
2018/05/04
8080
一致性hash原理与实现
一致性hash算法原理及实践
今天我们就来看看工作和面试中经常被点名的算法,一致性hash算法,并且我会介绍它在实际的应用场景并用代码实现出来。
蓝胖子的编程梦
2023/06/21
2440
一致性hash算法原理及实践
pymemcached框架之一致性哈希算法实现
由于memcached本身没有提供集群的功能,也就是说每个memcached节点是相互独立的,对于多节点的memcached,数据的读写,都是通过客户端自己来实现的,比如有的就通过一致性hash来寻址memcached节点,从而操作其数据。
tunsuy
2022/10/27
1950
Redis扩容机制与一致性哈希算法解析
在分布式系统设计中,Redis是一个备受欢迎的内存数据库,而一致性哈希算法则是分布式系统中常用的数据分片和负载均衡技术。本文将深入探讨Redis的扩容机制以及一致性哈希算法的原理,同时提供示例代码以帮助读者更好地理解这两个重要概念。
疯狂的KK
2023/08/23
1.3K0
Redis扩容机制与一致性哈希算法解析
分布式数据缓存中的一致性哈希算法
一致性哈希算法在分布式缓存领域的 MemCached,负载均衡领域的 Nginx 以及各类 RPC 框架中都有广泛的应用,它主要是为了解决传统哈希函数添加哈希表槽位数后要将关键字重新映射的问题。
程序员历小冰
2019/05/13
9570
分布式数据缓存中的一致性哈希算法
相关推荐
一致性哈希算法的理解与实践
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验