第2讲:一致性哈希——数据分片与负载均衡

上一讲我们实现了节点发现和网络通信,三个节点能互相感知了。但有个核心问题没解决:数据该存到哪个节点上?

最简单的做法是取模:hash(key) % N。但如果节点数变化(扩容或宕机),几乎所有数据的映射关系都会改变,导致大规模数据迁移。

这一讲,我们实现一致性哈希------一种让数据迁移量最小化的分片策略。


一、一致性哈希原理

1.1 核心思想

复制代码
传统取模:key → hash(key) % N
  ❌ 节点增减时,几乎所有 key 都需要重新映射

一致性哈希:key → hash(key) → 环上的第一个节点
  ✅ 节点增减时,只有少量 key 需要迁移

哈希环

复制代码
hash(node1)
            ▲
            │
   hash(k4) │  hash(k1)
      ◄─────┼──────►
            │
   hash(k3) │  hash(k2)
            │
            ▼
         hash(node2)

每个 key 顺时针找到的第一个节点,就是它所属的节点。

1.2 虚拟节点

为了解决两个问题:

  1. 数据倾斜:节点在环上分布不均匀,导致某些节点数据过多

  2. 负载不均:不同节点性能不同,需要差异化权重

引入虚拟节点:每个物理节点对应多个虚拟节点,分散在环上。

复制代码
物理节点 A → 虚拟节点 A-0, A-1, A-2, ..., A-159
物理节点 B → 虚拟节点 B-0, B-1, B-2, ..., B-159
物理节点 C → 虚拟节点 C-0, C-1, C-2, ..., C-159

二、一致性哈希实现

2.1 哈希环

复制代码
# minikv/hashring/consistent_hash.py
import hashlib
import bisect
from typing import Dict, List, Optional, Callable
from dataclasses import dataclass, field

