一致性哈希算法 mysql_一致性哈希算法(Java实现)

一致性哈希算法在分布式缓存中用于解决哈希取模的局限性,如动态伸缩导致的数据迁移问题。本文介绍了算法原理,包括哈希环、服务器节点和key的分布,以及如何通过虚拟节点解决负载不均衡。并提供了Java代码实现和负载均衡性的评估方法。

前言

一致性哈希算法在分布式系统的应用中是十分广泛的。常见的应用场景是分布式缓存。它主要解决了哈希取模算法在分布式系统中存在的动态伸缩等问题。

哈希取模算法的局限性

在分布式缓存集群中,当新增加缓存服务器或其中一台挂掉后,由路由算法发生改变,导致大量的缓存数据不能命中。从而造成数据库面临巨大压力而崩溃,可能导致整个系统崩溃。

一致性哈希算法原理

一致性哈希算法通过一个叫作一致性哈希环的数据结构实现。这个环的起点是

math?formula=0,终点是

math?formula=2%5E%7B32%7D-1,并且起点和终点相连接,故这个环的整数分布范围是

math?formula=%5B0%2C%202%5E%7B32%7D-1%5D

将服务器节点和key放到哈希环上

我们将服务器节点和key的hash值放置到哈希环上,如下图:

27578e130525

服务器节点分别是NODE0、NODE1、NODE2。key分别代表key1 ~ key8。

将key和服务器节点都放置到同一个哈希环后,在哈希环上顺时针查找距离这个 key 的 hash 值最近的机器,即是这个key所属的机器。

key1、key8在节点NODE0上;key2、key5在节点NODE1上;key3、key4、key6在节点NODE2上。

增加服务器(扩容)

由于业务需要,如缓存集群压力过大,我们需要增加一台服务器(NODE3)。经过hash函数计算,NODE3节点落在NODE1和NODE2之间。如下图:

27578e130525

对上述情况,只有NODE1和NODE2节点之间的key需要重新分配。key4没有变,还在NODE2节点。只是key3、key6重新分配新的节点NODE3上。我们发现,一致性哈希算法只需要很少部分key需要重新分配。而哈希取模方式则大部分缓存会失效。

减少服务器(缩容)

由于某个服务器出现故障导致下线,如NODE3下线,这时原本key3、key6存储在NODE3上,需要重新被分配至NODE2节点上,其它key不受此影响。

服务器节点分布不均匀

如果服务器节点不是均匀的分布在哈希环上,那么有可能造成服务节点负载压力不均衡。

当新增加一台服务器时,节点NODE3只是分担了节点NODE2的压力,造成服务器压力不均摊。显然这个结果不是我们期望的。

针对这个问题,我们可以通过引入虚拟节点来解决负载不均衡的问题。即将每台物理服务器虚拟为一组虚拟服务器,将虚拟服务器放置到哈希环上,如果要确定key所在的服务器,需先确定key所在的虚拟服务器,再由虚拟服务器确定物理服务器。

基于虚拟节点的一致性哈希

27578e130525

一台物理服务器,虚拟成若干个虚拟节点,随机分布在环上,压力近似均衡分摊。如有三台物理服务器,每台物理服务器虚拟出150个虚拟节点,随机分配在Hash环上的150个位置上。最后可使三台物理服务器近似均摊压力。当增加一台服务器时,先虚拟150个节点,然后散落在哈希环上。

一致性哈希算法实现

Java代码:

