Java 和 Python 程序的相同一致哈希算法实现

2022-09-04 19:41:14

我们有一个应用程序,Python模块将数据写入redis分片,Java模块将从redis分片读取数据,所以我需要为Java和Python实现完全相同的一致哈希算法,以确保可以找到数据。

我用谷歌搜索了几个实现,但发现Java和Python的实现总是不同的,不能用来聚集。需要您的帮助。

编辑,我尝试过的在线实现:Java:
http://weblogs.java.net/blog/tomwhite/archive/2007/11/consistent_hash.html
Python:http://techspot.zzzeek.org/2012/07/07/the-absolutely-simplest-consistent-hashing-example/
http://amix.dk/blog/post/19367

编辑,附加Java(使用Google Guava lib)和我写的Python代码。代码基于上述文章。

import java.util.Collection;
import java.util.SortedMap;
import java.util.TreeMap;
import com.google.common.hash.HashFunction;

public class ConsistentHash<T> {
    private final HashFunction hashFunction;
    private final int numberOfReplicas;
    private final SortedMap<Long, T> circle = new TreeMap<Long, T>();

    public ConsistentHash(HashFunction hashFunction, int numberOfReplicas,
            Collection<T> nodes) {
        this.hashFunction = hashFunction;
        this.numberOfReplicas = numberOfReplicas;

        for (T node : nodes) {
            add(node);
        }
    }

    public void add(T node) {
        for (int i = 0; i < numberOfReplicas; i++) {
            circle.put(hashFunction.hashString(node.toString() + i).asLong(),
                    node);
        }
    }

    public void remove(T node) {
        for (int i = 0; i < numberOfReplicas; i++) {
            circle.remove(hashFunction.hashString(node.toString() + i).asLong());
        }
    }

    public T get(Object key) {
        if (circle.isEmpty()) {
            return null;
        }
        long hash = hashFunction.hashString(key.toString()).asLong();
        if (!circle.containsKey(hash)) {
            SortedMap<Long, T> tailMap = circle.tailMap(hash);
            hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
        }
        return circle.get(hash);
    }
}

测试代码:

        ArrayList<String> al = new ArrayList<String>(); 
        al.add("redis1");
        al.add("redis2");
        al.add("redis3");
        al.add("redis4");

        String[] userIds = 
        {"-84942321036308",
        "-76029520310209",
        "-68343931116147",
        "-54921760962352"
        };
        HashFunction hf = Hashing.md5();

        ConsistentHash<String> consistentHash = new ConsistentHash<String>(hf, 100, al); 
        for (String userId : userIds) {
            System.out.println(consistentHash.get(userId));
        }

Python 代码:

import bisect
import md5

class ConsistentHashRing(object):
    """Implement a consistent hashing ring."""

    def __init__(self, replicas=100):
        """Create a new ConsistentHashRing.

        :param replicas: number of replicas.

        """
        self.replicas = replicas
        self._keys = []
        self._nodes = {}

    def _hash(self, key):
        """Given a string key, return a hash value."""

        return long(md5.md5(key).hexdigest(), 16)

    def _repl_iterator(self, nodename):
        """Given a node name, return an iterable of replica hashes."""

        return (self._hash("%s%s" % (nodename, i))
                for i in xrange(self.replicas))

    def __setitem__(self, nodename, node):
        """Add a node, given its name.

        The given nodename is hashed
        among the number of replicas.

        """
        for hash_ in self._repl_iterator(nodename):
            if hash_ in self._nodes:
                raise ValueError("Node name %r is "
                            "already present" % nodename)
            self._nodes[hash_] = node
            bisect.insort(self._keys, hash_)

    def __delitem__(self, nodename):
        """Remove a node, given its name."""

        for hash_ in self._repl_iterator(nodename):
            # will raise KeyError for nonexistent node name
            del self._nodes[hash_]
            index = bisect.bisect_left(self._keys, hash_)
            del self._keys[index]

    def __getitem__(self, key):
        """Return a node, given a key.

        The node replica with a hash value nearest
        but not less than that of the given
        name is returned.   If the hash of the
        given name is greater than the greatest
        hash, returns the lowest hashed node.

        """
        hash_ = self._hash(key)
        start = bisect.bisect(self._keys, hash_)
        if start == len(self._keys):
            start = 0
        return self._nodes[self._keys[start]]

测试代码:

import ConsistentHashRing

if __name__ == '__main__':
    server_infos = ["redis1", "redis2", "redis3", "redis4"];
    hash_ring = ConsistentHashRing()
    test_keys = ["-84942321036308",
        "-76029520310209",
        "-68343931116147",
        "-54921760962352",
        "-53401599829545"
        ];

    for server in server_infos:
        hash_ring[server] = server

    for key in test_keys:
        print str(hash_ring[key])

答案 1

您似乎同时遇到两个问题:编码问题和表示问题。

编码问题尤其会出现,因为您似乎正在使用Python 2 - Python 2的类型根本不像Java的类型,实际上更像是Java数组。但是Java不能保证给你一个与Python具有相同内容的字节数组(它们可能使用兼容的编码,但不能保证 - 即使这个修复不会改变事情,一般来说,避免将来出现问题是一个好主意)。strStringbyteString.getBytes()str

因此,解决此问题的方法是使用行为类似于Java的Python类型,并将两种语言的相应对象转换为指定相同编码的字节。从Python的角度来看,这意味着您要使用该类型,如果您使用的是Python 3,则它是默认的字符串文本类型,或者将其放在.py文件的顶部附近:Stringunicode

from __future__ import unicode_literals

如果这两个选项都不是选项,请按以下方式指定字符串文本:

u'text'

前面的强制它使用 unicode。然后可以使用其方法将其转换为字节,该方法采用(不出所料)编码:uencode

u'text'.encode('utf-8')

从Java方面来看,有一个重载版本需要编码 - 但它将其作为java.nio.Charset而不是字符串 - 所以,您需要这样做:String.getBytes

"text".getBytes(java.nio.charset.Charset.forName("UTF-8"))

这些将为您提供两种语言的等效字节序列,以便哈希具有相同的输入并为您提供相同的答案。

您可能遇到的另一个问题是表示,具体取决于您使用的哈希函数。Python的hashlib(这是自Python 2.5以来md5和其他加密哈希的首选实现)与Java的MessageDigest完全兼容 - 它们都提供字节,因此它们的输出应该是等效的。

另一方面,Python的zlib.crc32和Java的java.util.zip.CRC32都给出了数字结果-但是Java的始终是无符号的64位数字,而Python的(在Python 2中)是有符号的32位数字(在Python 3中,它现在是一个无符号的32位数字,所以这个问题消失了)。要将有符号结果转换为无符号结果,请执行:,结果应与 Java 结果相当。result & 0xffffffff


答案 2

根据对哈希函数的分析

Murmur2、Meiyan、SBox 和 CRC32 为各种按键提供了良好的性能。可以建议将它们作为 x86 上的通用哈希函数。

硬件加速CRC(表中标记为iSCSI CRC)是最近Core i5 / i7处理器上最快的哈希函数。但是,AMD 和早期的英特尔处理器不支持 CRC32 指令。

Python有zlib.crc32,Java有CRC32类。由于它是一种标准算法,因此您应该在两种语言中获得相同的结果。

MurmurHash 3在Google Guava(一个非常有用的Java库)和Python的pyfasthash中可用。

请注意,这些不是加密哈希函数,因此它们速度很快,但不能提供相同的保证。如果这些哈希对安全性很重要,请使用加密哈希。