class HashRing:
    """
    一致性哈希环
    
    支持:
    - 虚拟节点(解决数据倾斜)
    - 权重(不同节点不同容量)
    - 节点增删(最小化数据迁移)
    """
    
    def __init__(self, nodes: List[str] = None, 
                 virtual_nodes: int = 160,
                 hash_fn: Callable = None):
        """
        Args:
            nodes: 初始节点列表
            virtual_nodes: 每个物理节点的虚拟节点数
            hash_fn: 哈希函数,默认使用 MD5
        """
        self.virtual_nodes = virtual_nodes
        self.hash_fn = hash_fn or self._md5_hash
        
        # 环:有序的 (hash_value, virtual_node_id) 列表
        self.ring: List[tuple] = []
        
        # 虚拟节点 → 物理节点 映射
        self.virtual_to_physical: Dict[str, str] = {}
        
        # 物理节点 → 虚拟节点列表 映射
        self.physical_to_virtuals: Dict[str, List[str]] = {}
        
        # 节点权重
        self.weights: Dict[str, int] = {}
        
        if nodes:
            for node in nodes:
                self.add_node(node)
    
    def _md5_hash(self, key: str) -> int:
        """MD5 哈希,返回 128 位整数"""
        return int(hashlib.md5(key.encode('utf-8')).hexdigest(), 16)
    
    def _build_virtual_key(self, node: str, vnode_index: int) -> str:
        """生成虚拟节点的 key"""
        return f"{node}#{vnode_index}"
    
    def add_node(self, node: str, weight: int = 1):
        """
        添加物理节点
        
        Args:
            node: 节点标识符(如 'node-1')
            weight: 权重,越大表示该节点承担的负载越多
        """
        if node in self.physical_to_virtuals:
            return  # 节点已存在
        
        self.weights[node] = weight
        self.physical_to_virtuals[node] = []
        
        # 根据权重创建虚拟节点
        actual_vnodes = self.virtual_nodes * weight
        for i in range(actual_vnodes):
            vnode_key = self._build_virtual_key(node, i)
            hash_val = self.hash_fn(vnode_key)
            
            # 插入到有序环中
            bisect.insort(self.ring, (hash_val, vnode_key))
            
            self.virtual_to_physical[vnode_key] = node
            self.physical_to_virtuals[node].append(vnode_key)
    
    def remove_node(self, node: str):
        """
        移除物理节点及其所有虚拟节点
        """
        if node not in self.physical_to_virtuals:
            return
        
        for vnode_key in self.physical_to_virtuals[node]:
            hash_val = self.hash_fn(vnode_key)
            # 从环中移除
            idx = bisect.bisect_left(self.ring, (hash_val, vnode_key))
            if idx < len(self.ring) and self.ring[idx] == (hash_val, vnode_key):
                self.ring.pop(idx)
            
            del self.virtual_to_physical[vnode_key]
        
        del self.physical_to_virtuals[node]
        del self.weights[node]
    
    def get_node(self, key: str) -> Optional[str]:
        """
        获取 key 所属的物理节点
        
        在环上顺时针查找第一个虚拟节点
        """
        if not self.ring:
            return None
        
        hash_val = self.hash_fn(key)
        
        # 二分查找第一个大于等于 hash_val 的位置
        idx = bisect.bisect_left(self.ring, (hash_val, ''))
        
        # 如果超出环尾,回到环首
        if idx >= len(self.ring):
            idx = 0
        
        vnode_key = self.ring[idx][1]
        return self.virtual_to_physical.get(vnode_key)
    
    def get_nodes(self, key: str, count: int = 1) -> List[str]:
        """
        获取 key 所属的多个物理节点(用于副本放置)
        
        返回顺时针方向上的 count 个不同物理节点
        """
        if not self.ring or count <= 0:
            return []
        
        result = []
        seen = set()
        
        hash_val = self.hash_fn(key)
        idx = bisect.bisect_left(self.ring, (hash_val, ''))
        
        # 从 idx 开始顺时针遍历
        for i in range(len(self.ring)):
            ring_idx = (idx + i) % len(self.ring)
            vnode_key = self.ring[ring_idx][1]
            physical_node = self.virtual_to_physical[vnode_key]
            
            if physical_node not in seen:
                seen.add(physical_node)
                result.append(physical_node)
                
                if len(result) >= count:
                    break
        
        return result
    
    def get_node_load(self) -> Dict[str, float]:
        """
        获取每个物理节点的理论负载比例
        """
        if not self.ring:
            return {}
        
        total_vnodes = len(self.ring)
        load = {}
        
        for vnode_key in self.virtual_to_physical:
            physical = self.virtual_to_physical[vnode_key]
            load[physical] = load.get(physical, 0) + 1
        
        return {k: v / total_vnodes for k, v in load.items()}
    
    def get_nodes_count(self) -> int:
        """获取物理节点数"""
        return len(self.physical_to_virtuals)
    
    def __str__(self):
        nodes = list(self.physical_to_virtuals.keys())
        return f"HashRing(nodes={nodes}, total_vnodes={len(self.ring)})"

2.2 带副本的一致性哈希

复制代码
# minikv/hashring/replicated_hash.py
from typing import List, Optional
from .consistent_hash import HashRing

class ReplicatedHashRing(HashRing):
    """
    带副本的一致性哈希环
    
    每个 key 映射到多个节点,实现数据冗余
    """
    
    def __init__(self, nodes: List[str] = None,
                 replication_factor: int = 3,
                 virtual_nodes: int = 160):
        """
        Args:
            nodes: 初始节点列表
            replication_factor: 副本数
            virtual_nodes: 每个物理节点的虚拟节点数
        """
        super().__init__(nodes, virtual_nodes)
        self.replication_factor = replication_factor
    
    def get_replica_nodes(self, key: str) -> List[str]:
        """
        获取 key 的所有副本节点
        
        返回 replication_factor 个不同的物理节点
        """
        return self.get_nodes(key, self.replication_factor)
    
    def is_primary(self, key: str, node: str) -> bool:
        """
        判断指定节点是否是 key 的主节点
        """
        primary = self.get_node(key)
        return primary == node
    
    def is_replica(self, key: str, node: str) -> bool:
        """
        判断指定节点是否是 key 的副本节点
        """
        replicas = self.get_replica_nodes(key)
        return node in replicas

三、数据分布管理器

3.1 分片管理器

