6

Google App Engineアプリケーションで使用するための一意のIDを生成しようとしていますが、使用を検討しているアプローチの実現可能性についてフィードバックを求めています(最後の質問)。このトピックに関するかなりの数の質問を読みましたが、この特定のアプローチに出くわしたことを覚えていません。

MD5ハッシュなどのランダムに見えるIDが欲しいのですが、それらも小さくしたいと思います。tinyurlの線に沿って4〜6文字が理想的です。IDは、ユーザーが作成したコンテンツ用であり、私のアプリケーションのコンテキストでは、人々が書くテストの質問などです。IDがランダムに見える必要はありませんが(シリアルIDのように見えても問題ありません)、私が考えているアプローチはこれに適しているので、実際には問題ではありません。

Google App Engineに精通している人は、データストアへの書き込みは特にコストがかかり、同じエンティティグループへの書き込みが多すぎるとタイムアウトになる可能性があることを知っています。シャードカウンターは、単一のグローバルカウンターでの書き込みの競合と、それに伴う失敗したトランザクションを回避するためによく使用されるアプローチです。

短いIDを取得し、書き込みの競合を回避するとともに、誕生日のパラドックスを回避しようとしています。少し行き過ぎても、何百万ものIDが存在する可能性に備えたいと思います。

私は次の線に沿ってシャードカウンターを使用することを考えていました:

  • カウンターはユーザーごとにシャーディングされるため、ユーザーごとにシャードがあります。各カウンターオブジェクトには、特定のユーザーに固有の独自のカウントがあり、そのユーザーによって新しいアイテムが作成されると増分されます。アイテムが正常に作成されたかどうかに関係なく、カウントは増加します。
  • IDの基本は、次の文字列のMD5ハッシュです: "<user-email-address>|<latest-counter-value>"。
  • 結果のMD5ハッシュは、最初は4文字に切り捨てられます。
  • 単一のグローバル「長さ」値が維持されます。前の手順でキーが重複する場合は常に(これは最初はかなり迅速に発生すると想像されます)、長さの値は1つ増加します。新しいIDのMD5ハッシュは、4文字ではなく「長さ」の文字で切り捨てられるようになりました。
  • ユーザーの電子メールアドレスを公開したくありません。これは、ある種のハッシュが適切な方法であることを示唆しています。

私の質問は次のとおりです。これにより、キーの重複による書き込みの競合が大幅に回避され、長さフィールドでの書き込みの競合は、特に長い長さではおそらく問題にならないだろうと思いますか?ここに含まれる数学を誰かが説明できますか?長さはMD5ハッシュの長さ近くまで急速に増加し、アプローチ全体の価値に疑問を投げかけますか?物事を維持しやすくするために、完全な(より長い)MD5ハッシュを使用する方が良いでしょうか?私が見落としているものはありますか?

4

3 に答える 3

2

このようなものはどうですか:

  1. a-zA-Z0-9を使用して4文字のキーが必要な場合は、次のようになります。62 ^ 4=>1,400万の可能な値。

  2. これをN個のパーティションに分割します:0000 ... 1000、1001 ... 2000、...、ZZAA ... ZZZZ

    各パーティションは、次のエンティティで表されます。開始ID終了ID現在のID

ここで、IDを生成するには:

  1. ランダムにパーティションを選択します。各パーティションの開始キーをキーとして使用します。トランザクションの開始:
  2. get_or_create()そのパーティションエンティティ。
  3. 現在のID==終了IDの場合は、手順1に進みます。
  4. 現在のIDを保存
  5. 現在のIDを1つのエンドトランザクションでインクリメントします

IDとして保存された現在のIDを使用します。

Nを140として選択すると、各パーティションの値は100,000になります。これにより、「空の」パーティションを選択することによる再試行の回数を制限しながら、かなりの数の同時挿入が可能になります。

これについてもっと考える必要があるかもしれません。特に、5桁または6桁のキーに移動する必要がある場合、どのように対処しますか?

-デイブ

于 2009-05-03T03:20:16.027 に答える
1

上記の質問に具体的な数値を追加するために、質問に記載されている行に沿って ID を生成する小さなプログラムを実装しました。これらは、試用実行の 1 つの結果でした。

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

ハッシュの長さが増加するたびに衝突が発生したため、この例では 6 回の衝突が発生しました。カウントは、衝突の前に生成された特定の長さのキーの数です。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 の生成に関連するいくつかの処理を行うクラスです。デフォルトでは、GAE キー名の最初の文字はアルファベットでなければならないため、このクラスによって生成されるキーは純粋なベース 62 ではありません。ここでは、最初の文字に base 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 (またはより高速なアドラー) を使用できるランダム ビット ジェネレーターです。特にコードが携帯電話で実行される場合。crc32 でランダム ビット ジェネレーターを実装するには、32 ビット値を文字列の先頭に追加してハッシュし、選択した定数値 (シード) で初期化します。次に、この文字列の crc32 を計算します。さらにバイトが必要な場合は、取得した crc32 値を文字列の前の 32 ビットに書き込み、crc32 を再計算します。十分なビットが得られるまで繰り返します。crc32 を選択したアルゴリズムに置き換えることができます。これにより、初期定数がシードである安価で高速なランダム ビット ジェネレーターが得られます。

このアルゴリズムを使用すると、生成されるランダム ビットの数を最小限に抑えることができ、長さの上限もありません。

エンコーディングに関しては、制約を指定しませんでした。数字に大文字と小文字を使用できますか? この例では、36 個の異なる ASCII 値のアルファベットを使用しています。必要なバイト数を生成できる上記の疑似乱数ジェネレーターを取得したら、長さを ID の文字数に定義し、次のランダム バイトのモジュロで次の文字を選択するだけです。一度に生成するバイト数が正確にわかり、ID の生成は簡単です。

于 2009-05-08T09:40:46.943 に答える