文章目录
前言
netty没有提供官方的分布式解决方法,当实战中用户量过大,并发过大,带宽不够用时,采用一致性哈希对netty做分布式处理。
下面实现基于Guava的一致性哈希算法,用于Netty分布式分片场景,将 50 万用户均匀分配到3台服务器节点。
一致性Hash算法实现(基于Guava)
一致性哈希管理器 ConsistentHashManager
package com.zgb.consistenthash;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import lombok.Getter;
import lombok.Setter;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* 一致性哈希管理器,用于Netty分布式分片
* 基于Guava实现,负责将用户请求均匀分配到3台服务器节点
*/
@Component
public class ConsistentHashManager {
/**
* 虚拟节点数量,增加虚拟节点可以使哈希分布更均匀
*/
private static final int VIRTUAL_NODES = 100;
/**
* 使用MD5哈希函数,提供较好的哈希分布特性
*/
private final HashFunction hashFunction = Hashing.md5();
/**
* 环形哈希空间,存储虚拟节点的哈希值与实际服务器节点的映射关系
* 采用TreeMap实现,便于通过tailMap寻找顺时针最近的节点
*/
private final SortedMap<Long, ServerNode> circle = new TreeMap<>();
/**
* 初始化一致性哈希环,添加服务器节点
*
* @param serverNodes 服务器节点集合,在本场景下应为3个节点
*/
public void init(Collection<ServerNode> serverNodes) {
// 清空哈希环
circle.clear();
// 添加所有服务器节点及其虚拟节点到哈希环
for (ServerNode serverNode : serverNodes) {
addServerNode(serverNode);
}
}
/**
* 添加服务器节点到哈希环,包括其对应的虚拟节点
*
* @param serverNode 服务器节点对象
*/
public void addServerNode(ServerNode serverNode) {
// 为每个实际节点创建多个虚拟节点
for (int i = 0; i < VIRTUAL_NODES; i++) {
// 生成虚拟节点的唯一标识
String virtualNodeName = serverNode.getNodeId() + "#" + i;
// 计算虚拟节点的哈希值
long hash = hash(virtualNodeName);
// 将虚拟节点添加到哈希环
circle.put(hash, serverNode);
}
}
/**
* 从哈希环中移除服务器节点,包括其对应的虚拟节点
*
* @param serverNode 服务器节点对象
*/
public void removeServerNode(ServerNode serverNode) {
// 移除该节点对应的所有虚拟节点
for (int i = 0; i < VIRTUAL_NODES; i++) {
String virtualNodeName = serverNode.getNodeId() + "#" + i;
long hash = hash(virtualNodeName);
circle.remove(hash);
}
}
/**
* 根据用户ID获取对应的服务器节点
*
* @param userId 用户唯一标识
* @return 分配给该用户的服务器节点,如果哈希环为空则返回null
*/
public ServerNode getServerNode(String userId) {
if (circle.isEmpty()) {
return null;
}
// 计算用户ID的哈希值
long hash = hash(userId);
// 如果哈希值不在环上,则获取大于该哈希值的子Map
if (!circle.containsKey(hash)) {
SortedMap<Long, ServerNode> tailMap = circle.tailMap(hash);
// 如果子Map为空,则取哈希环的第一个节点(循环查找)
hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
}
// 返回对应的服务器节点
return circle.get(hash);
}
/**
* 计算字符串的哈希值
*
* @param key 要计算哈希值的字符串
* @return 哈希值(长整型)
*/
private long hash(String key) {
// 使用MD5哈希函数计算哈希值,并转换为长整型
return hashFunction.hashString(key, StandardCharsets.UTF_8).asLong();
}
/**
* 服务器节点模型类,存储节点信息
*/
@Getter
@Setter
public static class ServerNode {
/**
* 节点唯一标识
*/
private String nodeId;
/**
* 节点IP地址
*/
private String ip;
/**
* 节点端口号
*/
private int port;
/**
* 构造函数
*
* @param nodeId 节点唯一标识
* @param ip 节点IP地址
* @param port 节点端口号
*/
public ServerNode(String nodeId, String ip, int port) {
this.nodeId = nodeId;
this.ip = ip;
this.port = port;
}
@Override
public String toString() {
return "ServerNode{" +
"nodeId='" + nodeId + '\'' +
", ip='" + ip + '\'' +
", port=" + port +
'}';
}
}
}
配置类 ConsistentHashConfig
package com.zgb.consistenthash;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.List;
/**
* 一致性哈希配置类,用于初始化服务器节点
*/
@Configuration
public class ConsistentHashConfig {
/**
* 初始化一致性哈希管理器,并配置3台服务器节点
*
* @return 配置好的一致性哈希管理器
*/
@Bean
public ConsistentHashManager consistentHashManager() {
ConsistentHashManager manager = new ConsistentHashManager();
// 创建3个服务器节点
ConsistentHashManager.ServerNode node1 = new ConsistentHashManager.ServerNode(
"node-1", "192.168.1.101", 8080);
ConsistentHashManager.ServerNode node2 = new ConsistentHashManager.ServerNode(
"node-2", "192.168.1.102", 8080);
ConsistentHashManager.ServerNode node3 = new ConsistentHashManager.ServerNode(
"node-3", "192.168.1.103", 8080);
// 初始化哈希环
List<ConsistentHashManager.ServerNode> nodes = Arrays.asList(node1, node2, node3);
manager.init(nodes);
return manager;
}
}
使用示例 UserShardingService
package com.zgb.service;
import com.zgb.consistenthash.ConsistentHashManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 用户分片服务,用于将用户分配到不同的服务器节点
*/
@Service
public class UserShardingService {
@Autowired
private ConsistentHashManager consistentHashManager;
/**
* 根据用户ID获取用户所在的服务器节点
*
* @param userId 用户唯一标识
* @return 服务器节点信息,包含IP和端口
*/
public ConsistentHashManager.ServerNode getUserServerNode(String userId) {
return consistentHashManager.getServerNode(userId);
}
/**
* 计算50万用户的分片分布情况,用于验证哈希分布均匀性
*
* @return 各节点的用户数量分布
*/
public int[] calculateUserDistribution() {
int[] counts = new int[3]; // 索引0: node-1, 1: node-2, 2: node-3
// 模拟50万用户
for (int i = 1; i <= 500000; i++) {
String userId = "user_" + i;
ConsistentHashManager.ServerNode node = consistentHashManager.getServerNode(userId);
// 统计各节点的用户数量
if ("node-1".equals(node.getNodeId())) {
counts[0]++;
} else if ("node-2".equals(node.getNodeId())) {
counts[1]++;
} else if ("node-3".equals(node.getNodeId())) {
counts[2]++;
}
}
return counts;
}
}
实现说明
-
核心原理:
- 使用Guava的HashFunction实现哈希计算,采用MD5算法
- 通过虚拟节点(每个实际节点对应100个虚拟节点)提高哈希分布均匀性
- 采用TreeMap实现环形哈希空间,支持高效查找
-
主要组件:
ConsistentHashManager
:核心类,实现一致性哈希的添加、移除节点和查找功能ServerNode
:内部类,封装服务器节点信息ConsistentHashConfig
:配置类,初始化3个服务器节点UserShardingService
:业务服务类,提供用户分片功能
-
使用方式:
- 通过
getUserServerNode(String userId)
方法获取用户对应的服务器节点 calculateUserDistribution()
方法可验证50万用户在3个节点上的分布情况
- 通过
该实现具有良好的扩展性,当需要增加或减少服务器节点时,只会影响少量用户的分片结果,符合分布式系统的高可用需求。
如果文章对你有一点点帮助,欢迎【点赞、留言、+ 关注】
您的关注是我创作的动力!若有疑问/交流/需求,欢迎留言/私聊!
多一个朋友多一条路!