复制代码
# minikv/hashring/shard_manager.py
import threading
import logging
from typing import Dict, List, Optional, Set, Callable
from dataclasses import dataclass, field
from .replicated_hash import ReplicatedHashRing

logger = logging.getLogger(__name__)

@dataclass
class ShardInfo:
    """分片信息"""
    shard_id: str
    primary_node: str
    replica_nodes: List[str]
    key_range_start: str = ""
    key_range_end: str = ""
    size_bytes: int = 0
    key_count: int = 0

class ShardManager:
    """
    分片管理器
    
    负责:
    1. 数据分布计算
    2. 分片迁移决策
    3. 负载均衡
    """
    
    def __init__(self, local_node_id: str,
                 replication_factor: int = 3,
                 virtual_nodes: int = 160):
        self.local_node_id = local_node_id
        self.replication_factor = replication_factor
        self.hash_ring = ReplicatedHashRing(
            replication_factor=replication_factor,
            virtual_nodes=virtual_nodes
        )
        
        # 分片信息
        self.shards: Dict[str, ShardInfo] = {}
        
        # 本节点负责的分片
        self.local_shards: Set[str] = set()
        
        # 迁移中的分片
        self.migrating_shards: Dict[str, str] = {}  # shard_id -> target_node
        
        self.lock = threading.RLock()
        
        # 回调
        self.on_shard_moved: Optional[Callable] = None
    
    def add_node(self, node_id: str, weight: int = 1):
        """添加节点"""
        with self.lock:
            self.hash_ring.add_node(node_id, weight)
            self._rebalance_local_shards()
            logger.info(f"添加节点: {node_id}")
    
    def remove_node(self, node_id: str):
        """移除节点"""
        with self.lock:
            # 记录需要迁移的分片
            shards_to_move = [
                sid for sid, info in self.shards.items()
                if info.primary_node == node_id
            ]
            
            self.hash_ring.remove_node(node_id)
            
            # 触发迁移
            for shard_id in shards_to_move:
                self._migrate_shard(shard_id)
            
            logger.info(f"移除节点: {node_id}")
    
    def get_shard_for_key(self, key: str) -> Optional[ShardInfo]:
        """
        获取 key 所在的分片
        """
        primary = self.hash_ring.get_node(key)
        if not primary:
            return None
        
        # 查找对应的分片
        for shard in self.shards.values():
            if shard.primary_node == primary:
                return shard
        
        return None
    
    def get_replicas_for_key(self, key: str) -> List[str]:
        """
        获取 key 的副本节点列表
        """
        return self.hash_ring.get_replica_nodes(key)
    
    def should_handle_key(self, key: str) -> bool:
        """
        判断本节点是否应该处理该 key
        """
        return self.hash_ring.is_replica(key, self.local_node_id)
    
    def _rebalance_local_shards(self):
        """重新平衡本节点负责的分片"""
        # 获取本节点现在应该负责的所有 key 范围
        # 简化实现:重新计算所有分片归属
        old_shards = self.local_shards.copy()
        new_shards = set()
        
        for shard_id, info in self.shards.items():
            new_primary = self.hash_ring.get_node(shard_id)
            if new_primary == self.local_node_id:
                new_shards.add(shard_id)
        
        self.local_shards = new_shards
        
        # 记录需要迁出的分片
        shards_to_remove = old_shards - new_shards
        for shard_id in shards_to_remove:
            logger.info(f"分片 {shard_id} 将从本节点迁出")
    
    def _migrate_shard(self, shard_id: str):
        """迁移分片到新的主节点"""
        shard = self.shards.get(shard_id)
        if not shard:
            return
        
        new_primary = self.hash_ring.get_node(shard_id)
        if new_primary == shard.primary_node:
            return  # 不需要迁移
        
        self.migrating_shards[shard_id] = new_primary
        logger.info(f"开始迁移分片 {shard_id}: {shard.primary_node} → {new_primary}")
        
        # 更新分片信息
        shard.primary_node = new_primary
        
        if self.on_shard_moved:
            self.on_shard_moved(shard_id, new_primary)
        
        del self.migrating_shards[shard_id]
    
    def get_distribution(self) -> Dict[str, Dict]:
        """
        获取数据分布情况
        """
        distribution = {}
        for node_id in self.hash_ring.physical_to_virtuals:
            shards_for_node = [
                sid for sid, info in self.shards.items()
                if info.primary_node == node_id
            ]
            distribution[node_id] = {
                'shard_count': len(shards_for_node),
                'shards': shards_for_node,
                'load': self.hash_ring.get_node_load().get(node_id, 0)
            }
        return distribution
    
    def get_stats(self) -> dict:
        """获取统计信息"""
        with self.lock:
            return {
                'nodes': self.hash_ring.get_nodes_count(),
                'shards': len(self.shards),
                'local_shards': len(self.local_shards),
                'migrating': len(self.migrating_shards),
                'distribution': self.get_distribution()
            }

