Такая же реализация алгоритма последовательного хеширования для Java и Python

у нас есть приложение, которое модуль Python будет записывать данные в Redis shards, а модуль Java будет считывать данные из Redis shards, поэтому мне нужно реализовать тот же самый согласованный алгоритм хэширования для Java и Python, чтобы убедиться, что данные могут быть найдены.

Я погуглил и попробовал несколько реализаций, но обнаружил, что реализации Java и Python всегда разные, не могут использоваться togather. Нужна твоя помощь.

Edit, онлайн-реализации, которые я пробовал:
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 ответов


вы, похоже, сталкиваетесь с двумя проблемами одновременно: проблемами кодирования и проблемами представления.

проблемы с кодировкой возникают, в частности, потому, что вы, похоже, используете Python 2 - Python 2 str тип совсем не похож на Java String тип, и на самом деле больше похож на массив Java byte. Но в Java String.getBytes() не гарантируется, что вы получите массив байтов с тем же содержимым, что и Python str (они наверное использовать совместимые кодировки, но не гарантированно - даже если это исправление ничего не меняет, это хорошая идея в целом, чтобы избежать проблем в будущем).

Итак, способ обойти это-использовать тип Python, который ведет себя как Java String, и преобразует соответствующие объекты из обоих языков в байты, указывающие одну и ту же кодировку. Со стороны Python это означает, что вы хотите использовать unicode type, который является строковым литералом по умолчанию, если вы используете 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 (который является предпочтительной реализацией md5 и других криптографических хэшей с Python 2.5) точно совместим с Java MessageDigest в этом - они оба дают байты, поэтому их вывод должен быть эквивалентным.

в Python zlib.crc32 и Java java.util.zip.CRC32, С другой стороны, оба дают числовые результаты - но Java всегда является беззнаковым 64-битным числом, в то время как Python (в Python 2) является подписанным 32-битным числом (в Python 3, его теперь беззнаковое 32-битное число, поэтому эта проблема уходит). Чтобы преобразовать подписанный результат в неподписанный, выполните:result & 0xffffffff, и результат должен быть сопоставим с Java one.


по данным этот анализ хэш-функций:

Мурмур2, Мейян, Сбокс, и КРК32 обеспечивают хорошую работу для всех видов ключей. Их можно рекомендовать в качестве универсальных функций хэширования на x86.

аппаратно-ускоренный CRC (обозначенный iSCSI CRC в таблице) является самой быстрой хэш-функцией на последних процессорах Core i5/i7. Однако инструкция CRC32 не поддерживается AMD и более ранней Intel процессоры.

Python имеет zlib.CRC32 в и Java имеет CRC32 в класс. Поскольку это стандартный алгоритм, вы должны получить одинаковый результат на обоих языках.

MurmurHash 3 доступен в Google Guava (очень полезная библиотека Java) и в pyfasthash для Python.

обратите внимание, что это не криптографические хэш-функции, поэтому они быстры, но не предоставляют одинаковых гарантий. Если эти хэши важны для безопасности, используйте криптографический хэш.


различные языковые реализации алгоритма хэширования не делают хэш-значение другим. The SHA-1 хэш, созданный в java или python, будет одинаковым.


Я не знаком с Redis, но пример Python, похоже, хэширует ключи, поэтому я предполагаю, что мы говорим о какой-то реализации HashMap.

ваш пример python, похоже, использует хэши MD5, которые будут одинаковыми как в Java, так и в Python.

вот пример хеширования MD5 на Java:

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

и в Python:

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

теперь вы можете найти более быстрый алгоритм хэширования. MD5 ориентирован на криптографическую безопасность, которая в данном случае не нужна.


вот простая функция хэширования, которая дает тот же результат как на 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;
}

для этого вам не нужен криптографически безопасный хэш. Это просто перебор.


давайте разберемся: то же самое бинарные вход в ту же хэш-функцию (SHA-1, MD5,...) в различных средах / реализациях (Python, Java,...) даст то же самое бинарные выход. Это потому, что эти хэш-функции реализован в соответствии с стандарты.

следовательно, вы обнаружите источники проблемы(ов), которые вы испытываете при ответе на эти вопросы:

  • предоставляете ли вы тот же двоичный вход для обеих хэш-функций (например, MD5 в Python и Java)?

  • вы интерпретируете двоичный вывод обеих хэш-функций (например, MD5 в Python и Java) эквивалентно?

ответ@lvc предоставляет гораздо более подробную информацию по этим вопросам.


для версии java я бы рекомендовал использовать MD5, который генерирует результат строки 128bit, а затем может быть преобразован в BigInteger (Integer и Long недостаточно для хранения данных 128bit).

пример кода здесь:

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.математика.Типа BigInteger.intValue () преобразует этот BigInteger в int. Это преобразование аналогично сужающееся примитивное преобразование из Long в int. Если это BigInteger слишком большой, чтобы поместиться в int, возвращаются только 32 бита низкого порядка. Это преобразование может потерять информацию об общей величине значения BigInteger, а также вернуть результат с противоположным знаком.