一致性哈希

一致性哈希

场景概述

在实现分布式缓存时,我们一般都会部署机台缓存服务器,用来缓存数据,我们这里使用三台缓存服务器,并且给他们编号为0,1,2。

我们使用这些缓存服务器缓存图片资源,假设有三万张图片需要缓存,那我们肯定是希望它能均匀的存放在三台服务器上,这样每台服务器的压力也不会很大,也满足了我们分布式存储的需求。

那我们该怎么来分配这些图片,是每台服务器都均匀放一万张图片?这样子做是可以的,但是会存在问题,比如我们想获取某一张图片时,并不会知道它被我们缓存在了哪个服务器,那么当我们要访问它时,只能通过遍历的方式去访问三个服务器,找到这个图片。但这样的方式耗费的时间肯定是比较长的,咱们之所以使用缓存,目的就是提升速度,减轻服务器压力,那这种方式肯定无法接受。

那对于这种场景,我们该如何解决?

我们可能会想到用哈希算法,因为对同一个key进行哈希计算,每次计算的结果都一定是相同的,我们可以依据这个来定位到具体的服务器
就比如我们可以使用图片名称作为key,进行哈希,然后使用取模的方法来选择服务器,公式为:hash(key) % 3
因为对3取模,最终结果一定是0,1,2中的其中一个,刚好对应了服务器的编号,这样就能根据图片名称来查找对应存放的服务器,当需要图片时直接找到对应服务器,再去请求,这样就大大减少了时间,提高了速度
流程如下图:
在这里插入图片描述

嗯,这个方案看起来很不错,但是也还是存在问题。

虽然上述方案解决了缓存性能问题,但是如果服务器节点数量变了怎么搞?

这种服务器节点的增加与减少都是比较常见的,比如将服务器新增一个,现在变成了四个,那哈希算法的被除数就变为了4,那得出的结果就是0,1,2,3,这样带来的后果就是几乎所有缓存失效。

所有缓存失效了,所有请求到来时都需要重新去DB获取数据,就可能造成瞬时DB请求量大,从而带来缓存雪崩。

同样的,服务器节点减少一个,变成2个,也是会出现类似问题,为了解决这些问题,就诞生了一致性哈希算法。

一致性哈希算法

其实一致性哈希算法也是使用取模的方式,只不过是对2^32取模,而不再是对服务器数量取模。
我们可以将对2^32想象为一个圆环,类似于钟表,只不过这个环有2^32个点:
在这里插入图片描述

一致性哈希会做两步哈希:

  1. 对服务器节点(一般是节点名称+ip+编号等)进行哈希计算,将哈希值放置在哈希环上
  2. 计算资源key(一般是资源名称等)的哈希值,也同样放在哈希环上,并且在该位置上顺时针查找到第一个服务器节点,就将该资源存在这个服务器上

比如,有三个服务器0,1,2,资源A,B,C,D,分布如下:
在这里插入图片描述

那么资源A将会缓存到服务器0上,资源B会缓存到服务器1,资源C和资源D会缓存到服务器2

接着我们来看看一致性哈希会不会出现上述方案的缓存雪崩等问题:
假设上图中服务器1宕机了,我们将它移走,那么受到影响的缓存是资源B,资源B的缓存地方会由服务器1,变为服务器2(顺时针查找),而其他资源是没有受到影响的。
同样的,在资源C和资源D中间加入一个服务器3,受影响的也只有资源C,他将从服务器2缓存改变到服务器3
在这里插入图片描述

因此,可以得出结论,使用一致性哈希时,如果服务器的数量发生变化,并不会所有缓存都失效,而是只会部分失效,这样不至于导致瞬时请求过大压垮服务器

一致性哈希的倾斜

上述的一致性哈希就没有问题了吗?
不见得。虽然示意图是这样画,但那是我们的理想状况,所有服务器节点分布都比较均匀,但一致性哈希算法并不能保证节点能均匀分布,这样就会导致一个问题,可能会所有的资源都集中在某个节点上,导致大量请求集中在某个节点
在这里插入图片描述

