一致性哈希算法 CARP 原理解析, 附 Golang 实现
本文来自:Segmentfault
感谢作者:CodeKiller
查看原文:一致性哈希算法 CARP 原理解析, 附 Golang 实现
在后端服务开发的过程中, 遇到了这样一个问题: 需要在 mysql 前面部署 redis 做一层缓存, 要求 redis 是集群部署, 并且每台 redis 节点只缓存总数据量的 1/N, N 为 redis 的个数.
看到这里大家都能想到到一个方法是使用 hash(key)%N
来选取 redis 进行 value 的存取, 这种方式当然可以很均匀的将数据分配到 N 个 redis 服务上, 并且实现起来也非常的简单. 但是使用这种哈希取余的方式有一个很大的问题, 那就是当 redis 集群扩容或者缩容, 或者发生宕机的时候, 也就是上述公式中的 N 发生变化的时候, 这个时候 hash(key)%N
的值保持不变的概率非常小, 换句话说, 缓存系统会在这种情况下整个失效, 这对于后端的 mysql 来说瞬时压力会非常大, 很可能造成 mysql 的奔溃, 进而造成整个服务的不可用.
所以必须想一种办法来应对上述的情况, 即当一台 redis 服务挂掉的时候, 能不能做到只有 1/N 的缓存失效呢?
答案就是使用一致性哈希算法 CARP, 严格来讲 CARP 并不是一种算法, 而是一种协议, Cache Array Routing Protocol,Cache 群组路由协议. 下面来介绍些下它的工作原理:
首先假设有 N 个 redis 服务, 分别是 redis1, redis2 .... redisN, key 是想要获取的数据在 redis 中的键.
第一步, 计算所有的服务名与 key 的哈希值:
1 2 3 4 | hash_v1 = hash(redis1 + key) hash_v2 = hash(redis2 + key) ... hash_vN = hash(redisN + key) |
第二步, 计算值最大的 hash_vX, 那么选中的便是 redisX:
1 | hash_vX = max(hash_v1, hash_v2, ..., hash_vN) |
为什么这么做就可以达到增加一台 redis 服务的时候, 只有 1/(N+1) 的缓存失效呢?
1 2 | hash_vX1 = max(hash_v1, hash_v2, ..., hash_vN) hash_vX2 = max(hash_v1, hash_v2, ..., hash_vN, hash_vN+1) |
看上面两个表达式, 如果要求 hash_vX2
大于 hash_vX1
, 那么必须 hash_vN+1
大于前面 N 个哈希值, 那么你选取的 hash 函数足够散列的话, hash_vN+1
大于前面 N 个哈希值的概率为 1/(N+1). 你可以自行算一下减少一台 redis 服务时, 缓存失效的概率.
最后, 附上 Golang 实现版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | package carp import ( "crypto/md5" "errors" ) var ( ErrHaveNoRedis = errors.New( "have no redis" ) ) // 根据 carp 算法, 从 redisArr 的数组中选择合适的 redis, 返回值为下标 func GetTargetIndex(key string, redisArr []string) (idx int, err error) { if len(redisArr) < 1 { return -1, ErrHaveNoTarget } else if len(redisArr) == 1 { return 0, nil } hashArr := make([]string, len(redisArr)) for k, v := range redisArr { hashArr[k] = hashString(v + key) } idx = minIdx(hashArr) return } // 返回 arr 数组中最小值的下标 func minIdx(arr []string) (idx int) { if len(arr) < 1 { return -1 } idx, min := 0, arr[0] for k, v := range arr { if v < min { idx = k min = v } } return } func hashString(str string) (hash string) { md5sum := md5.Sum([]byte(str)) return string(md5sum[:]) } |
上面的代码中使用了 md5 的哈希算法, 如果你对性能有更高的要求可以使用 FNV 哈希算法.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
2019-01-18 python实现用户登陆(sqlite数据库存储用户信息)
2019-01-18 pyrhon SQLite数据库
2019-01-18 Python的getpass模块
2019-01-18 pandas学习(创建多层索引、数据重塑与轴向旋转)