golang分布式缓存项目 Day4 一致性哈希

news2024/12/23 13:21:59

注:该项目原作者:https://geektutu.com/post/geecache-day1.html。本文旨在记录本人做该项目时的一些疑惑解答以及部分的测试样例以便于本人复习

为什么使用一致性哈希

我该访问谁

对于分布式缓存来说,当一个节点接收到请求,如果该节点并没有存储缓存值,那么它面临的难题是,从谁那获取数据?自己,还是节点1, 2, 3, 4… 。假设包括自己在内一共有 10 个节点,当一个节点接收到请求时,随机选择一个节点,由该节点从数据源获取数据。

假设第一次随机选取了节点 1 ,节点 1 从数据源获取到数据的同时缓存该数据;那第二次,只有 1/10 的可能性再次选择节点 1, 有 9/10 的概率选择了其他节点,如果选择了其他节点,就意味着需要再一次从数据源获取数据,一般来说,这个操作是很耗时的。这样做,一是缓存效率低,二是各个节点上存储着相同的数据,浪费了大量的存储空间。

那有什么办法,对于给定的 key,每一次都选择同一个节点呢?使用 hash 算法也能够做到这一点。那把 key 的每一个字符的 ASCII 码加起来,再除以 10 取余数可以吗?当然可以,这可以认为是自定义的 hash 算法。
在这里插入图片描述
在这里插入图片描述
从上面的图可以看到,任意一个节点任意时刻请求查找键 Tom 对应的值,都会分配给节点 2,有效地解决了上述的问题。

节点数量变化了怎么办?

简单求取 Hash 值解决了缓存性能的问题,但是没有考虑节点数量变化的场景。假设,移除了其中一台节点,只剩下 9 个,那么之前 hash(key) % 10 变成了 hash(key) % 9,也就意味着几乎缓存值对应的节点都发生了改变。即几乎所有的缓存值都失效了。节点在接收到对应的请求时,均需要重新去数据源获取数据,容易引起 缓存雪崩。

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

算法原理

步骤

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

  • 计算节点/机器(通常使用节点的名称、编号和 IP 地址)的哈希值,放置在环上。
  • 计算 key 的哈希值,放置在环上,顺时针寻找到的第一个节点,就是应选取的节点/机器。

PS:下面的图片是哈希环。其中key11, key2…在环上的位置是由其哈希值(由一个特殊函数算法)决定的,每一个key所用的节点是在环上顺时间取到的第一个节点peer

在这里插入图片描述
环上有 peer2,peer4,peer6 三个节点,key11,key2,key27 均映射到 peer2,key23 映射到 peer4。此时,如果新增节点/机器 peer8,假设它新增位置如图所示,那么只有 key27 从 peer2 调整到 peer8,其余的映射均没有发生改变。

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

数据倾斜问题

如果服务器的节点过少,容易引起 key 的倾斜。例如上面例子中的 peer2,peer4,peer6 分布在环的上半部分,下半部分是空的。那么映射到环下半部分的 key 都会被分配给 peer2,key 过度向 peer2 倾斜,缓存节点间负载不均。

为了解决这个问题,引入了虚拟节点的概念,一个真实节点对应多个虚拟节点。

假设 1 个真实节点对应 3 个虚拟节点,那么 peer1 对应的虚拟节点是 peer1-1、 peer1-2、 peer1-3(通常以添加编号的方式实现),其余节点也以相同的方式操作。

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

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

Go语言实现

我们在 geecache 目录下新建 package consistenthash,用来实现一致性哈希算法。

package consistenthash

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

// Hash maps bytes to uint32
type HashFunc func(data []byte) uint32

// Map constains all hashed keys
type Map struct {
	hashFunc     HashFunc
	replicas int
	keys     []int // Sorted
	hashMap  map[int]string
}

// New creates a Map instance
func New(replicas int, fn HashFunc) *Map {
	m := &Map{
		replicas: replicas,
		hashFunc:     fn,
		hashMap:  make(map[int]string),
	}
	if m.hash == nil {
		m.hashFunc = crc32.ChecksumIEEE//如果没有输入哈希函数,则使用默认哈希函数crc32.ChecksumIEEE
	}
	return m
}
  • 定义了函数类型 Hash,采取依赖注入的方式,允许用于替换成自定义的 Hash 函数,也方便测试时替换,默认为
    crc32.ChecksumIEEE 算法。

  • Map 是一致性哈希算法的主数据结构,包含 4 个成员变量:Hash 函数 hash;虚拟节点倍数 replicas;哈希环
    keys;虚拟节点与真实节点的映射表 hashMap,键是虚拟节点的哈希值,值是真实节点的名称。

  • 构造函数 New() 允许自定义虚拟节点倍数和 Hash 函数。

