微小的随机ID生成

时间:2009-05-02 21:07:10

标签: google-app-engine md5

我正在尝试生成用于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哈希会更好吗?有什么我可以忽略的吗?

3 个答案:

答案 0 :(得分:2)

这样的事情怎么样:

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

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

    每个分区由一个实体表示: 开始id 结束身份 当前身份

  3. 现在,要生成ID:

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

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

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

      -Dave

答案 1 :(得分: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中的键(如果我已正确完成转换)。

看起来每个附加角色都会生成越来越多的键,这正是您想象的。我希望这种方法可以扩展用户生成的内容。

对于任何有兴趣的人,以下是我如何获得最后一列的基本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)

答案 2 :(得分:1)

这种算法很聪明,确实可以最大限度地减少写操作。因此,长度值将收敛到最短长度和缺失碰撞之间的平衡。

您可以通过使用哈希表中使用的策略来放松对冲突的约束。尝试一些其他唯一的ID,然后再回到增加长度。

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

您最终会更有效地使用您的ID地址空间,并且不会更频繁地增加长度。

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

使用此算法可以最小化生成的随机位数,并且长度也没有上限。

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