крошечная случайная генерация удостоверений личности - PullRequest
6 голосов
/ 03 мая 2009

Я пытаюсь сгенерировать уникальные идентификаторы для использования в приложении Google App Engine и хотел бы получить отзыв о целесообразности подхода, который я собираюсь использовать (вопросы в конце). Я прочитал довольно много вопросов на эту тему, но я не помню, чтобы сталкивался с этим конкретным подходом.

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

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

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

Я думал об использовании осколочного счетчика в следующих строках:

  • Счетчик размечен для пользователей, так что для каждого пользователя есть осколок. Каждый счетчик имеет свой собственный счетчик, специфичный для данного пользователя, который увеличивается при создании нового элемента этим пользователем. Количество увеличивается независимо от того, успешно ли создан элемент.
  • Основой идентификатора является хеш MD5 следующей строки: "<адрес электронной почты пользователя> | <значение последнего счетчика>".
  • Полученный хэш MD5 затем усекается, первоначально до четырех символов.
  • Поддерживается единое глобальное значение "длина". Всякий раз, когда предыдущие шаги приводят к дублированию ключа (можно предположить, что это произойдет довольно быстро вначале), значение длины будет увеличено на единицу. Хеши MD5 для новых идентификаторов теперь будут обрезаться до «длинных» символов, а не до четырех символов.
  • Я не хочу раскрывать адрес электронной почты пользователя, что говорит о том, что какой-то хэш был бы хорошим способом.

Мои вопросы: Правильно ли я считаю, что это в значительной степени позволит избежать конфликта записи из-за дублирования ключей и что конфликт записи из-за поля длины, вероятно, не будет проблемой, особенно при более длинной длине? Кто-нибудь может описать здесь математику? Будет ли длина быстро увеличиваться почти до длины хеша MD5, ставя под сомнение ценность всего подхода? Было бы лучше просто использовать полный (более длинный) хэш MD5, чтобы было легче поддерживать его? Есть ли что-то, что я пропускаю?

Ответы [ 3 ]

2 голосов
/ 03 мая 2009

Как насчет этого:

  1. Если вы хотите использовать 4-символьные клавиши, использующие a-zA-Z0-9, то у вас будет: 62 ^ 4 => 14 миллионов возможных значений.

  2. Разбейте это на N разделов: 0000 ... 1000, 1001 ... 2000, ..., ZZAA ... ZZZZ

    Каждый раздел представлен объектом с: идентификатор начала конечный идентификатор текущий идентификатор

Теперь, чтобы сгенерировать ID:

  1. случайным образом выбирает раздел. Используйте ключ запуска для каждого раздела в качестве ключа. Начать транзакцию:
  2. get_or_create () этой сущности раздела.
  3. если текущий идентификатор == конечный идентификатор, перейдите к шагу 1
  4. сохранить текущий идентификатор
  5. увеличить текущий идентификатор на единицу Завершение транзакции

использовать текущий идентификатор, который был сохранен как ваш идентификатор.

Если вы выберете N как 140, у каждого раздела будет 100 000 значений. Это позволило бы довольно много одновременных вставок при ограничении количества повторных попыток из-за выбора «пустого» раздела.

Возможно, вам нужно подумать об этом больше. Особенно, как вы будете обращаться со случаем, когда вам нужно перейти на 5 или 6-значные клавиши?

-Dave

1 голос
/ 08 мая 2009

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

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

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

В результате вы будете более эффективно использовать адресное пространство своего идентификатора и будете значительно реже увеличивать длину.

Что касается вашего вопроса об ограничении длины MD5, я думаю, что выбор MD5 - это перебор. Вам действительно не нужен криптографический (псевдо) безопасный хеш. Вам нужен генератор случайных битов, для которого вы можете использовать crc32 (или adler, который работает быстрее). Особенно, если код должен быть запущен на мобильном телефоне. Чтобы реализовать генератор случайных битов с помощью crc32, добавьте 32-битное значение к строке для хеширования и инициализируйте ее постоянным значением по вашему выбору (начальное число). Затем вычислите crc32 для этой строки. Если вам нужно больше байтов, запишите полученное значение crc32 в 32 бита перед строкой и пересчитайте crc32. Итерируйте, пока у вас не будет достаточно битов. Вы можете заменить crc32 алгоритмом по вашему выбору. Это дает дешевый и быстрый генератор случайных битов, где начальная константа является начальным числом.

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

