如何在Java中实现Nginx Ketama一致性哈希算法?
Nginx Ketama一致性哈希的Java实现方案
刚好之前在做类似的负载均衡缓存复用场景,给你梳理下完全适配Nginx Ketama算法的Java实现方案,完美匹配你现在的需求——让节点能复用预加载缓存,确保/abc/这类请求不管是Nginx路由还是节点内部计算,都指向同一个节点。
1. 成熟开源实现选型
有几个靠谱的开源实现可以直接用,它们都严格遵循Ketama算法规范,和Nginx的实现完全对齐,适配不同场景:
- 一个是支持动态节点增减的版本,能应对集群节点在线扩容/缩容的场景,适合规模经常变化的集群
- 另一个是轻量级实现,代码极简,不需要额外依赖,很适合嵌入到现有服务中快速集成
- 还有一个附带完整单元测试的版本,能快速验证和Nginx哈希结果的一致性,避免踩坑
这些实现的核心逻辑都是Ketama的标准流程:
- 给每个真实节点生成多个虚拟节点(默认160个,和Nginx保持一致)
- 用MD5哈希计算虚拟节点的哈希值,排序后存在哈希环上
- 对请求的URL前缀(比如/abc/)计算哈希值,在环上找到第一个大于等于该哈希值的虚拟节点,对应的真实节点就是目标节点
2. 极简版Java实现示例
如果不想引入外部依赖,这里给你一个简化但严格对齐Nginx Ketama算法的实现,你可以直接集成到代码里:
import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.SortedMap; import java.util.TreeMap; public class KetamaConsistentHash { // 哈希环,存储虚拟节点哈希值到真实节点的映射 private final SortedMap<Long, String> hashCircle = new TreeMap<>(); // 每个真实节点对应的虚拟节点数量,和Nginx保持一致用160 private final int virtualNodesPerServer; public KetamaConsistentHash(int virtualNodesPerServer, String... servers) { this.virtualNodesPerServer = virtualNodesPerServer; for (String server : servers) { addServerNode(server); } } // 添加真实节点,同时生成对应的虚拟节点 public void addServerNode(String server) { for (int i = 0; i < virtualNodesPerServer; i++) { long virtualNodeHash = calculateKetamaHash(server + ":" + i); hashCircle.put(virtualNodeHash, server); } } // 移除真实节点及对应的虚拟节点 public void removeServerNode(String server) { for (int i = 0; i < virtualNodesPerServer; i++) { long virtualNodeHash = calculateKetamaHash(server + ":" + i); hashCircle.remove(virtualNodeHash); } } // 根据key(比如URL前缀)获取对应的真实节点 public String getTargetServer(String key) { if (hashCircle.isEmpty()) { return null; } long keyHash = calculateKetamaHash(key); // 找到哈希环上第一个大于等于keyHash的虚拟节点 SortedMap<Long, String> tailMap = hashCircle.tailMap(keyHash); long targetHash = tailMap.isEmpty() ? hashCircle.firstKey() : tailMap.firstKey(); return hashCircle.get(targetHash); } // 严格按照Ketama算法计算哈希值:MD5取前4字节大端序 private long calculateKetamaHash(String input) { try { MessageDigest md5Digest = MessageDigest.getInstance("MD5"); byte[] md5Bytes = md5Digest.digest(input.getBytes()); // 取MD5结果的前4字节,转成大端序的long值 return ((long) (md5Bytes[3] & 0xFF) << 24) | ((long) (md5Bytes[2] & 0xFF) << 16) | ((long) (md5Bytes[1] & 0xFF) << 8) | (md5Bytes[0] & 0xFF); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("MD5算法不可用", e); } } // 测试用例:验证同一URL前缀始终路由到同一节点 public static void main(String[] args) { KetamaConsistentHash hashRouter = new KetamaConsistentHash(160, "node-1", "node-2", "node-3"); String urlPrefix = "/abc/"; // 连续两次请求,结果应该一致 System.out.println(hashRouter.getTargetServer(urlPrefix)); System.out.println(hashRouter.getTargetServer(urlPrefix)); } }
关键注意事项:
- 虚拟节点数量:一定要和Nginx配置的保持一致(默认是160),否则哈希分布会不一样,导致路由结果不匹配
- 哈希计算逻辑:必须用MD5取前4字节的大端序,这是Ketama算法的标准,也是Nginx实现的逻辑,确保同一key的计算结果和Nginx完全相同
- URL前缀处理:计算哈希时的key要和Nginx侧使用的完全一致——比如Nginx是取
$uri的第一部分(/abc/),那你这里也要传入相同的字符串到getTargetServer方法
用这个实现,你的节点侧就能和Nginx的路由结果100%匹配,预加载的缓存就能被正确复用啦。
内容的提问来源于stack exchange,提问作者Janning Vygen
相关产品推荐
相关产品推荐