四、负载均衡器

4.1 自适应负载均衡

复制代码
# minikv/hashring/load_balancer.py
import threading
import time
import logging
from typing import Dict, List, Optional
from dataclasses import dataclass
from .shard_manager import ShardManager

logger = logging.getLogger(__name__)

@dataclass
class NodeLoad:
    """节点负载信息"""
    node_id: str
    cpu_usage: float = 0.0
    memory_usage: float = 0.0
    disk_usage: float = 0.0
    request_count: int = 0
    avg_latency_ms: float = 0.0
    shard_count: int = 0

class LoadBalancer:
    """
    自适应负载均衡器
    
    根据节点负载动态调整权重和分片分布
    """
    
    def __init__(self, shard_manager: ShardManager,
                 balance_interval: float = 30.0,
                 max_load_diff: float = 0.2):
        self.shard_manager = shard_manager
        self.balance_interval = balance_interval
        self.max_load_diff = max_load_diff
        
        self.node_loads: Dict[str, NodeLoad] = {}
        self.running = False
        self.lock = threading.Lock()
    
    def start(self):
        """启动负载均衡"""
        self.running = True
        thread = threading.Thread(target=self._balance_loop, daemon=True)
        thread.start()
        logger.info("负载均衡器启动")
    
    def stop(self):
        """停止负载均衡"""
        self.running = False
    
    def report_load(self, node_id: str, load: NodeLoad):
        """上报节点负载"""
        with self.lock:
            self.node_loads[node_id] = load
    
    def _balance_loop(self):
        """负载均衡循环"""
        while self.running:
            time.sleep(self.balance_interval)
            self._try_balance()
    
    def _try_balance(self):
        """尝试进行负载均衡"""
        with self.lock:
            if len(self.node_loads) < 2:
                return
            
            # 找出最忙和最闲的节点
            loads = list(self.node_loads.values())
            loads.sort(key=lambda x: x.request_count)
            
            min_load = loads[0]
            max_load = loads[-1]
            
            # 如果负载差异超过阈值,触发 rebalance
            if max_load.request_count > 0:
                ratio = min_load.request_count / max_load.request_count
                if ratio < (1 - self.max_load_diff):
                    logger.info(f"触发负载均衡: {min_load.node_id}({min_load.request_count}) "
                               f"vs {max_load.node_id}({max_load.request_count})")
                    self._move_shard(min_load.node_id, max_load.node_id)
    
    def _move_shard(self, from_node: str, to_node: str):
        """将一个分片从繁忙节点移到空闲节点"""
        # 获取繁忙节点的一个分片
        distribution = self.shard_manager.get_distribution()
        from_shards = distribution.get(from_node, {}).get('shards', [])
        
        if not from_shards:
            return
        
        # 移动第一个分片
        shard_id = from_shards[0]
        logger.info(f"移动分片 {shard_id}: {from_node} → {to_node}")
        
        # 这里触发实际的数据迁移
        # ... (具体迁移逻辑在后续实现)

五、集成到节点

5.1 增强节点类

复制代码
# minikv/node_with_sharding.py
from typing import List, Optional
from .node import MiniKVNode
from .hashring.shard_manager import ShardManager
from .hashring.load_balancer import LoadBalancer, NodeLoad

