12

我们有一个应用程序,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])
4

7 回答 7

10

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

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

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

from __future__ import unicode_literals

如果这些都不是一个选项,请以这种方式指定您的字符串文字:

u'text'

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

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

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

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

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

您可能遇到的另一个问题是表示,这取决于您使用的散列函数。Python hashlib(这是自 Python 2.5 以来 md5 和其他加密哈希的首选实现)在这方面与 Java 完全兼容MessageDigest——它们都给出字节,所以它们的输出应该是等价的。

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

于 2012-09-11T08:02:41.190 回答
3

根据哈希函数的分析

Murmur2、Meiyan、SBox 和 CRC32 为各种键提供了良好的性能。它们可以被推荐为 x86 上的通用散列函数。

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

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

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

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

于 2012-09-11T03:51:38.570 回答
2

散列算法的不同语言实现不会使散列值不同。在 java 或 python 中生成的SHA-1散列将是相同的。

于 2012-09-11T03:51:13.907 回答
2

我不熟悉 Redis,但 Python 示例似乎是散列键,所以我假设我们正在谈论某种 HashMap 实现。

您的 python 示例似乎正在使用 MD5 哈希,这在 Java 和 Python 中都是相同的。

以下是 Java 中的 MD5 散列示例:

http://www.dzone.com/snippets/get-md5-hash-few-lines-java

在 Python 中:

http://docs.python.org/library/md5.html

现在,您可能想找到一种更快的散列算法。MD5 专注于加密安全,在这种情况下并不真正需要。

于 2012-09-11T03:53:14.027 回答
2

这是一个简单的散列函数,它在 python 和 java 上为您的键生成相同的结果:

Python

def hash(key):
        h = 0
        for c in key:
                h = ((h*37) + ord(c)) & 0xFFFFFFFF
        return h;

爪哇

public static int hash(String key) {
    int h = 0;
    for (char c : key.toCharArray())
        h = (h * 37 + c) & 0xFFFFFFFF;
    return h;
}

为此,您不需要加密安全哈希。这只是矫枉过正。

于 2012-09-11T14:30:31.607 回答
1

让我们直截了当地说:在不同的环境/实现(Python,Java,...)中,相同的哈希函数(SHA-1,MD5,...)的相同二进制输入将产生相同的二进制输出。那是因为这些哈希函数是按照标准实现的。

因此,您将在回答这些问题时发现您遇到的问题的根源:

  • 您是否为两个哈希函数提供相同的二进制输入(例如 Python 和 Java 中的 MD5)?

  • 您是否等效地解释了两个哈希函数(例如 Python 和 Java 中的 MD5)的二进制输出?

@lvc 的回答提供了有关这些问题的更多详细信息。

于 2012-09-11T08:58:56.110 回答
0

对于 java 版本,我建议使用 MD5 生成 128 位字符串结果,然后可以将其转换为 BigInteger(Integer 和 Long 不足以容纳 128 位数据)。

示例代码在这里:

private static class HashFunc {

    static MessageDigest md5;

    static {
        try {
            md5 = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            //
        }
    }

    public synchronized int hash(String s) {
        md5.update(StandardCharsets.UTF_8.encode(s));
        return new BigInteger(1, md5.digest()).intValue();
    }
}

注意:

The java.math.BigInteger.intValue() converts this BigInteger to an int. This conversion is analogous to a narrowing primitive conversion from long to int. If this BigInteger is too big to fit in an int, only the low-order 32 bits are returned. This conversion can lose information about the overall magnitude of the BigInteger value as well as return a result with the opposite sign.

于 2016-10-25T17:24:29.577 回答