接下来,实现添加真实节点/机器的 Add() 方法。

// Add adds some keys to the hash.
func (m *Map) Add(keys ...string) {
	for _, key := range keys {
		for i := 0; i < m.replicas; i++ {
			hash := int(m.hashFunc([]byte(strconv.Itoa(i) + key)))//strconv.Itoa()函数用于将整数转换为字符串
			m.keys = append(m.keys, hash)
			m.hashMap[hash] = key
		}
	}
	sort.Ints(m.keys)
}
  • Add 函数允许传入 0 或 多个真实节点的名称。
  • 对每一个真实节点 key,对应创建 m.replicas 个虚拟节点,虚拟节点的名称是:strconv.Itoa(i) + key,即通过添加编号的方式区分不同虚拟节点.
  • 使用 m.hash() 计算虚拟节点的哈希值,使用 append(m.keys, hash) 添加到环上。
  • 在 hashMap 中增加虚拟节点和真实节点的映射关系。
  • 最后一步,环上的哈希值排序。

最后一步,实现选择节点的 Get() 方法。

// Get gets the closest item in the hash to the provided key.
func (m *Map) Get(key string) string {
	if len(m.keys) == 0 {
		return ""
	}

	hash := int(m.hashFunc([]byte(key)))
	// Binary search for appropriate replica.
	idx := sort.Search(len(m.keys), func(i int) bool {
		return m.keys[i] >= hash
	})

	return m.hashMap[m.keys[idx%len(m.keys)]]
}
  • 选择节点就非常简单了,第一步,计算 key 的哈希值。
  • 第二步,顺时针找到第一个匹配的虚拟节点的下标 idx,从 m.keys 中获取到对应的哈希值。如果 idx == len(m.keys),说明应选择 m.keys[0],因为 m.keys 是一个环状结构,所以用取余数的方式来处理这种情况。
  • 第三步,通过 hashMap 映射得到真实的节点。

sort.Search函数的用法:
函数签名

func Search(n int, f func(int) bool) int

参数
n:表示要搜索的集合的大小,通常是切片的长度。
f:是一个函数,它接受一个整数参数 i,并返回一个布尔值。这个函数定义了搜索的条件。
工作原理
sort.Search 会找到并返回 [0, n) 范围内,使得 f(i) 为 true 的最小索引 i。如果不存在这样的索引,Search 返回 n。Search 要求 f 对于输入范围 [0, n) 的某些(可能为空)前缀为 false,然后对(可能为空)余数为 true;Search 返回第一个 true 的索引。

常见用途
sort.Search 常用于在排序的、可索引的数据结构(例如数组或切片)中查找值 x 的索引 i。在这种情况下,参数 f(通常是闭包)捕获要搜索的值,以及数据结构的索引和排序方式。

示例代码

package main

import (
    "fmt"
    "sort"
)

func main() {
    data := []int{1, 2, 3, 5, 10}
    target := 4
    index := sort.Search(len(data), func(i int) bool {
        return data[i] >= target
    })
    if index < len(data) && data[index] == target {
        // target is present at data[index]
        fmt.Printf("找到了不小于 %d 的元素,索引为 %d,值为 %d\n", target, index, data[index])
    } else {
        // target is not present in data,
        // but index is the index where it would be inserted.
        fmt.Printf("没有找到不小于 %d 的元素,索引为 %d\n", target, index)
    }
}

至此,整个一致性哈希算法就实现完成了。

测试

package consistenthash

import (
	"strconv"
	"testing"
)

func TestHashing(t *testing.T) {
	hash := New(3, func(key []byte) uint32 {
		i, _ := strconv.Atoi(string(key))
		return uint32(i)
	})

	// Given the above hash function, this will give replicas with "hashes":
	// 2, 4, 6, 12, 14, 16, 22, 24, 26
	hash.Add("6", "4", "2")

	testCases := map[string]string{
		"2":  "2",
		"11": "2",
		"23": "4",
		"27": "2",
	}

	for k, v := range testCases {
		if hash.Get(k) != v {
			t.Errorf("Asking for %s, should have yielded %s", k, v)
		}
	}

	// Adds 8, 18, 28
	hash.Add("8")

	// 27 should now map to 8.
	testCases["27"] = "8"

	for k, v := range testCases {
		if hash.Get(k) != v {
			t.Errorf("Asking for %s, should have yielded %s", k, v)
		}
	}

}

