🌐

UUID v4よりも短く、同程度の衝突確率となるユニークなURL用文字列を自動生成したい (UUID v4 → Base64)

2025/02/16に公開

目的

構築中のサービスで、頻繁にURLに使われる文字列発行する必要があるとします。
例えば、ユーザーIDやそのユーザーに紐づく複数の詳細情報のキーとして生成し、それらの情報にアクセスするためのWEBページのURLとなるものです。
パッと思いつくのは UUID v4 を使うことですが、そのまま使うと32桁(ハイフン込みだと36桁)で長すぎるため、短くしたいと考えています。

例えば、以下のように user_iduser_history_id に使うような文字列を想定しています。
例:https://example.com/{user_id}/{user_history_id}

前提

  • 生成される文字列はURLに使えること
  • 生成される文字列にソート考慮(順序性)は不要
  • 生成される文字列に意味を持たせることは不要(人間が見てわかりやすくする必要なし)
  • UUID v4 並みの衝突確率を誇る方法でより短く表現できること
  • AWS Lambda(Ruby3.3) で動くこと

コード例

今回はいろんなLambda関数で同様に生成したいため、別のLambda関数からリクエストをもらったらレスポンスする感じで構築します。

Lambda 関数名の例: generateShortId
ファイル名の例: lambda_function.rb

require 'securerandom'
require 'base64'

def generate_short_id
  uuid = SecureRandom.uuid
  Base64.urlsafe_encode64([uuid.delete('-')].pack('H*')).delete('=')
end

def lambda_handler(event:, context:)
  { statusCode: 200, body: generate_short_id }
end

レスポンス例

上記を実行すると、以下のような22桁の結果が得られます。
UUID v4 を使った場合より短くなりました。

{
  "statusCode": 200,
  "body": "tXXVzFtZQ0e1xlGbFJ_N9Q"
}

原理

UUID v4とは?

簡単に言うと、「超ランダムでかぶりにくいID!」です。
UUIDにはいろんなバージョンがありますが、特にv4は以下の特徴があります。

  • 形式:xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx(4 は固定、y は 8, 9, a, b のどれか)
    • 550e8400-e29b-41d4-a716-446655440000
  • 乱数ベース(時間やMACアドレスに依存しない)
  • どのくらいかぶりにくいのか?
    • 128bitから固定の6bitを除くと 2^122 通りあるランダム識別子のため以下の数のバリエーションがあり、とりあえず自分が今世生きてる間に毎秒発番しても衝突はしなさそう。
      (2^122 = 5,316,911,983,139,663,491,615,228,241,121,378,304 ≒ 5.3澗)
  • セッションIDやDBのユニークキーに最適

https://ja.wikipedia.org/wiki/UUID#バージョン4

とはいえ、ほんとに衝突することを検討しなくていいか?

心配性な方向けに、以下の記事が参考になるかもしれません。
衝突させるのに何回くらい生成させると発生しそうかの期待値を考察された記事です。

信じるか信じないかは・・・あなた次第!
https://qiita.com/ta_ta_ta_miya/items/1f8f71db3c1bf2dfb7ea

securerandom

require 'securerandom' ですぐ使えます。
以下のリファレンスによると、Rubyにおいては securerandom を使うと、UUID v4 が利用可能とのことです。

https://docs.ruby-lang.org/ja/3.3/class/SecureRandom.html#S_UUID

Base64

require 'base64'ですぐ使えます。
Base64 エンコードは英字の小文字(a-z)と大文字(A-Z)、数字(0-9)、記号2種(+/)の64種類の文字が利用されます。厳密には、エンコードの過程で足りない部分を埋めるために=も利用されます(つまり全部で65種類の文字となる)。
詳しくは、みんな大好きWikipediaの「変換形式」に詳しく書かれているのでご参照ください。

https://ja.wikipedia.org/wiki/Base64#変換形式

Base64はあらゆるところに活用されていて、HTMLに小さなバイナリデータ(アイコンなど)を文字列として埋め込むことができたり、特殊文字の使用が制限されているシステムでデータを送信する場合に向いています。

