《一致性哈希算法的原理与实现》 对一致性哈希算法做了比较完善的解读,但存在2个问题,本文主要解决这2个问题:
- 算法时间复杂度为O(1)
- 没有给出扩容逻辑,扩容时如何保证并发度,如何降低对写性能的影响?
虚拟节点和物理节点的定义
// 物理节点
public static class Node {
private Map<Integer, Integer> data; // 物理节点中的数据, 可以替换为远程服务
private final int id; // 物理节点id
public Node(int id) {
this.id = id;
this.data = new ConcurrentHashMap<>();
}
}
// 虚拟节点
public class VirtualNode {
private Node node; // 虚拟节点管理的物理节点
private final int id; // 虚拟节点id
private final long from; // 虚拟节点管理的hash范围, 用long可以表示无符号整数, 方便迁移时遍历当前节点管理的所有数据
private final long to; // 虚拟节点管理的hash范围, 用long可以表示无符号整数, 方便迁移时遍历当前节点管理的所有数据
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); // 控制只读状态的锁
private int count; // 记录当前节点管理的数据量, 用来加快扩容
// virtualNodeCapacity 是虚拟节点的容量, 是2的整数次幂
public VirtualNode(int id, int virtualNodeCapacity) {
this.id = id;
this.from = ((long) id) * virtualNodeCapacity;
this.to = from + virtualNodeCapacity;
}
}
时间复杂度为O(1)的一致性哈希算法
假设有3个物理节点,为物理节点编号
int count =3;
Node[] nodes = new Node[count];
for (int id = 0; id < count; id++) {
nodes[id] = new Node(id);
}
假设有1000个虚拟节点,为虚拟节点编号
int count =1000;
VirtualNode[] virtualNodes = new VirtualNode[count];
for (int id = 0; id < count; id++) {
virtualNodes[id] = new VirtualNode(id);
}
把物理节点均匀的分布到虚拟节点上
int len = nodes.length;
for (VirtualNode virtualNode : virtualNodes) {
virtualNode.node = nodes[virtualNode.id % len];
}
查询虚拟节点
public VirtualNode getVirtualNode(int hash) {
long l = ((long)hash) & 0xFFFFFFFFL; // 把int转为无符号整数
long virtualNodeIndex = l / virtualNodes.length;
return virtualNodes[(int) virtualNodeIndex];
}
至此,一致性哈希算法,可以降到O(1)。
但这里使用了除法,可以用位运算继续优化。
限制虚拟节点数量为2的整数次幂,比如1<<10 == 1024。
查询虚拟节点的逻辑优化为
public VirtualNode getVirtualNode(int hash) {
int virtualNodeIndex = hash >>> (32 - 10); // 32是int的位数,虚拟节点数量为1<<10
VirtualNode virtualNode = virtualNodes[virtualNodeIndex];
return virtualNode;
}
一致性哈希算法的扩容
每个虚拟节点维护了一定范围的数据,这些数据存储在一个物理节点里。
扩容后,虚拟节点维护的数据不会发生变化,但存储数据的物理节点可能发生变化了。
假设虚拟节点virtualA扩容前管理的物理节点为 nodeA, 扩容后管理的物理节点为 nodeB。
先将virtualA设置为只读状态(读请求由nodeA处理),将virtualA管理的数据,从nodeA复制到nodeB,复制完成后,用nodeB替换nodeA(读写请求由nodeB处理),恢复读写状态,延迟一定时间,将nodeA中对应的数据删除。
// 扩容, 传入的真实节点要把编号设置好
public void addNodes(Node[] newNodes) {
Node[] nodes = new Node[this.nodes.length + newNodes.length];
System.arraycopy(this.nodes, 0, nodes, 0, this.nodes.length);
System.arraycopy(newNodes, 0, nodes, this.nodes.length, newNodes.length);
int len = nodes.length;
for (VirtualNode virtualNode : virtualNodes) {
Node fromNode = virtualNode.node;
Node toNode = nodes[virtualNode.id % len];
if (fromNode.id == toNode.id) {
continue; // 无需迁移
}
virtualNode.transform(toNode); // 迁移数据
}
this.nodes = nodes;
}
// 数据迁移
private void transform(Node target) {
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
try { // 加写锁, 进入只读模式
writeLock.lock();
if (this.count == 0) {
return;
}
System.out.println(String.format("数据迁移, 虚拟节点=%s, 真实节点from=%s, 真实节点to=%s, hashFrom=%s, hashTo=%s", id, node.id, target.id, from, to));
int count = 0;
for (long i = from; i < to; i++) { // 迁移数据
int hash = (int) i;
Integer value = node.data.get(hash);
if (value != null) {
count++;
target.data.put(hash, value);
}
}
// 迁移完成
Node replacedNode = node;
node = target;
threadPoolExecutor.schedule(() -> {
for (long i = from; i < to; i++) { // 迁移完了, 延迟删除数据, 因为可能还会从旧的node中读数据
int hash = (int) i;
replacedNode.data.remove(hash);
}
}, 3, TimeUnit.SECONDS);
System.out.println(String.format("迁移了 %s 条数据", count));
} finally {
writeLock.unlock(); // 迁移完成, 恢复读写模式
}
}
完整代码
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 一致性hash
*
* @since: 2022/3/29 上午11:11
*/
public class ConsistencyHash {
private Node[] nodes; // 真实节点
public final VirtualNode[] virtualNodes; // 虚拟节点
private final int virtualNodeBitCount;
private final int virtualNodeCount; // 虚拟节点个数
private final int virtualNodeCapacityBitCount;
private final int virtualNodeCapacity; // 虚拟节点容量
private final ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(4);
/**
* 将虚拟节点容量定义为2的整数次幂, 同时虚拟节点容量也是2的整数次幂, 可以用位运算在O(1)时间内计算出虚拟节点编号, 从而直接得到真实节点
* int占用32个bit, 假设虚拟节点有1024个, 那么可以用9个bit来表示虚拟节点数量(1<<10), 用23个bit来表示虚拟节点容量(1<<24)
* 扩容时, 先确定要迁移的虚拟节点, 假设扩容前, virtualNodeA 指向的是 realNodeA, 扩容后 virtualNodeA 指向的是 realNodeB
* 在扩容前, 设置 virtualNodeA 为只读模式
* 将 virtualNodeA 管理的数据, 从 realNodeA 拷贝到 realNodeB
* 扩容完成后, 设置 virtualNodeA 为读写模式, 延迟3秒删除 realNodeA 中已迁移的数据
*
* @param bitCount bitCount 虚拟节点个数为 2^bitCount 个, bitCount最少为6(64), 最大为16(65536)
* @param nodes nodes
* @since 2022/3/29 下午2:00
*/
public ConsistencyHash(int bitCount, Node[] nodes) {
if (bitCount < 6 || bitCount > 16) {
throw new RuntimeException("offset范围为[6, 15]");
}
virtualNodeBitCount = bitCount;
virtualNodeCount = 1 << virtualNodeBitCount; // 使用bitCount计算虚拟节点个数
virtualNodeCapacityBitCount = 32 - virtualNodeBitCount; // 剩余的bit, 用来计算虚拟节点容量
virtualNodeCapacity = 1 << virtualNodeCapacityBitCount; // 计算虚拟节点容量
virtualNodes = new VirtualNode[virtualNodeCount];
for (int i = 0; i < virtualNodeCount; i++) {
virtualNodes[i] = new VirtualNode(i, virtualNodeCapacity); // 创建虚拟节点
}
this.nodes = nodes;
initNodes(nodes); // 初始化真实节点
int count =3;
for (int id = 0; id < count; id++) {
nodes[id] = new Node(id);
}
}
public int getNodeCount() {
return nodes.length;
}
public int size() {
int size = 0;
for (Node node : nodes) {
size += node.data.size();
}
return size;
}
private void initNodes(Node[] nodes) {
int len = nodes.length;
for (VirtualNode virtualNode : virtualNodes) {
virtualNode.node = nodes[virtualNode.id % len]; // 使用取模的方式将真实节点均匀地分配给虚拟节点
}
}
// 扩容, 传入的真实节点要把编号设置好
public void addNodes(Node[] newNodes) {
Node[] nodes = new Node[this.nodes.length + newNodes.length];
System.arraycopy(this.nodes, 0, nodes, 0, this.nodes.length);
System.arraycopy(newNodes, 0, nodes, this.nodes.length, newNodes.length);
int len = nodes.length;
for (VirtualNode virtualNode : virtualNodes) {
Node fromNode = virtualNode.node;
Node toNode = nodes[virtualNode.id % len];
if (fromNode.id == toNode.id) {
continue; // 无需迁移
}
virtualNode.transform(toNode); // 迁移数据
}
this.nodes = nodes;
}
public Integer put(int hash, int value) {
return getVirtualNode(hash).put(hash, value);
}
public Integer get(int hash) {
return getVirtualNode(hash).get(hash);
}
public Integer remove(int hash) {
return getVirtualNode(hash).remove(hash);
}
public boolean contains(int hash) {
return getVirtualNode(hash).contains(hash);
}
private VirtualNode getVirtualNode(int hash) {
int virtualNodeIndex = getVirtualNodeIndex(hash);
VirtualNode virtualNode = virtualNodes[virtualNodeIndex];
return virtualNode;
}
private int getVirtualNodeIndex(int hash) {
return hash >>> virtualNodeCapacityBitCount; // 位运算快速定位虚拟节点
}
// 物理节点
public static class Node {
private Map<Integer, Integer> data; // 物理节点中的数据, 可以替换为远程服务
private final int id; // 物理节点id
public Node(int id) {
this.id = id;
this.data = new ConcurrentHashMap<>();
}
@Override
public String toString() {
return String.format("id=%s, count=%s", id, data.size());
}
}
// 虚拟节点
public class VirtualNode {
private Node node; // 虚拟节点管理的物理节点
private final int id; // 虚拟节点id
private final long from; // 虚拟节点管理的hash范围, 用long可以表示无符号整数, 方便迁移时遍历当前节点管理的所有数据
private final long to; // 虚拟节点管理的hash范围, 用long可以表示无符号整数, 方便迁移时遍历当前节点管理的所有数据
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); // 控制只读状态的锁
private int count; // 记录当前节点管理的数据量, 用来加快扩容
// virtualNodeCapacity 是虚拟节点的容量, 是2的整数次幂
public VirtualNode(int id, int virtualNodeCapacity) {
this.id = id;
this.from = ((long) id) * virtualNodeCapacity;
this.to = from + virtualNodeCapacity;
}
public int getSize() {
return count;
}
// 数据迁移
private void transform(Node target) {
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
try { // 加写锁, 进入只读模式
writeLock.lock();
if (this.count == 0) {
return;
}
System.out.println(String.format("数据迁移, 虚拟节点=%s, 真实节点from=%s, 真实节点to=%s, hashFrom=%s, hashTo=%s", id, node.id, target.id, from, to));
int count = 0;
for (long i = from; i < to; i++) { // 迁移数据
int hash = (int) i;
Integer value = node.data.get(hash);
if (value != null) {
count++;
target.data.put(hash, value);
}
}
// 迁移完成
Node replacedNode = node;
node = target;
threadPoolExecutor.schedule(() -> {
for (long i = from; i < to; i++) { // 迁移完了, 延迟删除数据, 因为可能还会从旧的node中读数据
int hash = (int) i;
replacedNode.data.remove(hash);
}
}, 3, TimeUnit.SECONDS);
System.out.println(String.format("迁移了 %s 条数据", count));
} finally {
writeLock.unlock(); // 迁移完成, 恢复读写模式
}
}
public Integer put(int hash, int value) {
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
try {
readLock.lock();
Integer replace = node.data.put(hash, value);
if (replace == null) {
count++;
}
return replace;
} finally {
readLock.unlock();
}
}
public Integer remove(int hash) {
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
try {
readLock.lock();
Integer v = node.data.remove(hash);
if (v != null) {
count--;
}
return v;
} finally {
readLock.unlock();
}
}
public Integer get(int hash) {
return node.data.get(hash);
}
public boolean contains(int hash) {
return node.data.containsKey(hash);
}
@Override
public String toString() {
return String.format("id=%s,nodeId%s(from=%s,to=%s)count=%s", id, node == null ? "null" : node.id, from, to, count);
}
}
}