class MiniKVShardedNode(MiniKVNode):
    """带分片的 MiniKV 节点"""
    
    def __init__(self, node_id: str, host: str, port: int,
                 replication_factor: int = 3):
        super().__init__(node_id, host, port)
        
        # 分片管理
        self.shard_manager = ShardManager(
            local_node_id=node_id,
            replication_factor=replication_factor
        )
        
        # 负载均衡
        self.load_balancer = LoadBalancer(self.shard_manager)
        
        # 本节点存储的数据(按分片组织)
        self.shard_data: dict = {}
    
    def start(self):
        """启动节点"""
        super().start()
        
        # 将自己加入分片环
        self.shard_manager.add_node(self.node_id)
        
        # 启动负载均衡
        self.load_balancer.start()
        
        # 启动负载上报
        self._start_load_reporting()
    
    def stop(self):
        """停止节点"""
        self.load_balancer.stop()
        super().stop()
    
    def on_node_join(self, node_info):
        """节点加入时更新分片"""
        super().on_node_join(node_info)
        self.shard_manager.add_node(node_info.node_id)
    
    def on_node_leave(self, node_info):
        """节点离开时更新分片"""
        super().on_node_leave(node_info)
        self.shard_manager.remove_node(node_info.node_id)
    
    def put(self, key: str, value: str) -> bool:
        """
        存储键值对
        
        根据一致性哈希决定存储到哪个节点
        """
        if self.shard_manager.should_handle_key(key):
            # 本节点负责
            shard = self.shard_manager.get_shard_for_key(key)
            if shard:
                if shard.shard_id not in self.shard_data:
                    self.shard_data[shard.shard_id] = {}
                self.shard_data[shard.shard_id][key] = value
                return True
        else:
            # 转发到目标节点
            target = self.shard_manager.hash_ring.get_node(key)
            if target and target != self.node_id:
                # 通过网络转发
                msg = self._create_kv_message('PUT', key, value)
                self.transport.send_to(target, msg)
                return True
        
        return False
    
    def get(self, key: str) -> Optional[str]:
        """
        获取键值对
        """
        if self.shard_manager.should_handle_key(key):
            shard = self.shard_manager.get_shard_for_key(key)
            if shard and shard.shard_id in self.shard_data:
                return self.shard_data[shard.shard_id].get(key)
        else:
            target = self.shard_manager.hash_ring.get_node(key)
            if target and target != self.node_id:
                msg = self._create_kv_message('GET', key)
                # 发送请求并等待响应
                # ... (异步处理)
                pass
        
        return None
    
    def _start_load_reporting(self):
        """启动负载上报"""
        import threading
        import time
        
        def report_loop():
            while True:
                time.sleep(5)
                # 计算本节点负载
                load = NodeLoad(
                    node_id=self.node_id,
                    shard_count=len(self.shard_data)
                )
                self.load_balancer.report_load(self.node_id, load)
        
        thread = threading.Thread(target=report_loop, daemon=True)
        thread.start()
    
    def _create_kv_message(self, operation: str, key: str, value: str = None):
        """创建 KV 操作消息"""
        from .transport.message import Message, MessageType
        
        msg_type_map = {
            'GET': MessageType.GET,
            'PUT': MessageType.PUT,
            'DELETE': MessageType.DELETE,
        }
        
        body = {'key': key}
        if value is not None:
            body['value'] = value
        
        return Message(
            msg_type=msg_type_map.get(operation, MessageType.GET),
            sender_id=self.node_id,
            body=body
        )

六、完整演示

复制代码
# examples/sharding_demo.py
import time
import logging
import sys

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(name)s: %(message)s'
)

sys.path.insert(0, '..')

from minikv.hashring.consistent_hash import HashRing
from minikv.hashring.replicated_hash import ReplicatedHashRing
from minikv.hashring.shard_manager import ShardManager

