UUID - 原理介紹

最近探討一個議題,也就是資料庫的主鍵欄位究竟要有 UUID 還是自動遞增的整數好呢?

那要先了解到底什麼是 UUID。

UUID 介紹

英文又叫做 Universally Unique Identifier,它是由 128 位元組成的一個識別碼,通常是透過 32 個十六進制來表示。

我查資料的時候大部分都說它具有唯一性,真的嗎?

我們來探討一下。

UUID 的版本演化

UUID 其實有多個版本的算法,每個版本的算法皆有差異性,也會影響它們究竟是不是能保持唯一性的關鍵。

  • Version 1 基於時間

    透過當前時間戳、機器 MAC 地址生成,因為 MAC 地址是全球唯一的,因此可以間接的保證 UUID 全球唯一,

    缺點:

    1. 會暴露電腦的 MAC adress
    2. 暴露生成這個 UUID 的時間
    3. 因為 MAC address 會不會重複還要取決於網咖製造商正確分配唯一的 MAC address,有出錯的功能
    4. 經證實,可以透過 UUID 回敲建立它的電腦,藉此散播病毒
  • Version 2 DCE 安全

    和 Version 1 算法相同,但是會將時間戳的前四位置換成 POSIX 的 UID 或 GID,但是在 UUID 規範中並沒有被明確指定,這版本其實可以選擇忽略過去

  • Version 3 基於 Namespace 和一個字串

    透過用戶指定一個命名空間及一個字串,然後利用 MD5 雜湊演算法來生成 UUID

    缺點:固定的輸入會產生一致的 UUID

  • Version 4 基於隨機數

    根據隨機樹或者偽隨機數生成 UUID,重複的機率是可以被計算出來的,但是該機率極其低微,也是最多被用的版本

  • Version 5 基於 Namespace 和一個字串

    基本上 Version 3 一樣,但是使用的演算法為 SHA1,缺點一樣就是輸入一樣會產生一致的 UUID

UUID 唯一性?

如果不考慮版本一跟二,那就只有版本三、四、五可以來當作每一筆資料的主鍵。可是我們都知道主鍵一定要有唯一性,如果是版本三、五只要輸入一樣就會產生相同的 UUID。

所以其實我查到最多的作法是採用版本四的算法來產生唯一性的 UUID,機率嘛,雖然數學式子證明確是極其低微才會重複,我覺得是可以選擇忽略重複的後果。

反正工程師一堆要擔心的事情,比這個發生的機率都大太多了…

Golang UUID 程式庫示範

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
package main

import (
"github.com/google/uuid"
"log"
)

func main() {
u1, err := uuid.NewUUID()
if err != nil {
log.Fatalf("can not generate Version 1 UUID: %v", err)
}
log.Printf("generate Version 1 UUID: %v", u1)

u2, err := uuid.NewDCEGroup()
if err != nil {
log.Fatalf("can not generate Version 2 UUID: %v", err)
}
log.Printf("generate Version 2 UUID: %v", u2)

u3 := uuid.NewMD5(u1, []byte("Kenny"))
log.Printf("generate Version 3 UUID: %v", u3)

u4, err := uuid.NewRandom()
if err != nil {
log.Fatalf("can not generate Version 4 UUID: %v", err)
}
log.Printf("generate Version 4 UUID: %v", u4)

u5 := uuid.NewSHA1(u1, []byte("Kenny"))
log.Printf("generate Version 5 UUID: %v", u5)
}

這個是由 Google 所開源的 Golang UUID 程式庫,也是我推薦可以使用該程式庫來實現 UUID。上面的程式碼分別就是示範了 Version 1 ~ Version 5 的 UUID。

在這邊可以發現 Version 3 跟 Version 5,都需要丟兩個值進去,分別是 UUID 跟 byte,因為 Version 3 跟 Version 5’上面說過是透過命名空間和一個字串去組成的,其實命名空間就是一個 UUID 的值,因此可以想成用一個 UUID 跟一個字串進行某種演算法在形成一個新的 UUID。

缺點就是只要每次給予相同的輸入,輸出的 UUID 就一定會一樣。比如說你可以這樣實驗:

1
2
3
4
5
6
a, _ := uuid.Parse("5a680224-2ae5-11ea-8051-00155d3db160")
u3 := uuid.NewMD5(a, []byte("Kenny"))
log.Printf("generate Version 3 UUID: %v", u3)

u5 := uuid.NewSHA1(a, []byte("Kenny"))
log.Printf("generate Version 5 UUID: %v", u5)

透過 uuid.Parse 將一個字串型態的 UUID,轉成 UUID 型態,最後在餵給 Version 3 跟 Version 5 去產生新的 UUID,你會發現每一次執行之後產生的 UUID 都是一樣的。

總結

通常會建議如果要以 UUID 唯一性的特性來考量的話,會採用 Version 4 來產生,確保唯一性,重複的機率極其低微這樣~

回歸資料庫主鍵的問題來看,我們假設 UUID 的唯一性是肯定的,那對於資料庫主鍵的問題就轉成效能的問題,以及每個不同場景的應用,但我覺得 UUID 最大的優點就是看起來是無序的,而且不像自動遞增那樣會被看出大致資料庫資料數量以及也猜不到 PK 的值。

不過對於不同的資料庫對於 UUID 的效能其實都會有所差異,等有空來好好探討資料庫主鍵用自動遞增跟 UUID 優缺點。