一致性哈希算法的原理与实现,时间复杂度O(1),支持扩容(扩容时可并发读,且最多只有一个虚拟节点处于只读状态)

本文深入探讨一致性哈希算法的原理与实现,介绍其时间复杂度为O(1)的特性,并提供完整的代码示例。重点讲解虚拟节点与物理节点的概念,以及在扩容过程中如何保证并发度并降低对写性能的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

《一致性哈希算法的原理与实现》 对一致性哈希算法做了比较完善的解读,但存在2个问题,本文主要解决这2个问题:

  1. 算法时间复杂度为O(1)
  2. 没有给出扩容逻辑,扩容时如何保证并发度,如何降低对写性能的影响?

虚拟节点和物理节点的定义


//    物理节点
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);
        }
    }

}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值