def demo_hash_ring():
    """演示一致性哈希环"""
    print("=" * 60)
    print("🎯 一致性哈希环演示")
    print("=" * 60)
    
    # 创建哈希环
    ring = HashRing(
        nodes=['node-A', 'node-B', 'node-C'],
        virtual_nodes=10  # 为了演示效果,使用较少虚拟节点
    )
    
    print("\n📊 初始节点分布:")
    load = ring.get_node_load()
    for node, ratio in sorted(load.items()):
        bar = '█' * int(ratio * 50)
        print(f"   {node}: {bar} {ratio:.1%}")
    
    # 测试 key 分布
    print("\n🔑 Key 分布测试:")
    test_keys = [f"user:{i}" for i in range(20)]
    distribution = {}
    
    for key in test_keys:
        node = ring.get_node(key)
        distribution[node] = distribution.get(node, 0) + 1
    
    for node, count in sorted(distribution.items()):
        bar = '█' * count
        print(f"   {node}: {bar} ({count} keys)")
    
    # 添加新节点
    print("\n➕ 添加 node-D:")
    ring.add_node('node-D')
    
    # 检查哪些 key 迁移了
    migrated = 0
    for key in test_keys:
        new_node = ring.get_node(key)
        old_node = distribution.get(key)
        if new_node != old_node:
            migrated += 1
    
    print(f"   迁移的 key 数: {migrated}/{len(test_keys)}")
    print(f"   (传统取模会迁移 {len(test_keys)} 个)")
    
    # 移除节点
    print("\n➖ 移除 node-B:")
    ring.remove_node('node-B')
    print(f"   剩余节点: {ring.get_nodes_count()}")

def demo_replication():
    """演示副本放置"""
    print("\n" + "=" * 60)
    print("📋 副本放置演示")
    print("=" * 60)
    
    ring = ReplicatedHashRing(
        nodes=['node-1', 'node-2', 'node-3', 'node-4', 'node-5'],
        replication_factor=3,
        virtual_nodes=100
    )
    
    test_keys = ['user:1001', 'order:2024001', 'product:X-200']
    
    print("\nKey 的副本分布:")
    for key in test_keys:
        replicas = ring.get_replica_nodes(key)
        primary = ring.get_node(key)
        print(f"\n   📌 {key}:")
        print(f"      主节点: {primary}")
        print(f"      副本: {replicas}")

def demo_shard_manager():
    """演示分片管理器"""
    print("\n" + "=" * 60)
    print("🗂️  分片管理器演示")
    print("=" * 60)
    
    manager = ShardManager(
        local_node_id='node-1',
        replication_factor=2
    )
    
    # 添加节点
    print("\n📡 添加节点:")
    for i in range(1, 4):
        manager.add_node(f'node-{i}')
        print(f"   添加 node-{i}")
    
    # 查看分布
    print("\n📊 数据分布:")
    dist = manager.get_distribution()
    for node, info in dist.items():
        print(f"   {node}: 负载={info['load']:.1%}")
    
    # 模拟节点故障
    print("\n💥 模拟 node-2 故障:")
    manager.remove_node('node-2')
    
    print("\n📊 故障后分布:")
    dist = manager.get_distribution()
    for node, info in dist.items():
        print(f"   {node}: 负载={info['load']:.1%}")

if __name__ == "__main__":
    demo_hash_ring()
    demo_replication()
    demo_shard_manager()

七、测试

复制代码
# tests/test_hashring.py
import unittest
from minikv.hashring.consistent_hash import HashRing
from minikv.hashring.replicated_hash import ReplicatedHashRing

