一致性哈希算法在1997年由麻省理工学院的Karger等人在解决分布式Cache中提出的,设计目标是为了解决因特网中的热点(Hot spot)问题
国际惯例,先上源码 https://github.com/manerfan/c...
- 可自定义节点数据类型
- 可自定义hash函数
原理
一致性哈希可应用于负载均衡、分布式缓存(缓存分片)等场景中,以下以分布式缓存为例
传统方式
如,现有N个缓存实例,将一个对象object映射到某一个缓存上可以采取取模方式 hash(object) % N
我们称之为简单hash算法。一般,简单hash算法确实能够比较均匀地实现分布式映射,但如果考虑缓存实例变动(增删)的情况:
- 某一缓存实例宕机,需要将该实例从集群中摘除,则映射公式变为
hash(object) % (N - 1)
- 增加一台缓存实例,将该实例加入集群,则映射公式变为
hash(object) % (N + 1)
对于以上情况,无论新增还是移除,大部分object所映射的缓存实例均会改变,缓存命中率大幅度降低从而回源到服务器,短时间内造成缓存雪崩现象
一致性哈希
一致性 Hash 算法简单的说,在移除/添加一个缓存实例时,尽可能小的改变已存在key映射关系,尽可能的满足单调性的要求。
1. 环形空间
通常的hash算法都是将一个value映射到一个32位的key值,我们可以将这个[0, 2^32-1]空间想象成一个首尾相接的环形队列
2. 将对象映射到hash空间
通过hash函数计算对象hash值,将对象映射到hash环形空间
3. 将缓存实例映射到hash空间
使用缓存实例的ip、port等信息,通过hash函数计算其hash值,将缓存实例映射到hash空间
4. 将对象映射到缓存实例
沿着顺时针方向,查找距离对象object最近的缓存实例,并将对象映射到该实例
5. 添加缓存实例
按照同样的算法,在添加实例后发现,只有少部分对象的映射关系改变
6. 移除缓存实例
按照同样的算法,在移除实例后,同样只有少部分对象的映射关系改变
7. 虚拟节点
为了使对象尽可能均匀地映射到所有的缓存实例中(解决缓存实例分布不均匀的问题),引入虚拟节点的概念
虚拟节点其实为真实节点在hash空间中的复制品,一个真实节点可以对应多个虚拟节点
虚拟节点的hash求值可以在真实节点的求值基础上加入编号等信息 hash(realCacheKey#1)
、 hash(realCacheKey#2)
通用实现
实现目标:
- 由于一致性哈希应用较多,并不局限于某一特定场景,故需要能够 自定义节点数据类型
- 常规hash算法一般采用md5等,但不限制hash函数实现,故需要能够 自定义hash函数
自定义节点数据类型
这里,我们定义一个公共接口
/**
* 真实节点
*/
interface PhysicalNode {
fun hashKey(): String
}
自定义节点数据,只需要实现PhysicalNode接口及hashKey方法,程序会通过hashKey的值计算节点的hash值
如,我们定义常规服务节点
/**
* 常规的服务节点
*
* @param name 服务名称
* @param host 服务host/ip
* @param port 服务port
*/
data class HostPortPhysicalNode(val name: String, val host: String, val port: Int) : PhysicalNode {
override fun hashKey() = "$name:$host:$port"
}
这里,我们还需要定义一个虚拟节点
/**
* 虚拟节点
*
* @param parent 真实节点
* @param replica 虚拟节点id
*/
data class VirtualNode(val parent: T, private val replica: Int) : PhysicalNode {
override fun hashKey() = "${parent.hashKey()}#$replica"
fun matches(key: String) = parent.hashKey() == key
}
虚拟节点的hashKey为真实节点hashKey加上节点编号
自定义hash函数
这里,我们同样定义一个公共接口
/**
* 哈希函数
*/
interface HashFunc {
fun hash(str: String): Long
}
自定义hash含蓄,只需要实现HashFunc接口及hash方法
如,我们定义md5函数
class Md5 : HashFunc {
override fun hash(str: String): Long {
val md5 = MessageDigest.getInstance("MD5")
md5.reset()
md5.update(str.toByteArray())
val bytes = md5.digest()
var h: Long = 0
bytes.forEach {
h = h shl 8
h = h or (it.toLong() and 0xFF)
}
return h
}
}
ConsistentHash的使用
ConsistentHash
及ConsistentHashHelper
的实现,见 ConsistentHashHelper.kt
ConsistentHashHelper
我们使用ConsistentHashHelper来构建ConsistentHash
val consistentHash = ConsistentHashHelper.create().build()
如果需要自定义hash函数,可以通过withHash
指定,默认使用md5
val consistentHash = ConsistentHashHelper.create()
.withHash(MyMd5HashFunc())
.build()
同样,可以通过withNodes指定在初始化时生成节点信息
val master = HostPortPhysicalNode("master", "192.169.1.1", 8080)
val slave = HostPortPhysicalNode("slave", "192.169.1.2", 8080)
val consistentHash = ConsistentHashHelper.create()
.withHash(MyMd5HashFunc())
.withNodes(listOf(master, slave), 2) // 节点,并指定每个节点的副本数(可以省略,缺省1)
.build()
ConsistentHash
运行过程中,可以动态增删节点
val backup = HostPortPhysicalNode("backup", "192.168.1.13", 8888)
consistentHash.add(backup, 4) // 增加节点,并指定每个节点的副本数(可以省略,缺省1)
consistentHash.remove(slave.hashKey())
通过getNode函数获取对应object所映射的缓存实例
consistentHash.getNode(hashFunc.hash(object1.key))
consistentHash.getNode(hashFunc.hash(object2.key))