public class ConsistentHashing {

private SortedMap hashCircle = new TreeMap<>();

private int virtualNums; // 虚拟节点数

public ConsistentHashing(Node[] nodes, int virtualNums) {

this.virtualNums = virtualNums;

// 初始化一致性hash环

for (Node node : nodes) {

// 创建虚拟节点

add(node);

}

}

/**

* 添加服务器节点

*

* @param node the server

*/

public void add(Node node) {

for (int i = 0; i < virtualNums; i++) {

hashCircle.put(hash(node.toString() + i), node);

}

}

/**

* 删除服务器节点

*

* @param node the server

*/

public void remove(Node node) {

for (int i = 0; i < virtualNums; i++) {

hashCircle.remove(hash(node.toString() + i));

}

}

/**

* 获取服务器节点

*

* @param key the key

* @return the server

*/

public Node getNode(String key) {

if (key == null || hashCircle.isEmpty())

return null;

int hash = hash(key);

if (!hashCircle.containsKey(hash)) {

// 未命中对应的节点

SortedMap tailMap = hashCircle.tailMap(hash);

hash = tailMap.isEmpty() ? hashCircle.firstKey() : tailMap.firstKey();

}

return hashCircle.get(hash);

}

/**

* FNV1_32_HASH算法

*

* @param key the key

* @return

*/

private int hash(String key) {

final int p = 16777619;

int hash = (int) 2166136261L;

for (int i = 0; i < key.length(); i++) {

hash = (hash ^ key.charAt(i)) * p;

}

hash += hash << 13;

hash ^= hash >> 7;

hash += hash << 3;

hash ^= hash >> 17;

hash += hash << 5;

// 如果算出来的值为负数则取其绝对值

if (hash < 0) {

hash = Math.abs(hash);

}

return hash;

}

/**

* 集群节点的机器地址

*/

public static class Node {

private String ipAddr;

private int port;

private String name;

public Node(String ipAddr, int port, String name) {

this.ipAddr = ipAddr;

this.port = port;

this.name = name;

}

@Override

public String toString() {

return name + ":";

}

}

}

评估服务器节点的负载均衡性

我们通过方差计算服务器节点的均衡性,代码如下:

public class ConsistentHashingTest {

public static void main(String[] args) {

ConsistentHashing.Node[] nodes = new ConsistentHashing.Node[4];

Map> map = new HashMap<>();

// make nodes 4台服务器节点

for (int i = 0; i < nodes.length; i++) {

nodes[i] = new ConsistentHashing.Node("10.1.32.2" + i, 8070, "myNode" + i);

}

ConsistentHashing ch = new ConsistentHashing(nodes, 160);

// make keys 100万个key

String[] keys = new String[1_000_000];

for (int i = 0; i < keys.length; i++) {

keys[i] = "key" + (i + 17) + "ss" + (i * 19);

}

// make results

for (String key : keys) {

ConsistentHashing.Node n = ch.getNode(key);

List list = map.computeIfAbsent(n, k -> new ArrayList<>());

list.add(key);

}

// 统计标准差,评估服务器节点的负载均衡性

int[] loads = new int[nodes.length];

int x = 0;

for (Iterator i = map.keySet().iterator(); i.hasNext(); ) {

ConsistentHashing.Node key = i.next();

List list = map.get(key);

loads[x++] = list.size();

}

int min = Integer.MAX_VALUE;

int max = 0;

for (int load : loads) {

min = Math.min(min, load);

max = Math.max(max, load);

}

System.out.println("最小值: " + min + "; 最大值: " + max);

System.out.println("方差:" + variance(loads));

}

public static double variance(int[] data) {

double variance = 0;

double expect = (double) sum(data) / data.length;

for (double datum : data) {

variance += (Math.pow(datum - expect, 2));

}

variance /= data.length;

return Math.sqrt(variance);

}

private static int sum(int[] data) {

int sum = 0;

for (int i = 0; i < data.length; i++) {

sum += data[i];

}

return sum;

}

}

测试结果:

# 虚拟节点是 120

最小值: 243919; 最大值: 253236

方差:3692.7378054771234

# 虚拟节点是 130

最小值: 240190; 最大值: 257384

方差:7432.346466628153

# 虚拟节点是 150

最小值: 233002; 最大值: 260369

方差:10227.744937179456

# 虚拟节点是 160

最小值: 241154; 最大值: 253743

方差:5150.429156876153

# 虚拟节点是 170

最小值: 235938; 最大值: 260044

方差:9244.906895150432

# 虚拟节点是 200

最小值: 233187; 最大值: 263222

方差:11395.83342717855

通过测试,每台物理服务的虚拟节点在120到200之间,均衡性更好。

总结

一致性hash算法解决了分布式环境下机器增加或者减少时,简单的取模运算无法获取较高命中率的问题。通过虚拟节点的使用,一致性hash算法可以均匀分担机器的负载,使得这一算法更具现实的意义。正因如此,一致性hash算法被广泛应用于分布式系统中。

参考资源

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值