[GoCache] 一致性哈希

3 篇文章 0 订阅
3 篇文章 0 订阅

一致性哈希

1 为什么要用一致性哈希?

1.1 如何选择节点

对于分布式缓存来说,当一个节点接收到请求时,如果该节点没有存储缓存值,它面临的问题是:该从哪个节点获取数据?

假设现在存在 10 个分布式节点,当第一个节点接收到请求时,随机选择一个节点,由该节点从数据源获取数据,该节点从数据源获取数据的同时缓存该数据。当第二次接收到同样的key时,只有1/10的概率选择同一个节点,有9/10的概率命中其他节点,这就意味着要再次从数据源获取数据。这样做,一是缓存效率低;二是各个节点上存储着重复数据,浪费了大量的存储空间。

如何能够对于给定的key,每一次都选择同一个节点呢?使用hash算法也许可以。

那么怎么设计hash函数呢?
可以将key的每一个字符的ASCII码加起来,再对节点个数取模得到hash值。这样的确可以解决上述问题。

1.2 节点数量变化

简单设计函数计算hash值可以解决缓存性能的问题,但当节点数量发生变化时,所有的缓存值对应的节点都会发生改变,即几乎所有的缓存值都失效了。节点在接收到对应的请求时,都需要重新从数据源获取数据,容易引起缓存雪崩

缓存雪崩:缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。常因为缓存服务器宕机,或缓存设置了相同的过期时间引起。

如何解决这个问题呢?一致性哈希可以。

2. 一致性哈希原理

一致性哈希算法将key映射到2^32的空间中,将这个空间首尾相连,形成一个环。

  • 计算节点/机器的哈希值,放置在环上。
  • 计算key的哈希值,放置在环上,顺时针找到的第一个节点,就是该key应选取的节点。

    一致性哈希在增加/删除节点时,只需要重新定位该节点附近的一小部分数据,而不需要重新定位所有的节点,这就解决了上述的问题。

3. 数据倾斜问题

如果服务器的节点过少,容易导致大量的数据集中在某个节点上,负载不均,这就是数据倾斜问题。
为了解决这个问题,引入了虚拟节点的概念,一个真实节点对应多个虚拟节点。

  • 第一步,计算虚拟节点的 Hash 值,放置在环上。
  • 第二步,计算 keyHash 值,在环上顺时针寻找到应选取的虚拟节点,例如是 peer2-1,那么就对应真实节点 peer2

虚拟节点扩充了节点的数量,解决了节点较少的情况下数据容易倾斜的问题。而且代价非常小,只需要增加一个字典(map)维护真实节点与虚拟节点的映射关系即可。

package consistenthash

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

type Hash func(data []byte) uint32

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

// New 允许自定义虚拟节点倍数及hash函数
func New(replicas int, fn Hash) *Map {
	m := &Map{
		replicas: replicas,
		hash:     fn,
		hashMap:  make(map[int]string),
	}

	if m.hash == nil {
		m.hash = crc32.ChecksumIEEE
	}
	return m
}

// Add 添加真实节点/机器
func (m *Map) Add(peers ...string) {
	// 对于每一个真实节点,生成对应的虚拟节点
	for _, key := range peers {
		for i := 0; i < m.replicas; i++ {
			// hash值根据 序号+真实节点的key 生成
			hash := int(m.hash([]byte(strconv.Itoa(i) + key)))
			// 加入环
			m.keys = append(m.keys, hash)
			// 记录映射关系
			m.hashMap[hash] = key
		}
	}
	sort.Ints(m.keys)
}

// Get 选择节点:根据传入的值key计算hash,获取处理请求的虚拟节点
func (m *Map) Get(key string) string {
	if len(m.keys) == 0 {
		return ""
	}
	// 计算数据key的hash值
	hash := int(m.hash([]byte(key)))
	// 二分查找 顺时针寻找第一个匹配的节点位置
	idx := sort.Search(len(m.keys), func(i int) bool {
		return m.keys[i] >= hash
	})
	// 返回当前节点对应的真实节点
	// 因为keys结构是个环,所以这里对idx取模更加严谨
	// 实际上idx范围是[0, len(m.keys)),并不会溢出
	return m.hashMap[m.keys[idx%len(m.keys)]]
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值