class TestHashRing(unittest.TestCase):
    """一致性哈希测试"""
    
    def setUp(self):
        self.ring = HashRing(
            nodes=['A', 'B', 'C'],
            virtual_nodes=1000
        )
    
    def test_key_distribution(self):
        """测试 key 分布均匀性"""
        keys = [f"key:{i}" for i in range(10000)]
        distribution = {}
        
        for key in keys:
            node = self.ring.get_node(key)
            distribution[node] = distribution.get(node, 0) + 1
        
        # 检查分布是否相对均匀(最大偏差不超过 20%)
        counts = list(distribution.values())
        avg = sum(counts) / len(counts)
        max_deviation = max(abs(c - avg) / avg for c in counts)
        
        self.assertLess(max_deviation, 0.2)
    
    def test_minimal_migration(self):
        """测试节点增减时迁移最小化"""
        keys = [f"key:{i}" for i in range(1000)]
        
        # 记录原始分布
        original = {key: self.ring.get_node(key) for key in keys}
        
        # 添加节点
        self.ring.add_node('D')
        
        # 统计迁移
        migrated = sum(1 for key in keys if self.ring.get_node(key) != original[key])
        
        # 迁移量应小于 1/N(约 25%)
        self.assertLess(migrated / len(keys), 0.3)
    
    def test_consistency(self):
        """测试一致性:同一个 key 始终映射到同一个节点"""
        key = "test_key"
        first = self.ring.get_node(key)
        
        for _ in range(100):
            self.assertEqual(self.ring.get_node(key), first)
    
    def test_empty_ring(self):
        """测试空环"""
        empty_ring = HashRing()
        self.assertIsNone(empty_ring.get_node("key"))
        self.assertEqual(empty_ring.get_nodes_count(), 0)
    
    def test_node_weight(self):
        """测试节点权重"""
        ring = HashRing(virtual_nodes=100)
        ring.add_node('heavy', weight=3)
        ring.add_node('light', weight=1)
        
        load = ring.get_node_load()
        heavy_load = load.get('heavy', 0)
        light_load = load.get('light', 0)
        
        # 重节点的负载应该是轻节点的约 3 倍
        self.assertAlmostEqual(heavy_load / light_load, 3.0, delta=0.5)

class TestReplicatedHashRing(unittest.TestCase):
    """副本哈希环测试"""
    
    def setUp(self):
        self.ring = ReplicatedHashRing(
            nodes=['A', 'B', 'C', 'D', 'E'],
            replication_factor=3
        )
    
    def test_replication_factor(self):
        """测试副本数"""
        replicas = self.ring.get_replica_nodes("some_key")
        self.assertEqual(len(replicas), 3)
        self.assertEqual(len(set(replicas)), 3)  # 不能有重复节点
    
    def test_primary_is_in_replicas(self):
        """测试主节点在副本列表中"""
        key = "test_key"
        primary = self.ring.get_node(key)
        replicas = self.ring.get_replica_nodes(key)
        
        self.assertIn(primary, replicas)

if __name__ == "__main__":
    unittest.main()

八、总结

这一讲我们实现了一致性哈希:

组件 功能
哈希环 虚拟节点、权重、有序环
副本管理 多副本放置、主从识别
分片管理 分片分配、迁移、负载均衡
自适应均衡 根据负载动态调整分布

关键成果:

  • 节点增减时,仅迁移 1/N 的数据(传统取模是全部迁移)

  • 通过虚拟节点解决了数据倾斜问题

  • 支持权重,不同性能的节点承担不同负载

  • 实现了多副本,为后续的容错打下基础

下一讲 :我们将实现分布式系统的核心------Raft 共识算法,让集群在节点故障时仍能保持一致。


🧰 开发之余的小工具推荐

处理 Base64、JWT 解析、JSON 格式化、Crontab 计算、PDF 合并压缩这些碎片需求,我常用一个纯前端本地工具箱:zz365.top(子页 PDF 大师:PDF 大师 - zz365工具箱)。所有计算在浏览器完成,文件不上传服务器,关页即清。免费、无登录、无广告,适合开发者当常驻标签页。

相关推荐
阿维的博客日记1 小时前
保姆级教程介绍分词算法BPE
人工智能·算法·分词算法·bpe
Tongzhi20262 小时前
通芝科技无感考勤一体机:软硬一体,开箱即用
数据结构·数据库·科技·算法·均值算法·数据库开发
地平线开发者2 小时前
BEVdet模型解析
算法·自动驾驶
tudousisi2222 小时前
P2758 编辑距离 题解复盘
算法
李可以量化2 小时前
Tornado 从了解到精通(四)上:实战搭建 Web 应用与核心组件详解
算法
linux-hzh3 小时前
百日算法修炼 · Day 09
数据结构·算法·排序算法
DFT计算杂谈3 小时前
Janus单层Cr2SSe中的应变可调多压电效应与谷电子学
人工智能·算法·机器学习
今天AI了吗3 小时前
从 LLM 到 Agent Skill:把 AI 底层概念串起来
数据库·人工智能·sql·深度学习·神经网络·算法·机器学习
hanlin033 小时前
动态规划专练:力扣第1035、392题
算法·leetcode·动态规划