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

13
我们有一个应用程序,Python模块将数据写入redis分片,Java模块将从redis分片读取数据,因此我需要为Java和Python实现完全相同的一致性哈希算法,以确保可以找到数据。
我在Google上搜索并尝试了几种实现,但发现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库)和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])

如果您已经编写了代码并且遇到了问题,请在您的问题中包含相关部分。如果您不展示代码,我们无法帮助您解决问题。 - Greg Hewgill
你考虑了哪些哈希函数?你是如何调用它们的?标准的加密哈希函数(如SHA-1)是否无法满足你的需求? - Brendan Long
1
@superche,你是在说用Python中的MD5哈希你的密钥与用Java中的MD5哈希你的密钥的结果是不同的吗?如果是这样,你应该检查一下你是否真的对相同的字符串/密钥/其他进行了哈希。并且你应该发布一下不起作用的代码。 - Jeff Tratner
4
我想知道你的数据在Python和Java中是否以不同的编码方式进行编码,因为它们对默认字符串编码的理解是不同的。 - Brendan Long
@superche 这里可能重要的是数据编码,而不是代码编码。 - Kos
显示剩余4条评论
7个回答

10

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

编码问题特别是由于您似乎正在使用Python 2 - Python 2的 str 类型与Java的 String 类型完全不同,实际上更像是一个由 byte 组成的Java数组。但是Java的 String.getBytes()不能保证给您一个与Python str 具有相同内容的字节数组(它们可能使用兼容的编码,但不能保证-即使这个修复程序没有改变任何东西,在未来避免问题是个好主意)。

因此,解决这个问题的方法是使用一个行为类似于Java的 String 的Python类型,并将两个语言中对应的对象转换为指定相同编码的字节。从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 完全兼容-它们都提供字节,因此它们的输出应该是相等的。

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


3

根据这篇哈希函数分析

Murmur2、Meiyan、SBox和CRC32对于所有类型的键提供良好的性能。它们可以作为x86通用哈希函数的推荐。

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

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

MurmurHash 3可以在Google Guava(一个非常有用的Java库)和pyfasthash(Python库)中使用。
请注意,这些不是加密哈希函数,因此速度很快,但不提供相同的保证。如果这些哈希对于安全性很重要,请使用加密哈希。

附上代码。我将尝试使用CRC32。 - superche
我发现Python的zlib.crc32和Java的CRC32类对于相同的键没有返回相同的值。Python的crc32是有符号整数,而Java返回长整型。 - superche
1
@superche 当Python将有符号值解释为正数时,它们的值是否相同? - lvc

2

使用不同语言实现的哈希算法并不会使哈希值不同。无论是在Java还是Python中生成的SHA-1哈希值都将是相同的。


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专注于密码安全,但在这种情况下并不是真正需要的。

MD5哈希在Java和Python中是相同的,但是当尝试为键实现一致性哈希时,由于Java和Python中的不同数据类型,它们是不同的。 - superche
你遇到了哪些数据类型的问题?最简单的解决方案可能是在哈希之前将它们转换为字符串。 - Brendan Long
@superche 我假设这里使用的是文本键,但实际情况可能并非如此。如果键不是文本类型,那么在任何解决方案中,他都需要将它们转换为某种通用格式。在他的示例代码中,键似乎是字符串,因此MD5应该可以正常工作。 - Chris Bode
我发现Java和Python中的MD5哈希值不同。我使用文本密钥“-84942321036308”进行测试,Python返回(195940655746834055544853328800610818493),而Java返回(8357636395451350515)。你能给我看一下你的代码吗? - superche
1
"-84942321036308"的MD5值为9368cc30fe241dcba2beb130bfe499bd,或者在十进制中为195940655746834055544853328800610818493。你所声称Java给你的甚至不是MD5哈希,因为它们都是128位的。当然,Java没有128位的数字数据类型,除了BigInteger之外,你真的不应该用它来进行哈希。正如我所说,你可能想找到一个更快的哈希算法。MD5被设计成不可逆的。有更简单的方法来做这件事。让我更新我的答案。 - Chris Bode

2

这是一个简单的哈希函数,针对你的键值,在Python和Java中都会产生相同的结果:

Python

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

Java

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

这并不需要使用加密安全哈希函数,那只是过度杀伤力。


我花了很长时间认为你使用的是标准JDK String.hashCode()函数,但事实证明你使用的是37作为基数,而JDK使用31。 - traviscj

1

让我们明确一下:相同的二进制输入在不同的环境/实现(Python、Java等)中输入到相同的哈希函数(SHA-1、MD5等)将产生相同的二进制输出。这是因为这些哈希函数是根据标准实现的。

因此,当回答这些问题时,您会发现问题的来源:

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

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

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


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();
    }
}

请注意:
java.math.BigInteger.intValue() 方法将BigInteger转换为int类型。此转换类似于从long到int的缩小原始转换。如果此BigInteger太大而无法适应int类型,只会返回低32位。这种转换可能丢失关于BigInteger值的整体大小的信息,并返回相反符号的结果。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接