如果要进行测试,那么我们需要明确地知道每一个传入的 key 的哈希值,那使用默认的 crc32.ChecksumIEEE 算法显然达不到目的。所以在这里使用了自定义的 Hash 算法。自定义的 Hash 算法只处理数字,传入字符串表示的数字,返回对应的数字即可。

  • 一开始,有 2/4/6 三个真实节点,对应的虚拟节点的哈希值是 02/12/22、04/14/24、06/16/26。
  • 那么用例 2/11/23/27 选择的虚拟节点分别是 02/12/24/02,也就是真实节点 2/2/4/2。
  • 添加一个真实节点 8,对应虚拟节点的哈希值是 08/18/28,此时,用例 27 对应的虚拟节点从 02 变更为 28,即真实节点 8。

PS:为什么2/4/6 三个真实节点,对应的虚拟节点的哈希值是 02/12/22、04/14/24、06/16/26?
:因为我们将节点值先转换为string类型, 再转换成[]byte类型。在转换成string类型后加上i = 0, 1, 2后,是直接在数据前相加的,也就是"0" + “2” = “02”, “1” + “2” = “12”, “2” + “2” = “22”…

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2239706.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL数据库专栏(四)MySQL数据库链接操作C#篇

摘要 本篇文章主要介绍C#链接MySQL数据库的接口介绍&#xff0c;使用实例及注意事项&#xff0c;辅助类的封装及调用实例&#xff0c;可以直接移植到项目里面使用。 目录 1、添加引用 2、接口介绍 2.1、MySqlConnection 2.2、MySqlCommand 2.3、MySqlDataReader…

借助 Pause 容器调试 Pod

借助 Pause 容器调试 Pod 在 K8S 中&#xff0c;Pod 是最核心、最基础的资源对象&#xff0c;也是 Kubernetes 中调度最小单元。在介绍 Pause 容器之前需要先说明下 Pod 与容器的关系来理解为什么需要 Pause 容器来帮助调试 1. Pod 与 容器的关系 Pod 是一个抽象的逻辑概念&…

UE5 umg学习(四) 将UI控件切换到关卡中

视频资料 7、将UI控件渲染到关卡_哔哩哔哩_bilibili 在前三节里&#xff0c;创建了用户的控件蓝图Widget_BP 目标是运行的时候&#xff0c;开始运行这个蓝图&#xff0c;因此需要在开始事件触发运行 首先&#xff0c;回到主页&#xff0c;点击关卡蓝图 要从事件开始运行时 …

React Hooks在现代前端开发中的应用

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 React Hooks在现代前端开发中的应用 React Hooks在现代前端开发中的应用 React Hooks在现代前端开发中的应用 引言 React Hooks …

军工行业运维:监控易引领自主可控新潮流

在军工行业&#xff0c;信息安全和技术创新始终是发展的重中之重。随着信息化建设的不断深入&#xff0c;对监控产品的要求也日益严格。 监控易作为一款高性能、全面性的运维监控解决方案&#xff0c;凭借其国产化、自主可控的特性&#xff0c;以及对军工行业特殊需求的深刻理解…

【操作系统】——调度算法

&#x1f339;&#x1f60a;&#x1f339;博客主页&#xff1a;【Hello_shuoCSDN博客】 ✨操作系统详见 【操作系统专项】 ✨C语言知识详见&#xff1a;【C语言专项】 目录 先来先服务&#xff08;FCFS, First Come First Serve) 短作业优先&#xff08;SJF, Shortest Job Fi…

C++入门基础知识147—【关于C++ 一元运算符重载】

成长路上不孤单&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a; 【14后&#x1f60a;///C爱好者&#x1f60a;///持续分享所学&#x1f60a;///如有需要欢迎收藏转发///&#x1f60a;】 今日分享关于C 一元运算符重载的相关内容&#xff0…

【测试框架篇】单元测试框架pytest(1):环境安装和配置

一、pytest简介 Pytest是Python的一种单元测试框架&#xff0c;与Python自带的unittest测试框架类似&#xff0c;但是比 unittest框架使用起来更简洁&#xff0c;效率更高。 二、pytest特点 Pytest是一个非常成熟的Python测试框架,主要特点有以下几点&#xff1a; 非常容易…

【CANOE】【学习】【DecodeString】字节转为中文字符输出