一見、暗号化された文字列のようにも見えますが、単純にマッピングして変換しているだけであり、Base64を使っていることがバレれば元に戻す(復号:デコード)ことができてしまいます。セキュアな要件がある場合は、不可逆な(復号化できない)別手段を検討してください。

URLに使える文字・使えない文字

RFC3968 で定義されています。
Base64で使われる+/、そして=は、URLを構成する予約後として定義されています。
+はクエリパラメータのスペース(空白)、 /はパス構造、=もクエリパラメータのkeyとvalueを区切るものです。そのため、URLとして自由に使える別の文字に置き換えるか、削除するとURLセーフな文字列になります。

今回のコードでは+-/_に置き換え、=は削除します。

UUID v4を変換して22桁の文字列をレスポンスしているコード

下記コードが今回の肝となるコードですが、おさらい的にやっていることを書きます。

Base64.urlsafe_encode64([uuid.delete('-')].pack('H*')).delete('=')

1. uuid.delete('-')

UUID v4に含まれるハイフンを全て削除します。

uuid = "550e8400-e29b-41d4-a716-446655440000"
uuid.delete('-') # => "550e8400e29b41d4a716446655440000"

2. pack('H*')

16進数文字列をバイナリデータに変換するメソッドで、今回はUUID v4からハイフンを削除したものをバイナリデータに変換しました。

["550e8400e29b41d4a716446655440000"].pack('H*')

3. Base64.urlsafe_encode64(...)

バイナリデータをURLセーフなBase64文字列に変換します。
urlsafe_encode64を使うと+-に、/_に置き換えられ、URLの一部として安全に使用できます。

Base64.urlsafe_encode64("\x55\x0e\x84\x00\xe2\x9b\x41\xd4\xa7\x16\x44\x66\x55\x44\x00\x00")

4. .delete('=')

Base64エンコードの結果で末尾に=がついたものを削除し、URLセーフかつより短い文字列にします。

"VQ6EAOKbQdSnFkRmVUQAAA==".delete('=')
# => "VQ6EAOKbQdSnFkRmVUQAAA"

他のLambda関数から呼び出す場合

IMAポリシー設定

呼び出し元(generateShortIDを呼び出して使いたい方)のIAMのポリシーで、アクセス許可設定をしておきましょう。

{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Effect": "Allow",
			"Action": "lambda:InvokeFunction",
			"Resource": [
				"arn:aws:lambda:ap-northeast-1:*********:function:generateShortId"
			]
		}
	]
}

呼び出しコード(例)

# generateShortIdのARM(このコードではわかりやすく埋め込んでいるような形をとりましたが、必要に応じて環境変数などで管理してください)
lambda_function_name = "arn:aws:lambda:ap-northeast-1:*********:function:generateShortId"

response = lambda_client.invoke({
  function_name: lambda_function_name,
  payload: JSON.generate({})
})

# レスポンスを取得
response_payload = JSON.parse(response.payload.string)
if response_payload["statusCode"] == 200
  short_id = response_payload["body"]
end

考察・展望

UUID v4をBase64にすることで文字列を短く、同等の衝突確率を実現できた

Base64は単に元のUUID v4の文字列を変換しただけなので、同程度の衝突確率と言えると思います。22桁にまで減らせたためコンパクトに扱えます。

Base62を使って、様々なシステムに対応できる汎用性を検討する

今回はBase64を使ったため、生成文字列にはハイフンやアンダーバーが含まれます。今回は一応URLセーフな文字列だけを扱うようにしていますが、この文字列を処理するシステムによってはハイフンやアンダーバーが特別な意味を持つ可能性もゼロではないかもしれません。その場合はBase62という英字の大文字・小文字と数字(Base64から記号を省いたもの)を使うほうが無難です。

ソートが必要な場合は向かない

今回はソートの考慮を無視しましたが、ソートを考慮する必要がある場合は今回の方法はきついです。Base64に変換せず、UUID v7やULIDなど別の方法を検討すると良いかもしれません。また、データベースのプライマリーキーとして使う場合もUUID v4(完全ランダム)よりは、UUID v7やULIDの方がパフォーマンスがいくらか良くなるようです。

https://zenn.dev/kazu1/articles/e8a668d1d27d6b

メディロムグループ Tech Blog

Discussion