【问题标题】:tiny random-looking ID generation微小的随机 ID 生成
【发布时间】:2017-08-23 14:11:10
【问题描述】:

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

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

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

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

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

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

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

【问题讨论】:

    标签: google-app-engine md5


    【解决方案1】:

    这样的事情怎么样:

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

    2. 把它分成 N 个分区: 0000 ... 1000, 1001 ... 2000 , ... , ZZAA ... ZZZZ

      每个分区都由一个实体表示: 起始编号 结束编号 当前身份

    现在,生成一个 ID:

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

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

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

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

    -戴夫

    【讨论】:

    • 感谢您提供有趣的方法。我会考虑一下并尝试更好地理解它。我的一个问题是,随着密钥数量的增加,它会导致多少冲突(或重试)。我正在尝试使碰撞尽可能接近于零。
    • 只有在分区填满时才会发生冲突。
    • 你可以用这个做其他优化: 1. Memcache 一个“填充分区”列表 2. 如果你要一次得到一堆 id,那么你可以抓取一个块来自分区的 n 个 id,然后将其计数器增加该值。
    • 当两个服务器实例同时从同一个分区请求一个新的 id 会发生什么?他们会得到相同的 id 吗?
    • 否,因为您将在事务中访问分区。因此,如果两个同时尝试,其中一个将失败并重试。
    【解决方案2】:

    这个算法很聪明,确实可以最大限度地减少写操作。因此长度值将收敛到最短长度和不存在冲突之间的平衡。

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

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

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

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

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

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

    【讨论】:

      【解决方案3】:

      只是为了在上面的问题中添加一些硬数字,我已经实现了一个小程序来按照问题中提到的方式生成 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)
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-01-21
        • 2021-02-07
        • 2020-11-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-05-16
        • 2020-12-08
        相关资源
        最近更新 更多