如图所示,所有服务器节点分布集中在右侧,资源集中在左侧,这样就会导致所有资源都缓存在服务器0,万一这个服务器0宕机了,那缓存失效也会达到了最大。
这种就是哈希倾斜。

所以一致性哈希存在分布不均匀的问题

解决方案

为了解决哈希倾斜的问题,一致性哈希算法中使用了虚拟节点来解决这个问题。
所谓虚拟节点,就是我们自己创建的,真实中不存在的,让这些虚拟节点和真实节点做一个映射,一个真实节点对应多个虚拟节点。
这样一来,我们的节点数量就会比较多,整体在哈希环的分布就会趋于均匀,就可以解决哈希倾斜问题了。

具体做法就是,不再将真实节点映射到哈希环上了,而是将所有虚拟节点映射在哈希环,并且再构建一个虚拟节点到实际节点的映射关系

比如我们现在给上述三台服务器节点各自设置2个虚拟节点

虚拟节点是没有限制的,如果觉得不够,可以虚拟更多的节点,以减少哈希倾斜所带来的的影响,虚拟节点越多,哈希环上的分布节点救护越多,缓存被均匀分布的改了就会越大

分别为:

  • 服务器0对应:v_0_a, v_0_b
  • 服务器1对应:v_1_a, v_1_b
  • 服务器2对应:v_2_a, v_2_b

在这里插入图片描述

代码实现

分析

根据上面的知识点,我们理下思路,要实现一致性哈希需要以下几点:

  1. 一个哈希环,可以使用切片(Golang语言)
  2. 一个map映射真实节点和虚拟节点,使用map就可以
  3. 一个哈希函数
  4. 节点数量
代码
package consistenthash

import (
	"hash/crc32"
	"sort"
	"strconv"
)

/**
	一致性哈希算法实现
**/

// 哈希算法函数,支持自定义
type Hash func([]byte) uint32

type HashMap struct {
	hash     Hash           // 哈希函数
	replicas int            // 虚拟节点个数
	keys     []int          // 哈希环,排序的
	hashMap  map[int]string // 虚拟节点和真实节点的映射,key为虚拟节点哈希值,value为真实节点名称
}

func New(replicas int, hash Hash) *HashMap {

	// 哈希函数没提供,则默认使用crc32.ChecksumIEEE
	if hash == nil {
		hash = crc32.ChecksumIEEE
	}

	m := &HashMap{
		hash:     hash,
		replicas: replicas,
		hashMap:  make(map[int]string),
	}

	return m
}

// 根据真实节点,生成虚拟节点,哈希等操作
func (m *HashMap) Add(nodes ...string) {
	// 遍历所有真实节点
	for _, node := range nodes {
		// 根据要生成的虚拟节点个数
		for i := 0; i < m.replicas; i++ {
			// 根据i+真实节点的格式,生成虚拟节点哈希值
			hashValue := int(m.hash([]byte(strconv.Itoa(i) + node)))
			// 将节点存入哈希环
			m.keys = append(m.keys, hashValue)
			// 保存好与真实节点映射关系
			m.hashMap[hashValue] = node
		}
	}

	// 对哈希环做排序
	sort.Ints(m.keys)
}

// 计算key的哈希,从哈希环中找到合适的虚拟节点,再找到真实节点
func (m *HashMap) Get(key string) string {
	if len(m.keys) == 0 {
		return ""
	}

	hashValue := int(m.hash([]byte(key)))

	// 通过二分法查找哈希环(递增的),返回的是可以插入的位置,也就是对应的虚拟节点的位置
	index := sort.SearchInts(m.keys, hashValue)

	// 如果是末尾,则找回第一个
	if index == len(m.keys) {
		index = 0
	}

	node := m.hashMap[m.keys[index]]

	return node
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值