Table
UUID 是什麼?
舉個簡單現實中的例子你的電話號碼、員工編號、銀行帳號和身份證字號等,都和 UUID 的功用非常相似,是一組唯一識別的號碼或代號。
而在網路世界中通用的唯一識別碼,我們稱為 UUID (Universally Unique Identifie),RFC 4122 中標準化了 UUID 的實作規範。
以下是幾個 UUID 的實際樣子,由 128 位元 (36 字元) 組成:
- c2e456b2-6093-4c68-9746-2460c3ce19bb
- efeddb6a-1471-49f1-ab67-189b5e2758e1
其中 UUID 有不同的版本,每種的使用情境不同,接下來將會為大家一一介紹:
UUID v1:
- 組成結構:時間戳記 + MAC address 所生成
- 缺點:
- MAC address 是每一台電腦都會有的網卡,每一張上都會有不同的編號,如果使用 v1 要小心會有隱私性問題
- 而現在虛擬主機的 MAC address 是會被修改,如果遇到相同的 MAC address 則會有碰撞的情況發生
- uuid.js 統計大概有 20% 的人會使用這個版本的 UUID
- 以下是 Python 呼叫內建 library 所產生 uuid v1 的方式:
1 2 3 4 5 6 7 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | timestamp | | MAC address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ import uuid print(uuid.uuid1()) >>> 2b233aa4-d02d-3b16-a4ff-fc44606ec6ae |
UUID v2:
- 是一個較少被使用和提及的版本,UUIDv2 更多的是針對特定的系統和需求設計的,它主要用於包含 POSIX 安全性資訊(如用戶 ID 和群組 ID)的 UUID,在 Python 內建 library 中並沒有實作 UUID v2。
- 缺點:特定系統使用,且不在 Python 內建 library 裡面
UUID v3:
- 組成結構:namespace identifier (UUID) + 字串,使用 MD5 hash 產生
- 非隨機性的 UUID,如果你放入相同的 namespace 和字串,所產生的 UUID 會是相同
- 以下是 Python 呼叫內建 library 所產生 uuid v3 的方式:
1 2 3 4 5 6 7 8 9 10 11 12 |
# 以下是 python library 的程式碼,可以看到由 md5 hash 你所放入的資料 def uuid3(namespace, name): """Generate a UUID from the MD5 hash of a namespace UUID and a name.""" from hashlib import md5 hash = md5(namespace.bytes + bytes(name, "utf-8")).digest() return UUID(bytes=hash[:16], version=3) # 以下是 uuid3 的使用方式 import uuid print(uuid.uuid3(uuid.NAMESPACE_DNS, "max_su")) >>> 6fa459ea-ee8a-3ca4-894e-db77e160355e |
UUID v4:
- 組成結構:隨機變數產生的 UUID
- uuid.js 統計大概有 70% 的人會使用這個版本的 UUID
- 缺點:
- 有非常非常小機率發生碰撞 (找到重複項的機率是十億分之一)
- 如果是當資料庫的 primary key index 時,因為是 UUID v4 是隨機的產生的,速度會比有順序的 id 還來得慢,所以才會有後面的 UUID v7 產生
- 以下是 Python 呼叫內建 library 所產生 uuid v4 的方式:
1 2 3 4 5 6 7 |
+-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | random | | version | | random | +-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ import uuid print(uuid.uuid4()) >>> c2e456b2-6093-4c68-9746-2460c3ce19bb |
UUID v5:
- 組成結構:namespace identifier (UUID) + 字串,使用 SHA1 hash 產生
- 非隨機性的 UUID,如果你放入相同的 namespace 和字串,所產生的 UUID 會是相同
- 以下是 Python 呼叫內建 library 所產生 uuid v5 的方式:
1 2 3 4 5 6 7 8 9 10 11 12 |
# 以下是 python library 的程式碼,可以看到由 sha1 hash 你所放入的資料 def uuid5(namespace, name): """Generate a UUID from the SHA-1 hash of a namespace UUID and a name.""" from hashlib import sha1 hash = sha1(namespace.bytes + bytes(name, "utf-8")).digest() return UUID(bytes=hash[:16], version=5) # 以下是 uuid5 的使用方式 import uuid print(uuid.uuid5(uuid.NAMESPACE_DNS, "max_su")) >>> 886313e1-3b8a-5372-9b90-0c9aee199e5d |
UUID v7:
- RFC 4122 最後更新於 16 February 2023
- 目前 Python 內建 library 還沒有實作 v7,不過 PR 已經在審核了
- 組成結構:前面 48 bits 由 timestamp 組成,後面是隨機組成
- 優點:
- 前面是由 timestamp 所組成是有順序性的,所以在當 primary key index 時速度上會遠大於 uuid v4 的版本,可以參考這篇文章有人做了速度實驗上比較:Why UUID7 is better than UUID4 as clustered index in RDBMS
- 後面時有隨機組成,所以解決了 uuid v1 的 MAC address 的隱私性和碰撞的可能
UUID.hex
“Hex” 是 “Hexadecimal” 的縮寫,意思是「十六進位」,十六進位是由 0 到 9 和 A 到 F 所表示,所以會去除 -,這種格式使得 UUID 更適合用於 URLs 和檔案命名上的傳輸,資料庫儲存的空間也從 36 字元下降為 32 字元。
1 2 3 4 5 |
uuid.uuid4() >>>2a93bb6e-e881-435c-9a8f-2390bb522804 uuid.uuid4().hex >>>2a93bb6ee881435c9a8f2390bb522804 |
特殊的 UUID
- nil UUID: 表示為「空」或「未指定」的 UUID,會將所有位元設為 0,00000000-0000-0000-0000-000000000000
- max UUID: UUID 範圍內的最大值,會將所有位元設為 1,ffffffff-ffff-ffff-ffff-ffffffffffff
其他相關 ID
Snowflake ID (雪花 ID)
UUID 是由 128 位元所組成,在儲存上的長度是個硬傷,如果想要縮短長度又會怕有碰撞,要怎麼解決這個問題?我們來看看由 Twitter (X) 所提出的開發的解決方案 Snowflake ID。
在現實中每片雪花都是獨一無二,且有自己獨特的結構。所以在命名上將其命名為「Snowflake (雪花) ID」。
Snowflake ID 是由 Twitter 開發的解決方案,他們需要解決平均每秒 9000 條推文,峰值高達每秒 143199 條推文的 ID,不僅可以在其龐大的分散式伺服器中擴展,還可以產生高效的儲存 ID (大致按時間排序)。Snowflake ID 也被 Discord 和 Instagram 等公司使用。
組成結構:由 64 位元組成,相較 UUID 少了一半的長度
1 2 3 |
+--------------------------------------------------------------------------+ | 1 Bit Unused | 41 Bit Timestamp | 10 Bit NodeID | 12 Bit Sequence ID | +--------------------------------------------------------------------------+ |
- 42 bit 時間戳記
- 10 bit 機器號碼 (or any random number you provide)
- 12 bit 順序號碼
以下是我參考 Source code 所改寫的 python snowflake,主要是不想安裝任何的套件,使用方式簡單,僅需呼叫 snowflake_id(),即可獲得一組新的雪花 ID。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
import random from dataclasses import dataclass from datetime import datetime, timedelta from time import time from typing import Optional __all__ = ("Snowflake", "SnowflakeGenerator") MAX_INSTANCE = 0b1111111111 # 分散式系統最多 1023 個節點 MAX_SEQ = 0b111111111111 # 每毫秒能生成的 4095 ID 數量 MAX_TS = 0b11111111111111111111111111111111111111111 # 大約等於 69.73 年 @dataclass(frozen=True) class Snowflake: timestamp: int instance: int epoch: int = 0 # 起始時間點(epoch) seq: int = 0 def __post_init__(self): if self.epoch < 0: raise ValueError(f"epoch must be greater than 0!") if self.timestamp < 0 or self.timestamp > MAX_TS: raise ValueError( f"timestamp must not be negative and must be less than {MAX_TS}!" ) if self.instance < 0 or self.instance > MAX_INSTANCE: raise ValueError( f"instance must not be negative and must be less than {MAX_INSTANCE}!" ) if self.seq < 0 or self.seq > MAX_SEQ: raise ValueError( f"seq must not be negative and must be less than {MAX_SEQ}!" ) @classmethod def parse(cls, snowflake: int, epoch: int = 0) -> "Snowflake": return cls( epoch=epoch, timestamp=snowflake >> 22, instance=snowflake >> 12 & MAX_INSTANCE, seq=snowflake & MAX_SEQ, ) @property def milliseconds(self) -> int: return self.timestamp + self.epoch @property def seconds(self) -> float: return self.milliseconds / 1000 @property def datetime(self) -> datetime: return datetime.utcfromtimestamp(self.seconds) @property def timedelta(self) -> timedelta: return timedelta(milliseconds=self.epoch) @property def value(self) -> int: return self.timestamp << 22 | self.instance << 12 | self.seq def __int__(self) -> int: return self.value class SnowflakeGenerator: def __init__( self, instance: int, *, seq: int = 0, epoch: int = 0, timestamp: Optional[int] = None, ): current = (time() * 1000.0).__int__() if current - epoch >= MAX_TS: raise OverflowError( f"The maximum current timestamp has been reached in selected epoch," f"so Snowflake cannot generate more IDs!" ) timestamp = timestamp or current if timestamp < 0 or timestamp > current: raise ValueError( f"timestamp must not be negative and must be less than {current}!" ) if epoch < 0 or epoch > current: raise ValueError( f"epoch must not be negative and must be lower than current time {current}!" ) self._epo = epoch self._ts = timestamp - self._epo if instance < 0 or instance > MAX_INSTANCE: raise ValueError( f"instance must not be negative and must be less than {MAX_INSTANCE}!" ) if seq < 0 or seq > MAX_SEQ: raise ValueError( f"seq must not be negative and must be less than {MAX_SEQ}!" ) self._inf = instance << 12 self._seq = seq @classmethod def from_snowflake(cls, sf: Snowflake) -> "SnowflakeGenerator": return cls(sf.instance, seq=sf.seq, epoch=sf.epoch, timestamp=sf.timestamp) @property def epoch(self) -> int: return self._epo def __iter__(self): return self def __next__(self) -> str: current = (time() * 1000.0).__int__() - self._epo if current >= MAX_TS: raise OverflowError( f"The maximum current timestamp has been reached in selected epoch," f"so Snowflake cannot generate more IDs!" ) if self._ts == current: if self._seq == MAX_SEQ: raise OverflowError( f"The maximum current sequence has been reached in selected epoch," f"so Snowflake cannot generate more IDs!" ) self._seq += 1 else: self._seq = 0 self._ts = current return str(self._ts << 22 | self._inf | self._seq) def __call__(self) -> str: return self.__next__() snowflake_id = SnowflakeGenerator(random.randint(1, 1023)) |
除了雪花 ID 外,以下幾種 id 都使用了類似的手法將 timestamp 加入在開頭:
Sharding & IDs at Instagram
MongoDB Object ID
ULID spec
以上是我花了點時間所整理的資訊,希望有幫助到你,那本篇就在這邊告一個段落,感謝收看 🙂
延伸閱讀
▍關於 Python 教學系列目錄:
▍關於 Flask 教學系列目錄:
▍關於 Git 教學系列目錄: