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

时间:2012-09-11 03:38:49

标签: java python consistent-hashing

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

7 个答案:

答案 0 :(得分:9)

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

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

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

unicode

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

from __future__ import unicode_literals

前面的u'text' 迫使它进行unicode。然后可以使用u方法将其转换为字节,这种方法(不出所料)采用了编码:

encode

从Java方面来看,有u'text'.encode('utf-8') 的重载版本需要编码 - 但它需要java.nio.Charset而不是字符串 - 所以,您需要这样做:< / p>

String.getBytes

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

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

另一方面,Python的zlib.crc32和Java的java.util.zip.CRC32都给出了数值结果 - 但Java总是无符号的64位数,而Python(在Python 2中)是带符号的32位数(在Python 3中,它现在是一个无符号的32位数字,所以这个问题就消失了)。要将签名结果转换为无符号结果,请执行:"text".getBytes(java.nio.charset.Charset.forName("UTF-8")) ,结果应与Java结果相当。

答案 1 :(得分:3)

根据this analysis of hash functions

  

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

     

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

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

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

请注意,这些不是cryptographic hash functions,所以它们很快但不提供相同的保证。如果这些哈希对安全性很重要,请使用加密哈希。

答案 2 :(得分:2)

散列算法的不同语言实现不会使散列值不同。无论是在java还是python中生成的SHA-1哈希都是相同的。

答案 3 :(得分: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专注于加密安全性,在这种情况下并不是真正需要的。

答案 4 :(得分: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;
}

您不需要加密安全哈希。这太过分了。

答案 5 :(得分:1)

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

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

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

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

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

答案 6 :(得分:0)

对于java版本,我建议使用生成128位字符串结果的MD5,然后将其转换为BigInteger(整数和长度不足以容纳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值的整体大小的信息,并返回符号相反的结果。

相关问题