架构之一致性哈希
定义
一致性哈希(Consistent Hashing)是一种分布式哈希算法,用于解决分布式缓存和数据分片中的数据分布问题。该算法通过将数据和节点映射到同一个哈希环上,实现了在节点动态增减时最小化数据迁移的效果。
在数据分片场景中,一致性哈希算法按照某个维度将存放在单一数据库中的数据,分散地存放至多个数据库或表中,以达到提升性能瓶颈以及可用性的效果。当需要扩容或缩容节点时,一致性哈希能够保证大部分数据不需要重新分配,只有少量数据需要在相邻节点间迁移。
核心原理
2.1 数据分片概述
数据分片指按照某个维度将存放在单一数据库中的数据,分散地存放至多个数据库或表中以达到提升性能瓶颈以及可用性的效果。主要的分片算法包括:
| 分片算法 | 说明 | 适用场景 |
|---|---|---|
| Range 分片 | 按照数据范围进行分片 | 时间序列数据、有序数据 |
| ID 取模分片 | 使用 ID 对节点数取余 | 关系型数据库设计 |
| Hash 哈希分片 | 使用哈希算法进行分片 | 缓存系统、分布式存储 |
2.2 一致性哈希环
一致性哈希将哈希值空间组织成一个环,通常范围为 0 到 2^32。每个数据节点和数据键都通过哈希函数映射到环上的某个位置。数据存储规则是:将数据键的哈希值沿顺时针方向查找,遇到的第一个节点即为该数据的存储节点。
2.3 一致性哈希的核心特性
| 特性 | 说明 |
|---|---|
| 单调性 | 当节点数量增加或减少时,只影响相邻节点的数据分布 |
| 平衡性 | 数据尽可能均匀地分布在各个节点上 |
| 分散性 | 相同的数据键总是映射到同一个节点 |
| 负载均衡 | 通过虚拟节点机制实现更均衡的数据分布 |
分片算法详解
3.1 Range 分片
3.1.1 基本原理
Range 分片按照数据范围进行分片,每个节点负责一段连续的数据范围。通常按照时间范围或数据范围来划分。
示例:将 1 到 100 的数字保存到 3 个节点
| 节点 | 数据范围 | 说明 |
|---|---|---|
| 节点 1 | 1 - 33 | 负责前三分之一数据 |
| 节点 2 | 34 - 66 | 负责中间三分之一数据 |
| 节点 3 | 67 - 100 | 负责后三分之一数据 |
3.1.2 代码实现
public class RangeShardingStrategy {
private final List<ShardNode> nodes;
public RangeShardingStrategy(List<ShardNode> nodes) {
this.nodes = nodes;
// 按范围排序
this.nodes.sort(Comparator.comparing(ShardNode::getStartRange));
}
public ShardNode getShard(long key) {
for (ShardNode node : nodes) {
if (key >= node.getStartRange() && key <= node.getEndRange()) {
return node;
}
}
throw new IllegalArgumentException("Key out of range: " + key);
}
public static class ShardNode {
private final String nodeId;
private final long startRange;
private final long endRange;
public ShardNode(String nodeId, long startRange, long endRange) {
this.nodeId = nodeId;
this.startRange = startRange;
this.endRange = endRange;
}
// getters
public String getNodeId() { return nodeId; }
public long getStartRange() { return startRange; }
public long getEndRange() { return endRange; }
}
}
3.1.3 优缺点分析
| 优点 | 缺点 |
|---|---|
| 实现简单,易于理解 | 容易发生数据倾斜 |
| 查询范围数据效率高 | 最新数据可能成为热点 |
| 数据局部性好 | 扩容时需要大量数据迁移 |
数据倾斜问题:在时间序列数据中,大量流量可能集中在最新的数据上,导致某些节点负载过高。
3.2 ID 取模分片
3.2.1 基本原理
ID 取模分片将数据分成 n 份(通常节点数也为 n),通过对数据 ID 进行取余运算,将数据均匀分布于各个表中或节点上。
公式:shardIndex = key % nodeCount
示例:100 个数据分配到 3 个节点
| 数据 ID | 取余结果 | 目标节点 |
|---|---|---|
| 1 | 1 % 3 = 1 | 节点 1 |
| 2 | 2 % 3 = 2 | 节点 2 |
| 3 | 3 % 3 = 0 | 节点 3 |
| 4 | 4 % 3 = 1 | 节点 1 |
| … | … | … |
3.2.2 代码实现
public class ModuloShardingStrategy {
private final List<ShardNode> nodes;
private final int nodeCount;
public ModuloShardingStrategy(List<ShardNode> nodes) {
this.nodes = nodes;
this.nodeCount = nodes.size();
}
public ShardNode getShard(long key) {
int index = (int) (key % nodeCount);
return nodes.get(index);
}
// 扩容时建议翻倍扩容
public void addNode(ShardNode newNode) {
nodes.add(newNode);
// 需要重新计算所有数据的位置
redistributeData();
}
private void redistributeData() {
// 扩容时需要迁移大量数据
// 例如从 3 个节点扩容到 6 个节点
// 需要迁移约 50% 的数据
}
public static class ShardNode {
private final String nodeId;
public ShardNode(String nodeId) {
this.nodeId = nodeId;
}
public String getNodeId() { return nodeId; }
}
}
3.2.3 优缺点分析
| 优点 | 缺点 |
|---|---|
| 配置简单,易于实现 | 节点伸缩时数据迁移量大 |
| 数据分布均匀 | 扩容时影响范围大 |
| 常用于关系型数据库设计 | 建议翻倍扩容以减少迁移 |
扩容建议:采用翻倍扩容策略,例如从 3 个节点扩容到 6 个节点,这样可以减少数据迁移的复杂度。
3.3 Hash 哈希分片
3.3.1 基本原理
Hash 哈希分片使用哈希算法对 key 进行运算,然后按照规则进行分片。这样可以保证数据被打散,同时保证数据分布比较均匀。
哈希分布方式分为三种:
- 哈希取余分片:对哈希结果取余
- 一致性哈希分片:使用哈希环和顺时针查找
- 虚拟槽分片:使用虚拟槽位进行数据分布
3.3.2 哈希取余分片
示例:100 个数据分配到 3 个节点
public class HashModuloShardingStrategy {
private final List<ShardNode> nodes;
private final int nodeCount;
public HashModuloShardingStrategy(List<ShardNode> nodes) {
this.nodes = nodes;
this.nodeCount = nodes.size();
}
public ShardNode getShard(String key) {
int hash = hash(key);
int index = Math.abs(hash) % nodeCount;
return nodes.get(index);
}
private int hash(String key) {
// 使用 Java 的 hashCode 或自定义哈希函数
return key.hashCode();
}
public static class ShardNode {
private final String nodeId;
public ShardNode(String nodeId) {
this.nodeId = nodeId;
}
public String getNodeId() { return nodeId; }
}
}
| 优点 | 缺点 |
|---|---|
| 配置简单 | 节点伸缩时导致数据迁移 |
| 数据分布均匀 | 迁移数量与添加节点数相关 |
| 适合大规模数据场景 | 建议翻倍扩容 |
一致性哈希分片
4.1 一致性哈希原理
一致性哈希将所有的数据当做一个 token 环,token 环中的数据范围是 0 到 2^32。然后为每一个数据节点分配一个 token 范围值,这个节点就负责保存这个范围内的数据。
对每一个 key 进行 hash 运算,被哈希后的结果在哪个 token 的范围内,则按顺时针去找最近的节点,这个 key 将会被保存在这个节点上。
4.2 一致性哈希的节点扩容
当在环上添加新节点时,只有新节点和其顺时针方向的相邻节点之间的数据需要迁移,其他节点的数据不受影响。
扩容影响分析:
- 有 4 个 key 被 hash 之后的值在 N1 节点和 N2 节点之间,按照顺时针规则,这 4 个 key 都会被保存在 N2 节点上
- 如果在 N1 节点和 N2 节点之间添加 N5 节点,当下次有 key 被 hash 之后的值在 N1 节点和 N5 节点之间,这些 key 就会被保存在 N5 节点上面了
- 数据迁移会在 N1 节点和 N2 节点之间进行
- N3 节点和 N4 节点不受影响
- 数据迁移范围被缩小很多
节点数量与扩容影响:
- 如果有 1000 个节点,此时添加一个节点,受影响的节点范围最多只有千分之 2
- 一致性哈希一般用在节点比较多的时候,节点越多,扩容时受影响的节点范围越少
4.3 一致性哈希实现
import java.util.*;
public class ConsistentHashing {
private final TreeMap<Long, String> hashRing;
private final int virtualNodeCount;
private final HashFunction hashFunction;
public ConsistentHashing(int virtualNodeCount) {
this.hashRing = new TreeMap<>();
this.virtualNodeCount = virtualNodeCount;
this.hashFunction = new MurmurHashFunction();
}
/**
* 添加节点到哈希环
* @param node 节点标识
*/
public void addNode(String node) {
for (int i = 0; i < virtualNodeCount; i++) {
String virtualNode = node + "#" + i;
long hash = hashFunction.hash(virtualNode);
hashRing.put(hash, node);
}
}
/**
* 从哈希环移除节点
* @param node 节点标识
*/
public void removeNode(String node) {
for (int i = 0; i < virtualNodeCount; i++) {
String virtualNode = node + "#" + i;
long hash = hashFunction.hash(virtualNode);
hashRing.remove(hash);
}
}
/**
* 获取数据对应的节点
* @param key 数据键
* @return 目标节点
*/
public String getNode(String key) {
if (hashRing.isEmpty()) {
throw new IllegalStateException("No nodes available");
}
long hash = hashFunction.hash(key);
// 顺时针查找第一个大于等于 hash 的节点
Map.Entry<Long, String> entry = hashRing.ceilingEntry(hash);
// 如果没有找到,则返回环上的第一个节点
if (entry == null) {
entry = hashRing.firstEntry();
}
return entry.getValue();
}
/**
* 获取所有节点
* @return 节点集合
*/
public Set<String> getAllNodes() {
return new HashSet<>(hashRing.values());
}
/**
* 哈希函数接口
*/
public interface HashFunction {
long hash(String key);
}
/**
* MurmurHash 实现
*/
public static class MurmurHashFunction implements HashFunction {
@Override
public long hash(String key) {
byte[] bytes = key.getBytes();
return murmurHash32(bytes, 0, bytes.length, 0x9747b28c) & 0xFFFFFFFFL;
}
private int murmurHash32(byte[] data, int offset, int length, int seed) {
final int c1 = 0xcc9e2d51;
final int c2 = 0x1b873593;
final int r1 = 15;
final int r2 = 13;
final int m = 5;
final int n = 0xe6546b64;
int hash = seed;
final int nblocks = length / 4;
for (int i = 0; i < nblocks; i++) {
int k = (data[offset + i * 4] & 0xff) |
((data[offset + i * 4 + 1] & 0xff) << 8) |
((data[offset + i * 4 + 2] & 0xff) << 16) |
((data[offset + i * 4 + 3] & 0xff) << 24);
k = k * c1;
k = Integer.rotateLeft(k, r1);
k = k * c2;
hash = hash ^ k;
hash = Integer.rotateLeft(hash, r2);
hash = hash * m + n;
}
int k1 = 0;
int tail = nblocks * 4;
switch (length & 0x03) {
case 3:
k1 ^= (data[offset + tail + 2] & 0xff) << 16;
case 2:
k1 ^= (data[offset + tail + 1] & 0xff) << 8;
case 1:
k1 ^= (data[offset + tail] & 0xff);
k1 = k1 * c1;
k1 = Integer.rotateLeft(k1, r1);
k1 = k1 * c2;
hash = hash ^ k1;
}
hash = hash ^ length;
hash = hash ^ (hash >>> 16);
hash = hash * 0x85ebca6b;
hash = hash ^ (hash >>> 13);
hash = hash * 0xc2b2ae35;
hash = hash ^ (hash >>> 16);
return hash;
}
}
}
4.4 虚拟节点机制
虚拟节点(Virtual Node)是解决一致性哈希数据倾斜问题的重要机制。通过为每个物理节点创建多个虚拟节点,可以更均匀地分布数据。
public class VirtualNodeConsistentHashing extends ConsistentHashing {
public VirtualNodeConsistentHashing(int virtualNodeCount) {
super(virtualNodeCount);
}
/**
* 分析节点负载分布
* @param testData 测试数据
* @return 节点负载统计
*/
public Map<String, Integer> analyzeDistribution(List<String> testData) {
Map<String, Integer> distribution = new HashMap<>();
for (String key : testData) {
String node = getNode(key);
distribution.put(node, distribution.getOrDefault(node, 0) + 1);
}
return distribution;
}
/**
* 计算负载均衡度
* @param distribution 节点负载分布
* @return 标准差(越小越均衡)
*/
public double calculateBalance(Map<String, Integer> distribution) {
if (distribution.isEmpty()) {
return 0;
}
double mean = distribution.values().stream()
.mapToInt(Integer::intValue)
.average()
.orElse(0);
double variance = distribution.values().stream()
.mapToDouble(count -> Math.pow(count - mean, 2))
.average()
.orElse(0);
return Math.sqrt(variance);
}
}
虚拟节点数量选择:
| 虚拟节点数 | 适用场景 | 负载均衡效果 |
|---|---|---|
| 10-50 | 小规模集群(<10 节点) | 基本均衡 |
| 50-150 | 中等规模集群(10-50 节点) | 良好均衡 |
| 150-500 | 大规模集群(>50 节点) | 优秀均衡 |
4.5 一致性哈希优缺点分析
| 优点 | 缺点 |
|---|---|
| 节点伸缩时只影响邻近节点 | 小规模场景下可能出现节点空闲 |
| 通过虚拟节点实现负载均衡 | 实现复杂度高于取模分片 |
| 适合大规模分布式系统 | 需要维护虚拟节点映射关系 |
| 数据迁移范围可控 | 哈希环的空间利用率可能不高 |
使用场景
5.1 典型应用场景
| 场景 | 说明 | 推荐算法 |
|---|---|---|
| 分布式缓存 | Redis Cluster、Memcached | 一致性哈希 |
| 分布式数据库 | MongoDB、Cassandra | 一致性哈希 |
| 负载均衡 | Nginx 一致性哈希负载均衡 | 一致性哈希 |
| 对象存储 | Amazon S3、阿里云 OSS | 一致性哈希 |
| 时间序列数据 | InfluxDB、TimescaleDB | Range 分片 |
| 关系型数据库 | MySQL 分库分表 | ID 取模分片 |
5.2 分片算法选择决策树
5.3 各算法对比
| 特性 | Range 分片 | ID 取模分片 | 哈希取余分片 | 一致性哈希分片 |
|---|---|---|---|---|
| 实现复杂度 | 低 | 低 | 低 | 中 |
| 数据分布均匀性 | 差 | 好 | 好 | 好(需虚拟节点) |
| 扩容影响 | 大 | 大 | 大 | 小 |
| 查询范围数据 | 高效 | 低效 | 低效 | 低效 |
| 适用节点规模 | 小 | 中 | 中 | 大 |
| 数据迁移量 | 大 | 大 | 大 | 小 |
最佳实践
6.1 一致性哈希配置原则
| 原则 | 说明 |
|---|---|
| 虚拟节点数量 | 根据物理节点数量设置,通常为物理节点的 10-100 倍 |
| 哈希函数选择 | 使用 MurmurHash、FNV 等分布均匀的哈希函数 |
| 节点标识 | 使用唯一且稳定的节点标识符 |
| 监控数据分布 | 定期检查各节点的数据分布情况 |
| 故障节点处理 | 及时检测并移除故障节点,重新分配数据 |
6.2 注意事项
-
避免频繁扩缩容
- 一致性哈希虽然减少了数据迁移量,但仍有迁移成本
- 批量进行节点变更,避免频繁操作
- 规划好节点容量,预留缓冲空间
-
合理选择虚拟节点数量
- 虚拟节点过多会增加内存开销
- 虚拟节点过少会导致数据分布不均
- 根据实际负载情况动态调整
-
监控数据分布
- 定期检查各节点的数据量和负载
- 及时发现和解决数据倾斜问题
- 建立自动化的数据重平衡机制
-
处理节点故障
- 及时检测并移除故障节点
- 实现自动化的故障转移机制
- 保证数据迁移的一致性
-
考虑数据局部性
- 相关数据尽可能存储在同一节点
- 减少跨节点查询
- 优化查询性能
代码示例:完整实现
7.1 分布式缓存一致性哈希实现
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class DistributedCacheCluster {
private final ConsistentHashing hashRing;
private final Map<String, CacheNode> cacheNodes;
private final DataMigrationStrategy migrationStrategy;
private final ConsistentHashMonitor monitor;
public DistributedCacheCluster(int virtualNodeCount) {
this.hashRing = new ConsistentHashing(virtualNodeCount);
this.cacheNodes = new ConcurrentHashMap<>();
this.migrationStrategy = new DataMigrationStrategy(hashRing, hashRing);
this.monitor = new ConsistentHashMonitor(hashRing, new MetricsCollector());
// 启动监控线程
startMonitoring();
}
/**
* 添加缓存节点
* @param nodeId 节点 ID
* @param host 主机地址
* @param port 端口
*/
public void addNode(String nodeId, String host, int port) {
CacheNode node = new CacheNode(nodeId, host, port);
cacheNodes.put(nodeId, node);
hashRing.addNode(nodeId);
// 触发数据迁移
triggerMigration(nodeId);
}
/**
* 移除缓存节点
* @param nodeId 节点 ID
*/
public void removeNode(String nodeId) {
CacheNode node = cacheNodes.get(nodeId);
if (node != null) {
// 触发数据迁移
redistributeData(nodeId);
cacheNodes.remove(nodeId);
hashRing.removeNode(nodeId);
}
}
/**
* 获取缓存值
* @param key 缓存键
* @return 缓存值
*/
public Object get(String key) {
String nodeId = hashRing.getNode(key);
CacheNode node = cacheNodes.get(nodeId);
if (node == null) {
throw new IllegalStateException("Node not found: " + nodeId);
}
return node.get(key);
}
/**
* 设置缓存值
* @param key 缓存键
* @param value 缓存值
*/
public void set(String key, Object value) {
String nodeId = hashRing.getNode(key);
CacheNode node = cacheNodes.get(nodeId);
if (node == null) {
throw new IllegalStateException("Node not found: " + nodeId);
}
node.set(key, value);
}
/**
* 删除缓存值
* @param key 缓存键
*/
public void delete(String key) {
String nodeId = hashRing.getNode(key);
CacheNode node = cacheNodes.get(nodeId);
if (node != null) {
node.delete(key);
}
}
/**
* 批量获取缓存值
* @param keys 缓存键集合
* @return 缓存值映射
*/
public Map<String, Object> mget(Collection<String> keys) {
// 按节点分组
Map<String, List<String>> groupedKeys = new HashMap<>();
for (String key : keys) {
String nodeId = hashRing.getNode(key);
groupedKeys.computeIfAbsent(nodeId, k -> new ArrayList<>()).add(key);
}
// 并行获取
Map<String, Object> result = new HashMap<>();
groupedKeys.entrySet().parallelStream().forEach(entry -> {
String nodeId = entry.getKey();
List<String> nodeKeys = entry.getValue();
CacheNode node = cacheNodes.get(nodeId);
if (node != null) {
result.putAll(node.mget(nodeKeys));
}
});
return result;
}
private void triggerMigration(String newNodeId) {
// 计算需要迁移的数据
Set<String> allKeys = getAllKeys();
Map<String, MigrationPlan> migrationPlan = migrationStrategy.calculateMigrationPlan(allKeys);
// 过滤出需要迁移到新节点的数据
Map<String, MigrationPlan> newNodeMigration = new HashMap<>();
for (MigrationPlan plan : migrationPlan.values()) {
if (plan.getTargetNode().equals(newNodeId)) {
newNodeMigration.put(plan.getKey(), plan);
}
}
// 执行迁移
migrationStrategy.executeMigration(newNodeMigration);
}
private void redistributeData(String removedNodeId) {
// 计算需要迁移的数据
Set<String> allKeys = getAllKeys();
Map<String, MigrationPlan> migrationPlan = migrationStrategy.calculateMigrationPlan(allKeys);
// 过滤出需要从移除节点迁移的数据
Map<String, MigrationPlan> removedNodeMigration = new HashMap<>();
for (MigrationPlan plan : migrationPlan.values()) {
if (plan.getSourceNode().equals(removedNodeId)) {
removedNodeMigration.put(plan.getKey(), plan);
}
}
// 执行迁移
migrationStrategy.executeMigration(removedNodeMigration);
}
private Set<String> getAllKeys() {
// 获取所有缓存键
Set<String> allKeys = new HashSet<>();
for (CacheNode node : cacheNodes.values()) {
allKeys.addAll(node.getAllKeys());
}
return allKeys;
}
private void startMonitoring() {
Thread monitorThread = new Thread(() -> {
while (true) {
try {
monitor.monitorDistribution();
monitor.monitorNodeHealth();
Thread.sleep(60000); // 每分钟检查一次
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
monitorThread.setDaemon(true);
monitorThread.start();
}
/**
* 缓存节点实现
*/
public static class CacheNode {
private final String nodeId;
private final String host;
private final int port;
private final Map<String, Object> data;
public CacheNode(String nodeId, String host, int port) {
this.nodeId = nodeId;
this.host = host;
this.port = port;
this.data = new ConcurrentHashMap<>();
}
public Object get(String key) {
return data.get(key);
}
public void set(String key, Object value) {
data.put(key, value);
}
public void delete(String key) {
data.remove(key);
}
public Map<String, Object> mget(Collection<String> keys) {
Map<String, Object> result = new HashMap<>();
for (String key : keys) {
Object value = data.get(key);
if (value != null) {
result.put(key, value);
}
}
return result;
}
public Set<String> getAllKeys() {
return new HashSet<>(data.keySet());
}
// getters
public String getNodeId() { return nodeId; }
public String getHost() { return host; }
public int getPort() { return port; }
}
/**
* 指标收集器
*/
public static class MetricsCollector {
private final Map<String, Integer> dataCountMap = new ConcurrentHashMap<>();
private final Map<String, Boolean> healthStatusMap = new ConcurrentHashMap<>();
public int getDataCount(String nodeId) {
return dataCountMap.getOrDefault(nodeId, 0);
}
public void setDataCount(String nodeId, int count) {
dataCountMap.put(nodeId, count);
}
public boolean isNodeHealthy(String nodeId) {
return healthStatusMap.getOrDefault(nodeId, true);
}
public void setNodeHealth(String nodeId, boolean healthy) {
healthStatusMap.put(nodeId, healthy);
}
public void recordMetric(String name, double value) {
// 记录指标到监控系统
}
}
}
7.2 Go 语言实现示例
package consistenthash
import (
"crypto/sha1"
"encoding/binary"
"sort"
"strconv"
"sync"
)
// ConsistentHash 一致性哈希实现
type ConsistentHash struct {
sync.RWMutex
virtualNodes int
ring []uint32
nodeMap map[uint32]string
nodeMapIndex map[string]bool
}
// New 创建新的一致性哈希实例
func New(virtualNodes int) *ConsistentHash {
return &ConsistentHash{
virtualNodes: virtualNodes,
ring: make([]uint32, 0),
nodeMap: make(map[uint32]string),
nodeMapIndex: make(map[string]bool),
}
}
// AddNode 添加节点到哈希环
func (ch *ConsistentHash) AddNode(node string) {
ch.Lock()
defer ch.Unlock()
if ch.nodeMapIndex[node] {
return
}
for i := 0; i < ch.virtualNodes; i++ {
virtualNode := node + "#" + strconv.Itoa(i)
hash := ch.hash(virtualNode)
ch.ring = append(ch.ring, hash)
ch.nodeMap[hash] = node
}
ch.nodeMapIndex[node] = true
sort.Slice(ch.ring, func(i, j int) bool {
return ch.ring[i] < ch.ring[j]
})
}
// RemoveNode 从哈希环移除节点
func (ch *ConsistentHash) RemoveNode(node string) {
ch.Lock()
defer ch.Unlock()
if !ch.nodeMapIndex[node] {
return
}
for i := 0; i < ch.virtualNodes; i++ {
virtualNode := node + "#" + strconv.Itoa(i)
hash := ch.hash(virtualNode)
delete(ch.nodeMap, hash)
}
delete(ch.nodeMapIndex, node)
// 重建环
newRing := make([]uint32, 0, len(ch.ring)-ch.virtualNodes)
for _, h := range ch.ring {
if _, exists := ch.nodeMap[h]; exists {
newRing = append(newRing, h)
}
}
ch.ring = newRing
}
// GetNode 获取数据对应的节点
func (ch *ConsistentHash) GetNode(key string) string {
ch.RLock()
defer ch.RUnlock()
if len(ch.ring) == 0 {
return ""
}
hash := ch.hash(key)
// 二分查找第一个大于等于 hash 的节点
index := sort.Search(len(ch.ring), func(i int) bool {
return ch.ring[i] >= hash
})
// 如果没有找到,则返回环上的第一个节点
if index == len(ch.ring) {
index = 0
}
return ch.nodeMap[ch.ring[index]]
}
// hash 哈希函数
func (ch *ConsistentHash) hash(key string) uint32 {
h := sha1.New()
h.Write([]byte(key))
hash := h.Sum(nil)
return binary.BigEndian.Uint32(hash[:4])
}
// GetAllNodes 获取所有节点
func (ch *ConsistentHash) GetAllNodes() []string {
ch.RLock()
defer ch.RUnlock()
nodes := make([]string, 0, len(ch.nodeMapIndex))
for node := range ch.nodeMapIndex {
nodes = append(nodes, node)
}
return nodes
}
// AnalyzeDistribution 分析数据分布
func (ch *ConsistentHash) AnalyzeDistribution(keys []string) map[string]int {
ch.RLock()
defer ch.RUnlock()
distribution := make(map[string]int)
for _, key := range keys {
node := ch.GetNode(key)
distribution[node]++
}
return distribution
}
7.3 Redis Cluster 一致性哈希模拟
import java.util.*;
public class RedisClusterSimulator {
private final ConsistentHashing hashRing;
private final Map<String, RedisNode> nodes;
private final int slots = 16384; // Redis Cluster 槽位数量
public RedisClusterSimulator() {
this.hashRing = new ConsistentHashing(40); // 每个节点 40 个虚拟节点
this.nodes = new HashMap<>();
}
/**
* 添加 Redis 节点
* @param nodeId 节点 ID
* @param host 主机地址
* @param port 端口
* @param slotRange 分配的槽位范围
*/
public void addNode(String nodeId, String host, int port, SlotRange slotRange) {
RedisNode node = new RedisNode(nodeId, host, port, slotRange);
nodes.put(nodeId, node);
hashRing.addNode(nodeId);
}
/**
* 根据键获取 Redis 节点
* @param key 键
* @return Redis 节点
*/
public RedisNode getNodeByKey(String key) {
// 提取哈希标签(如果有)
String hashTag = extractHashTag(key);
String keyToHash = hashTag != null ? hashTag : key;
// 计算槽位
int slot = calculateSlot(keyToHash);
// 查找负责该槽位的节点
for (RedisNode node : nodes.values()) {
if (node.getSlotRange().contains(slot)) {
return node;
}
}
throw new IllegalStateException("No node found for slot: " + slot);
}
/**
* 提取哈希标签
* Redis 支持 {tag} 语法,确保相同标签的键在同一节点
*/
private String extractHashTag(String key) {
int start = key.indexOf('{');
if (start == -1) {
return null;
}
int end = key.indexOf('}', start);
if (end == -1) {
return null;
}
return key.substring(start + 1, end);
}
/**
* 计算 Redis 槽位
*/
private int calculateSlot(String key) {
int hash = key.hashCode();
return Math.abs(hash) % slots;
}
/**
* 槽位迁移
*/
public void migrateSlot(int slot, String sourceNodeId, String targetNodeId) {
RedisNode sourceNode = nodes.get(sourceNodeId);
RedisNode targetNode = nodes.get(targetNodeId);
if (sourceNode == null || targetNode == null) {
throw new IllegalArgumentException("Invalid node ID");
}
// 获取该槽位上的所有键
Set<String> keys = sourceNode.getKeysBySlot(slot);
// 迁移数据
for (String key : keys) {
Object value = sourceNode.get(key);
targetNode.set(key, value);
sourceNode.delete(key);
}
// 更新槽位分配
sourceNode.getSlotRange().removeSlot(slot);
targetNode.getSlotRange().addSlot(slot);
}
/**
* Redis 节点
*/
public static class RedisNode {
private final String nodeId;
private final String host;
private final int port;
private final SlotRange slotRange;
private final Map<String, Object> data;
public RedisNode(String nodeId, String host, int port, SlotRange slotRange) {
this.nodeId = nodeId;
this.host = host;
this.port = port;
this.slotRange = slotRange;
this.data = new HashMap<>();
}
public Object get(String key) {
return data.get(key);
}
public void set(String key, Object value) {
data.put(key, value);
}
public void delete(String key) {
data.remove(key);
}
public Set<String> getKeysBySlot(int slot) {
Set<String> keys = new HashSet<>();
for (String key : data.keySet()) {
int keySlot = Math.abs(key.hashCode()) % 16384;
if (keySlot == slot) {
keys.add(key);
}
}
return keys;
}
// getters
public String getNodeId() { return nodeId; }
public String getHost() { return host; }
public int getPort() { return port; }
public SlotRange getSlotRange() { return slotRange; }
}
/**
* 槽位范围
*/
public static class SlotRange {
private final int start;
private final int end;
private final BitSet slots;
public SlotRange(int start, int end) {
this.start = start;
this.end = end;
this.slots = new BitSet(end - start + 1);
slots.set(0, end - start + 1);
}
public boolean contains(int slot) {
return slot >= start && slot <= end && slots.get(slot - start);
}
public void addSlot(int slot) {
if (slot >= start && slot <= end) {
slots.set(slot - start);
}
}
public void removeSlot(int slot) {
if (slot >= start && slot <= end) {
slots.clear(slot - start);
}
}
// getters
public int getStart() { return start; }
public int getEnd() { return end; }
}
}
总结
一致性哈希是分布式系统中解决数据分片和负载均衡的重要算法。通过将数据和节点映射到同一个哈希环上,实现了在节点动态增减时最小化数据迁移的效果。
核心要点
-
分片算法选择
- Range 分片:适合时间序列数据,但容易发生数据倾斜
- ID 取模分片:实现简单,适合关系型数据库,但扩容影响大
- 一致性哈希分片:适合大规模分布式系统,扩容影响小
-
一致性哈希优势
- 节点伸缩时只影响相邻节点,数据迁移范围可控
- 通过虚拟节点机制实现负载均衡
- 适合缓存系统、分布式数据库等场景
-
实施建议
- 合理设置虚拟节点数量,平衡负载和性能
- 建立完善的监控体系,及时发现数据倾斜问题
- 规划好节点容量,避免频繁扩缩容
- 实现自动化的数据迁移和故障转移机制
-
注意事项
- 小规模场景下可能出现节点空闲问题
- 需要维护虚拟节点映射关系,增加复杂度
- 哈希环的空间利用率可能不高
- 数据迁移仍需保证一致性
一致性哈希算法与分治模式结合,为分布式存储系统提供了高效、可靠的数据分片解决方案,是构建大规模分布式系统的重要技术基石。
772

被折叠的 条评论
为什么被折叠?