Что касается кодирования, вы не указали ограничения. Можете ли вы использовать прописные и строчные буквы с цифрами? В вашем примере используется алфавит из 36 различных значений ASCII. Если у вас есть генератор псевдослучайных чисел, описанный выше, который может генерировать столько байтов, сколько необходимо, просто определите длину, чтобы быть числом букв вашего идентификатора, и выберите следующую букву по модулю следующего случайного байта. Тогда вы точно будете знать, сколько байтов нужно сгенерировать за один раз, а генерация идентификатора - тривиально.

1 голос
/ 03 мая 2009

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

Length     Count      MD5             Base 62
4          400        3d0e            446
5          925        4fc04           1Myi
6          2434       0e9368          40V6
7          29155      e406d96         GBFiA
8          130615     7ba787c8        2GOiCm
9          403040     75525d163       YNKjL9
10         1302992    e1b3581f52      H47JAIs
Total:     1869561

Каждый раз, когда длина хеша увеличивалась, это происходило из-за столкновения, поэтому в данном случае произошло шесть столкновений. Количество - это количество ключей заданной длины, которые были сгенерированы до столкновения. Столбец MD5 показывает последний усеченный ключ, который был успешно добавлен до того, как произошла ошибка дублированного ключа. В крайнем правом столбце показан ключ в базе 62 (если я правильно сделал преобразование).

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

Для всех, кто заинтересован, вот как я получил представление base 62 для последнего столбца:

def base_62_encode(input):
    "Inspired by code at http://en.wikipedia.org/wiki/Base_36."
    CLIST="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
    rv = ""
    while input != 0:
        rv = CLIST[input % 62] + str(rv)
        input /= 62
    return rv

base62_id = base_62_encode(long(truncated_hash, 16))

(добавлено позже:)

Вот класс, который заботится о нескольких вещах, связанных с генерацией этих идентификаторов в контексте Google App Engine. По умолчанию ключи, сгенерированные этим классом, не являются чисто базовыми 62, поскольку первый символ имени ключа GAE должен быть буквенным. Это требование было решено с использованием базы 52 для первого символа.

Первичная база может быть изменена на что-то, отличное от 62, путем изменения значения "clist", которое было передано (или опущено) в конструкторе. Возможно, вы захотите удалить символы, которые легко перепутать, например, «1», «l», «i» и т. Д.

Использование:

keygen = LengthBackoffIdGenerator(SomeClass, initial_length=5)
small_id, modified = keygen.small_id(seed_value_from_sharded_counter)

Вот класс:

class LengthBackoffIdGenerator(object):
    """Class to generate ids along the lines of tinyurl.

    By default, ids begin with a alphabetic character.  Each id that is created is checked against previous ones
    to ensure uniqueness.  When a duplicate is generated, the length of the seed value used is increased by one
    character.

    Ids become gradually longer over time while remaining relatively short, and there are very few retries in 
    relation to the number of keys generated.
    """
    ids = set()

    def __init__(self, cls, clist='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', alpha_first=False,
            initial_length=5, check_db=False):
        self.clist = clist
        self.alpha_first = alpha_first
        if self.alpha_first:
            import re
            alpha_list = re.sub(r'\d', '', clist)
            if len(alpha_list) < 1:
                raise Exception, 'At least one non-numeric character is required.'
            self.alpha_list = alpha_list
        self.cls = cls
        self.length = initial_length
        self.check_db = check_db

    def divide_long_and_convert(self, radix, clist, input, rv):
        "Inspired by code at http://en.wikipedia.org/wiki/Base_36."
        rv += clist[input % radix]
        input /= radix
        return (input, rv)

    def convert_long(self, input):
        rv = ""
        if self.alpha_first:
            input, rv = self.divide_long_and_convert(len(self.alpha_list), self.alpha_list, input, rv)
        while input != 0:
            input, rv = self.divide_long_and_convert(len(self.clist),      self.clist,      input, rv)
        return rv

    def hash_truncate_and_binify(self, input, length):
        """Compute the MD5 hash of an input string, truncate it and convert it to a long.

        input  - A seed value.
        length - The length to truncate the MD5 hash to.
        """
        from assessment.core.functions import md5_hash
        input = md5_hash(input)[0:length]
        return long(input, 16)

    def _small_id(self, input):
        """Create an ID that looks similar to a tinyurl ID.

        The first letter of the ID will be non-numeric by default.
        """
        return self.convert_long(self.hash_truncate_and_binify(input, self.length))

    def small_id(self, seed):
        key_name = self._small_id(seed)
        increased_length = False
        if self.check_db and not self.cls.get_by_key_name(key_name) is None:
            self.ids.add(key_name)
        if key_name in self.ids:
            self.length += 1
            increased_length = True
            key_name = self._small_id(seed)
        self.ids.add(key_name)
        return (key_name, increased_length)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...