6

我正在尝试生成用于 Google App Engine 应用程序的唯一 ID,并希望就我正在考虑使用的方法的可行性提供反馈(最后有问题)。我已经阅读了很多关于这个主题的问题,但我不记得遇到过这种特殊的方法。

我想要看起来随机的 ID,例如 MD5 哈希,但我也希望它们很小。沿着 tinyurl 行的四到六个字符将是理想的。ID 将用于用户生成的内容,在我的应用程序的上下文中,例如人们将要编写的测试问题。ID 没有必要看起来是随机的(如果它们看起来像序列 ID 就可以了),但我正在考虑使用的方法适合这一点,所以这不是一个真正的问题。

熟悉 Google App Engine 的人都知道,写入数据存储的成本特别高,如果同一实体组的写入次数过多,可能会导致超时。分片计数器是一种通常用于避免单个全局计数器上的写争用以及随之而来的失败事务的方法。

除了获得短 ID 和避免写入争用之外,我还试图避免生日悖论。我想为有数百万个 ID 的可能性做准备,即使这有点过火了。

我正在考虑按照以下方式使用分片计数器:

  • 计数器在用户上分片,因此每个用户都有一个分片。每个计数器对象都有自己的特定于给定用户的计数,当该用户创建新项目时,该计数会增加。无论项目是否成功创建,计数都会增加。
  • ID 的基础是以下字符串的 MD5 哈希:“<user-email-address>|<latest-counter-value>”。
  • 然后将生成的 MD5 散列截断,最初截断为四个字符。
  • 维护单个全局“长度”值。每当前面的步骤导致重复键时(人们想象这会很快发生),长度的值将增加一。新 ID 的 MD5 哈希现在将被截断为“长度”字符,而不是四个字符。
  • 我不想公开用户电子邮件地址,这表明某种哈希值将是一个好方法。

我的问题是:我是否正确地认为这将在很大程度上避免由于重复键而导致的写入争用,并且长度字段上的写入争用可能不是问题,尤其是在更长的长度时?谁能描述这里涉及的数学?长度会很快增加到接近 MD5 哈希的长度,从而质疑整个方法的价值吗?使用完整(更长)的 MD5 哈希以使事情更易于维护会更好吗?有什么我忽略的吗?

4

3 回答 3

2

像这样的东西怎么样:

  1. 如果您想要使用 a-zA-Z0-9 的 4 个字符键,那么您将拥有:62^4 = > 1400 万个可能的值。

  2. 将其分成 N 个分区: 0000 ... 1000, 1001 ... 2000 , ... , ZZAA ... ZZZZ

    每个分区由一个实体表示:start id end id current id

现在,要生成一个 ID:

  1. 随机选择一个分区。使用每个分区的开始键作为键。开始交易:
  2. get_or_create() 那个分区实体。
  3. 如果当前 id == 结束 id,则转到步骤 1
  4. 保存当前id
  5. 将当前 id 增加一个 End Transaction

使用保存为您的 ID 的当前 ID。

如果您选择 N 作为 140,则每个分区将有 100,000 个值。这将允许相当多的并发插入,同时由于选择“空”分区而限制重试次数。

您可能需要更多地考虑这一点。特别是,当您需要移动到 5 位或 6 位数字键时,您将如何处理?

-戴夫

于 2009-05-03T03:20:16.027 回答
1

只是为了在上面的问题中添加一些硬数字,我已经实现了一个小程序来按照问题中提到的方式生成 id,这些是其中一次试运行的结果:

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 上下文中处理与这些 ID 生成相关的几件事的类。默认情况下,此类生成的密钥不是纯 62 进制的,因为 GAE 密钥名称的第一个字符必须是字母。该要求已在此处通过使用基数 52 作为第一个字符来解决。

通过更改已传递到(或从)构造函数中省略的“clist”值,可以将主基数更改为 62 以外的值。可能想要删除容易混淆的字符,例如“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)
于 2009-05-03T04:25:27.883 回答
1

这个算法很聪明,并且确实可以最小化写操作。所以长度值将收敛到最短长度和没有碰撞 之间的平衡。

您可以通过使用哈希表中使用的策略来放松没有冲突的约束。在回退到增加长度之前尝试一些其他唯一 ID。

因此,我建议您在初始化为 0 的散列字符串的末尾添加一个测试计数器。如果生成的 ID 已被使用,请增加计数器并重试,直到在增加长度后达到最大计数器值。

您最终将更有效地使用您的 ID 地址空间,并且长度增量的频率要低得多。

关于您关于 MD5 长度限制的问题,我认为选择 MD5 是一种矫枉过正。您真的不需要加密(伪)安全哈希。您需要的是一个随机位生成器,您可以使用 crc32 (或更快的 adler)。特别是如果代码要在手机上运行。要使用 crc32 实现随机位生成器,请在字符串前面添加一个 32 位值以进行散列,并使用您选择的常量值(种子)对其进行初始化。然后计算这个字符串的 crc32。如果需要更多字节,将得到的 crc32 值写入字符串前面的 32 位,重新计算 crc32。迭代直到你有足够的位。您可以用您选择的算法替换 crc32。这产生了一个廉价且快速的随机位生成器,其中初始常数是种子。

使用此算法,您可以最大限度地减少生成的随机位数,并且长度也没有上限。

关于编码,您没有指定约束。你可以在数字上使用大写和小写字母吗?您的示例使用 36 个不同 ASCII 值的字母表。一旦你有了上面描述的可以生成任意多字节的伪随机数生成器,只需将长度定义为你的 ID 的字母数,然后用下一个随机字节的模数选择下一个字母。然后您将确切知道一次生成多少字节,并且生成 ID 是微不足道的。

于 2009-05-08T09:40:46.943 回答