適合用於資料庫主鍵的最佳UUID工具庫 - Vlad Mihalcea

banq發表於2022-12-11

在本文中,我們將瞭解哪種 UUID(通用唯一識別符號)型別最適合具有主鍵約束的資料庫列。
雖然標準的 128 位隨機 UUID 是一個非常受歡迎的選擇,但您會發現這非常適合資料庫主鍵列。

通用唯一識別符號 (UUID) 是一個 128 位偽隨機序列,可以獨立生成,無需單個集中式系統負責確保識別符號的唯一性。
RFC 4122 規範定義了UUID 的五個標準化版本,它們由各種資料庫函式或程式語言實現。
例如,UUID()MySQL 函式返回版本 1 UUID 編號。
並且 JavaUUID.randomUUID()函式返回版本 4 UUID 編號。
對於許多開發人員來說,使用這些標準 UUID 作為資料庫識別符號非常有吸引力,因為:
  • ids 可以由應用程式生成。因此不需要中央協調。
  • 識別符號衝突的可能性極低。
  • id 值是隨機的,您可以安全地將它傳送到 UI,因為使用者將無法猜測其他識別符號值並使用它們來檢視其他人的資料。

但是,出於多種原因,使用隨機 UUID 作為資料庫表主鍵不是一個好主意。
首先,UUID 很大。每條記錄都需要 16 個位元組作為資料庫識別符號,這也會影響所有關聯的外來鍵列。
其次,Primary Key 列通常有一個關聯的 B+Tree 索引來加速查詢或連線,B+Tree 索引按排序順序儲存資料。
然而,使用 B+Tree 索引隨機值會導致很多問題:
  • 索引頁面將具有非常低的填充因子,因為這些值是隨機出現的。因此,一個 8kB 的頁面最終將只儲存幾個元素,因此在磁碟和資料庫記憶體中浪費了大量空間,因為索引頁面可以快取在緩衝池中。
  • 由於 B+Tree 索引需要重新平衡自身以保持其等距樹結構,隨機鍵值將導致更多的索引頁拆分和合並,因為沒有預先確定的填充樹結構的順序。

如果你使用的是 SQL Server 或 MySQL,那就更糟了,因為整個表基本上是一個聚集索引

事實上,幾乎所有資料庫專家都會告訴您避免使用標準 UUID 作為資料庫表主鍵:


TSID – 按時間排序的唯一識別符號
如果您計劃將 UUID 值儲存在主鍵列中,那麼您最好使用 TSID(按時間排序的唯一識別符號)。

TSID Creator OSS 庫提供了一種此類實現,它提供了一個由兩部分組成的 64 位 TSID:

  • 一個 42 位時間元件
  • 一個 22 位隨機分量

隨機成分有兩部分:
  • 節點識別符號(0 到 20 位)
  • 一個計數器(2 到 22 位)

tsidcreator.node引導應用程式時,系統屬性可以提供節點識別符號:
-Dtsidcreator.node="12"

節點識別符號也可以透過環境變數提供TSIDCREATOR_NODE:
export TSIDCREATOR_NODE="12"

該庫在 Maven Central 上可用,因此您可以透過以下依賴項獲取它:

<dependency>
    <groupId>com.github.f4b6a3</groupId>
    <artifactId>tsid-creator</artifactId>
    <version>${tsid-creator.version}</version>
</dependency>

您可以建立一個Tsid最多可以使用 256 個節點的物件,如下所示:

Tsid tsid = TsidCreator.getTsid256();
從Tsid物件中,我們可以提取以下值:

64 位數值,
編碼 64 位值的Crockford 的 Base32 字串值,
儲存在42-bit 序列中的紀元以來的 Unix 毫秒數
為了視覺化這些值,我們可以將它們列印到日誌中:

long tsidLong = tsid.toLong();
String tsidString = tsid.toString();
long tsidMillis = tsid.getUnixMilliseconds();
 
LOGGER.info(
    "TSID numerical value: {}",
    tsidLong
);
 
LOGGER.info(
    "TSID string value: {}",
    tsidString
);
 
LOGGER.info(
    "TSID time millis since epoch value: {}",
    tsidMillis
);

我們得到以下輸出:

TSID numerical value: 388400145978465528
TSID string value: 0ARYZVZXW377R
TSID time millis since epoch value: 1670438610927

生成十個值時:

for (int i = 0; i < 10; i++) {
    LOGGER.info(
        "TSID numerical value: {}",
        TsidCreator.getTsid256().toLong()
    );
}

我們可以看到值是單調遞增的:

TSID numerical value: 388401207189971936
TSID numerical value: 388401207189971937
TSID numerical value: 388401207194165637
TSID numerical value: 388401207194165638
TSID numerical value: 388401207194165639
TSID numerical value: 388401207194165640
TSID numerical value: 388401207194165641
TSID numerical value: 388401207194165642
TSID numerical value: 388401207194165643
TSID numerical value: 388401207194165644

避免同步
因為透過TsidCreator工具提供的預設TSID工廠帶有一個同步的隨機值生成器,所以最好使用一個自定義的TsidFactory,提供以下最佳化。

  • 它可以使用ThreadLocalRandom生成隨機值,因此避免了同步塊上的執行緒阻塞
  • 它可以使用少量的節點位,因此為隨機生成的數值留下更多的位。

因此,我們可以定義下面的TsidUtil,它為我們提供了一個TsidFactory,在我們想要生成一個新的Tsid物件時使用。


public static class TsidUtil {
    public static final String TSID_NODE_COUNT_PROPERTY =
        "tsid.node.count";
    public static final String TSID_NODE_COUNT_ENV =
        "TSID_NODE_COUNT";
 
    public static TsidFactory TSID_FACTORY;
 
    static {
        String nodeCountSetting = System.getProperty(
            TSID_NODE_COUNT_PROPERTY
        );
        if(nodeCountSetting == null) {
            nodeCountSetting = System.getenv(
                TSID_NODE_COUNT_ENV
            );
        }
 
        int nodeCount = nodeCountSetting != null ?
            Integer.parseInt(nodeCountSetting) :
            256;
 
        int nodeBits = (int) (Math.log(nodeCount) / Math.log(2));
 
        TSID_FACTORY = TsidFactory.builder()
            .withRandomFunction(length -> {
                final byte[] bytes = new byte[length];
                ThreadLocalRandom.current().nextBytes(bytes);
                return bytes;
            })
            .withNodeBits(nodeBits)
            .build();
    }
}


結論
使用標準 UUID 作為主鍵值不是一個好主意,除非第一個位元組是單調遞增的。
因此,使用按時間排序的 TSID 是一個更好的主意。它不僅需要標準 UUID 一半的位元組數,而且更適合作為 B+Tree 索引鍵。
雖然 SQL Server 透過 提供按時間排序的 GUID NEWSEQUENTIALID,但 GUID 的大小為 128 位,因此它是 TSID 的兩倍。

UUID 規範的第 7 版也存在同樣的問題,它提供了按時間排序的 UUID。但是,它使用相同的規範格式(128 位),但格式太大了。每個引用外來鍵列都會放大主鍵列儲存的影響。
如果您所有的主鍵都是 128 位 UUID,那麼主鍵和外來鍵索引將需要大量空間,包括磁碟和資料庫記憶體,因為緩衝池同時包含表和索引頁。

相關文章