哈希算法常用的有哪些(一文讲透一致性哈希的原理和实现)
哈希算法常用的有哪些(一文讲透一致性哈希的原理和实现)
2024-07-03 09:58:47  作者:打击婚外恋  网址:https://m.xinb2b.cn/sport/dha204328.html
为什么需要一致性哈希

首先介绍一下什么是哈希

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

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

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

哈希算法常用的有哪些(一文讲透一致性哈希的原理和实现)(1)

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

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

一致性哈希工作原理

哈希算法常用的有哪些(一文讲透一致性哈希的原理和实现)(2)

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

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

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

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

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

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

哈希算法常用的有哪些(一文讲透一致性哈希的原理和实现)(3)

代码实现

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

添加节点删除节点查询节点

我们来定义一下接口:

ConsistentHash interface { Add(node Node) Get(key Node) Node Remove(node Node)}

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

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

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

ConsistentHash interface { Add(node Node) AddWithReplicas(node Node, replicas int) AddWithWeight(node Node, weight int) Get(key Node) Node Remove(node Node)}

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

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

core/hash/consistenthash.go

详细注释可查看:https://github.com/Ouyangan/go-zero-annotation/blob/84ae351e4ebce558e082d54f4605acf750f5d285/core/hash/consistenthash.go

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

go-zero 使用的哈希函数是 MurmurHash3,GitHub:https://github.com/spaolacci/murmur3

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

// 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

// 可以理解为确定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 代码如下:

// 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)}

添加节点

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

// 扩容操作,增加物理节点func (h *ConsistentHash) Add(node interface{}) { h.AddWithReplicas(node, h.replicas)}

添加节点 - 指定权重

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

// 按权重添加节点// 通过权重来计算方法因子,最终控制虚拟节点的数量// 权重越高,虚拟节点数量越多func (h *ConsistentHash) AddWithWeight(node interface{}, weight int) { replicas := h.replicas * weight / TopWeight h.AddWithReplicas(node, replicas)}

添加节点 - 指定虚拟节点数量

// 扩容操作,增加物理节点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] })}

删除节点

// 删除物理节点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) } }}

查询节点

// 根据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 }}

项目地址

https://github.com/zeromicro/go-zero

欢迎使用 go-zero 并 star 支持我们!

微信交流群

关注『微服务实践』公众号并点击 交流群 获取社区群二维码。

  • 平行线的性质定理是什么(平行线的性质定理有哪些)
  • 2024-07-04平行线的性质定理有哪些平行线的性质定理,即存在两条平行直线的图形中所具有的性质,共有三条:两条平行线被第三条直线所截,同位角相等两条平行线被第三条直线所截,内错角相等两条平行线被第三条直线所截,同旁内角互补这三个结论是平面。
  • 400多和500多的断桥铝有什么区别(300和1500的有什么区别)
  • 2024-07-04300和1500的有什么区别窗户的材质有很多种,包括塑钢窗,铝包木窗,还有断桥铝窗很多人的首选就是断桥铝窗,因为断桥铝不管是隔音,还是保温隔热效果都是杠杠的还记得我家那天去建材市场买断桥铝窗,看到断桥铝窗的差价非常大,便宜的30。
  • 广东湛江菠萝蜜做法(茂名放鸡岛上的菠萝蜜吃法)
  • 2024-07-04茂名放鸡岛上的菠萝蜜吃法菠萝蜜有30多个品种,分为两类:硬肉类和软肉类每年春天开花,夏秋间果熟,成熟时香气飘溢菠萝蜜的价值-当地年轻人约会前会将其当作口香糖咀嚼几口,可以改变口腔异味;另外还有健体益寿的作用此树树龄愈大,果也。
  • 粽子里面放什么豆好吃(粽子是什么)
  • 2024-07-04粽子是什么粽子里面放红豆好吃,红豆会带有淡淡的豆香,而且口感十足但经常包粽子用的豆子多数是用绿豆来做的,因为绿豆比较容易煮熟,而且绿豆煮熟了之后会比较烂定,口感非常有层次感所以会挑选绿豆作为种子的馅料粽子里面放。
  • 清朝提标中营守备是什么官职
  • 2024-07-04清朝提标中营守备是什么官职正五品武官清制,各省提督直辖的绿营官兵,称为“提标”凡提标均置中军参将一员,主掌全标营务参将的副职为游击,游击以下有都司,都司以下就是守备。
  • 汽车由哪几部分组成各部的功用(汽车总体由哪四部分组成)
  • 2024-07-04汽车总体由哪四部分组成(1)发动机发动机是汽车的动力装置其作用是使燃料燃烧产生动力,然后通过底盘的传动系驱动车轮使汽车行驶发动机主要有汽油机和柴油机两种汽油发动机由曲柄连杆机构、配气机构和燃料供给系、冷却系、润滑系、点火系。
  • 固体燃料导弹是如何机动的(号称天使恶魔的导弹燃料)
  • 2024-07-04号称天使恶魔的导弹燃料上文书说到苏联人奋起直追因为苏联人是有紧迫感的,他们承受着美国人的核威胁,但是却没有任何一件趁手的兵器能打到美国本土所以斯大林当时要钱给钱,要人给人集中力量办大事,要完成轰炸机和导弹的突破相反,美国人。
  • 健康行业创业项目有哪些(适合个人投资的大健康项目有哪些)
  • 2024-07-04适合个人投资的大健康项目有哪些  适合个人投资的大健康项目有哪些?跑付招商经理介绍,大健康是当下很火的创业领域,由于人们对健康的追求,大健康产业得到了飞速的发展它围绕人们的生老病死,关注影响健康的各种危险因素和误区,倡导自我健康管。
  • 女生提高跑步水平的方法(女生提高跑步水平的方法分享)
  • 2024-07-04女生提高跑步水平的方法分享解一些跑步术语当你逐渐深入探究跑步,你肯定会碰到一些以前从未听过的专业术语如何避免延迟性肌肉酸痛?怎样进行法特莱克训练法?交叉训练,间歇训练的目的分别是什么?了解它们、掌握它们,并用于你的跑步训练中,。
  • 西安共享电动自行车怎么扫(西安街头又见共享电动自行车)
  • 2024-07-04西安街头又见共享电动自行车近日,记者接到市民反映,在高新四路上存在大量共享电动自行车,而早在2017年10月,由西安市交通管理委员会发布的《西安市鼓励规范互联网租赁自行车发展的指导意见(试行)》(以下简称指导意见)明确,“西安。
  • 进口全自动泳池水处理设备厂家(AQUA爱克泳池水处理设备)
  • 2024-07-04AQUA爱克泳池水处理设备当下,泳池设备市场纷繁更迭,更呼唤品牌的力量在琳琅满目的市场上,人们为了更加高效地做出选择,会更加注重对品牌的选择品牌的力量在于它代表着品质、信誉、价值与可识别性,就像夜晚中璀璨的星光,方便消费者在众。
  • 全民枪神边境王者超稳灵敏度(边境王者刺激开火)
  • 2024-07-04边境王者刺激开火随着近年来的H5游戏的渐渐发展,大家也了解到了H5游戏的独到之处,想丢弃过于庞大的游戏数据包?当遇到一款心动的游戏还需要复杂的下载安装,让人感到头疼?这些问题,H5游戏都能解决今天给大家带来《全民枪神。