系列文章目录 文章目录 系列文章目录前言一、DecodeString 转为中文字节输出二、代码举例1.代码Demo2.DecodeString 函数说明函数语法&#xff1a;参数说明&#xff1a;返回值&#xff1a;使用示例&#xff1a;示例代码&#xff1a; 说明&#xff1a; 前言 有时候使用的时候&a…

边缘计算在智能交通系统中的应用

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 边缘计算在智能交通系统中的应用 边缘计算在智能交通系统中的应用 边缘计算在智能交通系统中的应用 引言 边缘计算概述 定义与原…

CC1链学习记录

&#x1f338; 前言 上篇文章学习记录了URLDNS链&#xff0c;接下来学习一下Common-Colections利用链。 &#x1f338; 相关介绍 Common-Colections是Apache软件基金会的项目&#xff0c;对Java标准的Collections API提供了很好的补充&#xff0c;在其基础上对常用的数据结构…

边缘计算在智能物流中的应用

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 边缘计算在智能物流中的应用 边缘计算在智能物流中的应用 边缘计算在智能物流中的应用 引言 边缘计算概述 定义与原理 发展历程 …

《多模稳控应急通讯设备技术规范》出炉,应急通讯有章可循!

近日&#xff0c;由深圳市遨游通讯设备有限公司参与的《多模稳控应急通讯设备技术规范》在经历多项严格的审核程序后&#xff0c;由中国中小商会企业协会正式发布实施。该标准详细规定了多模稳控应急通讯设备的术语和定义、产品型号、技术要求、试验方法、检验规则、标志、包装…

生成任意3D和4D场景!GenXD:通用3D-4D联合生成框架 | 新加坡国立微软

文章链接: https://arxiv.org/pdf/2411.02319 项目链接&#xff1a;https://gen-x-d.github.io/ 有视频 亮点直击 设计了一个数据整理流程&#xff0c;从视频中获取包含可移动物体的高质量4D数据&#xff0c;并为30,000个视频标注了相机姿态。这个大规模数据集称为CamVid-30K&…

全面解读:低代码开发平台的必备要素——系统策划篇

在传统开发过程中&#xff0c;系统策划起着举足轻重的作用&#xff0c;它宛如一位幕后的总指挥&#xff0c;把控着整个软件开发项目的走向。而随着技术的不断进步&#xff0c;低代码开发平台逐渐崭露头角&#xff0c;它以快速开发、降低技术门槛等优势吸引了众多企业和开发者的…

如何通过工单池与抢单机制提升企业服务效率与客户满意度?

在许多企业的售后服务或维修管理中&#xff0c;工单管理常常是一个瓶颈。工单处理不及时、任务分配不合理&#xff0c;或者客户的需求没有被准确跟踪&#xff0c;都可能影响到服务质量&#xff0c;进而影响客户满意度。随着企业规模扩大&#xff0c;如何高效管理工单池、合理调…

智慧电厂新纪元:基于亿信ABI的电厂环保指挥管控平台实践

​在当今全球能源转型与环境保护的双重驱动下&#xff0c;电力行业正加速向智能化、绿色化迈进。本案例聚焦于某电厂的智能化转型实践&#xff0c;通过引入先进的烟气达标监测技术与碳资产管理平台&#xff0c;不仅实现了对电厂排放的精准控制&#xff0c;还极大提升了碳数据的…

【C++练习】计算并输出1到10的所有整数的阶乘之和

题目&#xff1a; 计算并输出1到10的所有整数的阶乘之和 描述 编写一个C程序&#xff0c;定义一个函数来计算给定整数n的阶乘&#xff0c;然后在main函数中利用该函数计算从1到10&#xff08;包括1和10&#xff09;所有整数的阶乘&#xff0c;并将这些阶乘值累加起来&#x…

51c嵌入式~单片机合集2

我自己的原文哦~ https://blog.51cto.com/whaosoft/12362395 一、不同的电平信号的MCU怎么通信&#xff1f; 下面这个“电平转换”电路&#xff0c;理解后令人心情愉快。电路设计其实也可以很有趣。 先说一说这个电路的用途&#xff1a;当两个MCU在不同的工作电压下工作&…

ROM修改进阶教程------安卓14 安卓15去除app签名验证的几种操作步骤 详细图文解析

在安卓14 安卓15的固件中。如果修改了系统级别的app。那么就会触发安卓14 15的应用签名验证。要么会导致修改的固件会进不去系统,或者进入系统有bug。博文将从几方面来解析去除安卓14 15应用签名验证的几种方法。 💝💝💝通过博文了解: 1💝💝💝-----安卓14去除…