UUIDv7的20種語言實現

banq發表於2024-06-16


UUIDv7 是一個 128 位唯一識別符號,就像它的前輩一樣,例如廣泛使用的 UUIDv4。但與 v4 不同,UUIDv7 可以按時間排序,精度為 1 毫秒。透過結合時間戳和隨機部分,UUIDv7 成為資料庫(包括分散式資料庫)中記錄識別符號的絕佳選擇。

讓我們簡要探索一下 UUIDv7 結構,然後討論 20 種語言的零依賴實現(根據 Stack Overflow 調查排名)。


結構
UUIDv7 以字串形式表示時如下所示:

0190163d-8694-739b-aea5-966c26f8ad91
└─timestamp─┘ │└─┤ │└───rand_b─────┘
             ver │var
              rand_a

這個 128 位的值由幾部分組成:
  • timestamp(48 位)是以毫秒為單位的 Unix 時間戳。
  • ver(4 位)是 UUID 版本(7)。
  • rand_a(12 位)是隨機生成的。
  • var* (2 位) 等於10。
  • rand_b(62 位)是隨機生成的。


* 在字串表示法中,每個符號編碼 4 位十六進位制數,因此示例中的 a 是 1010,其中前兩位是固定變體(10),後兩位是隨機變數。因此得到的十六進位制數可以是 8 (1000)、9 (1001)、a (1010) 或 b (1011)。

細節: RFC 9652

JavaScript
用 crypto.getRandomValues()初始化一個隨機陣列,用 Date.now()獲取當前時間戳,用時間戳填充陣列,設定版本和變體。

function uuidv7() {
    // random bytes
    const value = new Uint8Array(16);
    crypto.getRandomValues(value);

    // current timestamp in ms
    const timestamp = BigInt(Date.now());

    // timestamp
    value[0] = Number((timestamp >> 40n) & 0xffn);
    value[1] = Number((timestamp >> 32n) & 0xffn);
    value[2] = Number((timestamp >> 24n) & 0xffn);
    value[3] = Number((timestamp >> 16n) & 0xffn);
    value[4] = Number((timestamp >> 8n) & 0xffn);
    value[5] = Number(timestamp & 0xffn);

    // version and variant
    value[6] = (value[6] & 0x0f) | 0x70;
    value[8] = (value[8] & 0x3f) | 0x80;

    return value;
}

const uuidVal = uuidv7();
const uuidStr = Array.from(uuidVal)
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
console.log(uuidStr);


TypeScript 版本相同,唯一的區別是函式簽名:

function uuidv7(): Uint8Array {
    // ...
}

Python
用os.urandom() 初始化一個隨機陣列,用time.time() 獲取當前時間戳,用時間戳填充陣列,設定版本和變體。

import os
import time

def uuidv7():
    # random bytes
    value = bytearray(os.urandom(16))

    # current timestamp in ms
    timestamp = int(time.time() * 1000)

    # timestamp
    value[0] = (timestamp >> 40) & 0xFF
    value[1] = (timestamp >> 32) & 0xFF
    value[2] = (timestamp >> 24) & 0xFF
    value[3] = (timestamp >> 16) & 0xFF
    value[4] = (timestamp >> 8) & 0xFF
    value[5] = timestamp & 0xFF

    # version and variant
    value[6] = (value[6] & 0x0F) | 0x70
    value[8] = (value[8] & 0x3F) | 0x80

    return value

if __name__ == "__main__":
    uuid_val = uuidv7()
    print(''.join(f'{byte:02x}' for byte in uuid_val))


SQL
使用 strftime() (SQLite) 或 now() (PostgreSQL) 獲取當前時間戳部分,使用 random() 獲取隨機部分,然後將所有內容連線成 UUID 字串。
SQLite (by Fabio Lima):

select
  -- timestamp
  format('%08x', ((strftime('%s') * 1000) >> 16)) || '-' ||
  format('%04x', ((strftime('%s') * 1000)
    + ((strftime('%f') * 1000) % 1000)) & 0xffff) || '-' ||
  -- version / rand_a
  format('%04x', 0x7000 + abs(random()) % 0x0fff) || '-' ||
  -- variant / rand_b
  format('%04x', 0x8000 + abs(random()) % 0x3fff) || '-' ||
  -- rand_b
  format('%012x', abs(random()) >> 16) as value;

PostgreSQL:

select
  -- timestamp
  lpad(to_hex(((extract(epoch from now()) * 1000)::bigint >> 16)), 8, '0') || '-' ||
  lpad(to_hex(((extract(epoch from now()) * 1000
    + (date_part('milliseconds', now())::bigint % 1000))::bigint & 0xffff)), 4, '0') || '-' ||
  -- version / rand_a
  lpad(to_hex((0x7000 + (random() * 0x0fff)::int)), 4, '0') || '-' ||
  -- variant / rand_b
  lpad(to_hex((0x8000 + (random() * 0x3fff)::int)), 4, '0') || '-' ||
  -- rand_b
  lpad(to_hex((floor(random() * (2^48))::bigint >> 16)), 12, '0') AS value;

Shell
用 /dev/urandom 初始化隨機位元組字串,用日期獲取當前時間戳,用時間戳和位元組字串填充陣列,設定版本和變數。

#!/bin/sh

uuidv7() {
    # random bytes
    rand_bytes=$(dd if=/dev/urandom bs=1 count=16 2>/dev/null | xxd -p)

    # current timestamp in ms
    timestamp=$(date +%s%3N)
    t_hex=$(printf "%012x" $timestamp)

    # timestamp
    value[0]=${t_hex:0:2}
    value[1]=${t_hex:2:2}
    value[2]=${t_hex:4:2}
    value[3]=${t_hex:6:2}
    value[4]=${t_hex:8:2}
    value[5]=${t_hex:10:2}

    # version / rand_a
    value[6]=$(printf "%02x" $((0x70 | (0x${rand_bytes:12:2} & 0x0F))))
    value[7]=${rand_bytes:14:2}

    # variant / rand_b
    value[8]=$(printf "%02x" $((0x80 | (0x${rand_bytes:16:2} & 0x3F))))

    # rand_b
    value[9]=${rand_bytes:18:2}
    value[10]=${rand_bytes:20:2}
    value[11]=${rand_bytes:22:2}
    value[12]=${rand_bytes:24:2}
    value[13]=${rand_bytes:26:2}
    value[14]=${rand_bytes:28:2}
    value[15]=${rand_bytes:30:2}

    echo "${value[@]}"
}

for byte in $(uuidv7); do
    printf "%s" "$byte"
done
echo


Java
使用 SecureRandom.nextBytes() 初始化隨機陣列,使用 System.currentTimeMillis() 獲取當前時間戳,根據時間戳填充陣列,設定版本和變數。

import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.util.UUID;

public class UUIDv7 {
    private static final SecureRandom random = new SecureRandom();

    public static UUID randomUUID() {
        byte[] value = randomBytes();
        ByteBuffer buf = ByteBuffer.wrap(value);
        long high = buf.getLong();
        long low = buf.getLong();
        return new UUID(high, low);
    }

    public static byte[] randomBytes() {
        // random bytes
        byte[] value = new byte[16];
        random.nextBytes(value);

        // current timestamp in ms
        long timestamp = System.currentTimeMillis();

        // timestamp
        value[0] = (byte) ((timestamp >> 40) & 0xFF);
        value[1] = (byte) ((timestamp >> 32) & 0xFF);
        value[2] = (byte) ((timestamp >> 24) & 0xFF);
        value[3] = (byte) ((timestamp >> 16) & 0xFF);
        value[4] = (byte) ((timestamp >> 8) & 0xFF);
        value[5] = (byte) (timestamp & 0xFF);

        // version and variant
        value[6] = (byte) ((value[6] & 0x0F) | 0x70);
        value[8] = (byte) ((value[8] & 0x3F) | 0x80);

        return value;
    }

    public static void main(String[] args) {
        var uuid = UUIDv7.randomUUID();
        System.out.println(uuid);
    }
}

by David Ankin


C#
使用 RandomNumberGenerator.GetBytes() 初始化隨機陣列,使用 DateTimeOffset.UtcNow 獲取當前時間戳,根據時間戳填充陣列,設定版本和變數。

using System;
using System.Security.Cryptography;

public class UUIDv7 {
    private static readonly RandomNumberGenerator random =
        RandomNumberGenerator.Create();

    public static byte[] Generate() {
        // random bytes
        byte[] value = new byte[16];
        random.GetBytes(value);

        // current timestamp in ms
        long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

        // timestamp
        value[0] = (byte)((timestamp >> 40) & 0xFF);
        value[1] = (byte)((timestamp >> 32) & 0xFF);
        value[2] = (byte)((timestamp >> 24) & 0xFF);
        value[3] = (byte)((timestamp >> 16) & 0xFF);
        value[4] = (byte)((timestamp >> 8) & 0xFF);
        value[5] = (byte)(timestamp & 0xFF);

        // version and variant
        value[6] = (byte)((value[6] & 0x0F) | 0x70);
        value[8] = (byte)((value[8] & 0x3F) | 0x80);

        return value;
    }

    public static void Main(string[] args) {
        byte[] uuidVal = Generate();
        foreach (byte b in uuidVal) {
            Console.Write("{0:x2}", b);
        }
        Console.WriteLine();
    }
}

C++
使用 random_device 初始化隨機陣列,使用 system_clock.time_since_epoch()獲取當前時間戳,根據時間戳填充陣列,設定版本和變數。

include <array>
include <chrono>
include <cstdint>
include <cstdio>
include <random>

std::array<uint8_t, 16> uuidv7() {
    // random bytes
    std::random_device rd;
    std::array<uint8_t, 16> random_bytes;
    std::generate(random_bytes.begin(), random_bytes.end(), std::ref(rd));
    std::array<uint8_t, 16> value;
    std::copy(random_bytes.begin(), random_bytes.end(), value.begin());

    // current timestamp in ms
    auto now = std::chrono::system_clock::now();
    auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(
        now.time_since_epoch()
    ).count();

    // timestamp
    value[0] = (millis >> 40) & 0xFF;
    value[1] = (millis >> 32) & 0xFF;
    value[2] = (millis >> 24) & 0xFF;
    value[3] = (millis >> 16) & 0xFF;
    value[4] = (millis >> 8) & 0xFF;
    value[5] = millis & 0xFF;

    // version and variant
    value[6] = (value[6] & 0x0F) | 0x70;
    value[8] = (value[8] & 0x3F) | 0x80;

    return value;
}

int main() {
    auto uuid_val = uuidv7();
    for (const auto& byte : uuid_val) {
        printf("%02x", byte);
    }
    printf("\n");
    return 0;
}

C語言
使用 getentropy() 初始化隨機陣列,使用 timespec_get()獲取當前時間戳,根據時間戳填充陣列,設定版本和變數。

include <stdio.h>
include <stdlib.h>
include <stdint.h>
include <time.h>
include <unistd.h>

int uuidv7(uint8_t* value) {
    // random bytes
    int err = getentropy(value, 16);
    if (err != EXIT_SUCCESS) {
        return EXIT_FAILURE;
    }

    // current timestamp in ms
    struct timespec ts;
    int ok = timespec_get(&ts, TIME_UTC);
    if (ok == 0) {
        return EXIT_FAILURE;
    }
    uint64_t timestamp = (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;

    // timestamp
    value[0] = (timestamp >> 40) & 0xFF;
    value[1] = (timestamp >> 32) & 0xFF;
    value[2] = (timestamp >> 24) & 0xFF;
    value[3] = (timestamp >> 16) & 0xFF;
    value[4] = (timestamp >> 8) & 0xFF;
    value[5] = timestamp & 0xFF;

    // version and variant
    value[6] = (value[6] & 0x0F) | 0x70;
    value[8] = (value[8] & 0x3F) | 0x80;

    return EXIT_SUCCESS;
}

int main() {
    uint8_t uuid_val[16];
    uuidv7(uuid_val);
    for (size_t i = 0; i < 16; i++) {
        printf("%02x", uuid_val[i]);
    }
    printf("\n");
}

PHP
使用 random_bytes() 初始化隨機字串,使用 microtime() 獲取當前時間戳,從時間戳中填充字元,設定版本和變數。

<?php
function uuidv7() {
    // random bytes
    $value = random_bytes(16);

    // current timestamp in ms
    $timestamp = intval(microtime(true) * 1000);

    // timestamp
    $value[0] = chr(($timestamp >> 40) & 0xFF);
    $value[1] = chr(($timestamp >> 32) & 0xFF);
    $value[2] = chr(($timestamp >> 24) & 0xFF);
    $value[3] = chr(($timestamp >> 16) & 0xFF);
    $value[4] = chr(($timestamp >> 8) & 0xFF);
    $value[5] = chr($timestamp & 0xFF);

    // version and variant
    $value[6] = chr((ord($value[6]) & 0x0F) | 0x70);
    $value[8] = chr((ord($value[8]) & 0x3F) | 0x80);

    return $value;
}

$uuid_val = uuidv7();
echo bin2hex($uuid_val);


Go
使用 rand.Read() 初始化隨機陣列,使用 time.Now() 獲取當前時間戳,根據時間戳填充陣列,設定版本和變數。

package main

import (
    "crypto/rand"
    "fmt"
    "time"
)

func uuidv7() ([16]byte, error) {
    // random bytes
    var value [16]byte
    _, err := rand.Read(value[:])
    if err != nil {
        return value, err
    }

    // current timestamp in ms
    timestamp := uint64(time.Now().UnixNano() / int64(time.Millisecond))

    // timestamp
    value[0] = byte((timestamp >> 40) & 0xFF)
    value[1] = byte((timestamp >> 32) & 0xFF)
    value[2] = byte((timestamp >> 24) & 0xFF)
    value[3] = byte((timestamp >> 16) & 0xFF)
    value[4] = byte((timestamp >> 8) & 0xFF)
    value[5] = byte(timestamp & 0xFF)

    // version and variant
    value[6] = (value[6] & 0x0F) | 0x70
    value[8] = (value[8] & 0x3F) | 0x80

    return value, nil
}

func main() {
    uuidVal, err := uuidv7()
    if err != nil {
        panic(err)
    }
    for _, byte := range uuidVal {
        fmt.Printf("%02x", byte)
    }
    fmt.Println()
}

Rust
使用 Rng.fill() 初始化隨機陣列,使用 SystemTime::now() 獲取當前時間戳,根據時間戳填充陣列,設定版本和變數。

use rand::Rng;
use std::time::{SystemTime, UNIX_EPOCH};
use std::error::Error;

fn uuidv7() -> Result<[u8; 16], Box<dyn Error>> {
    // random bytes
    let mut value = [0u8; 16];
    rand::thread_rng().fill(&mut value);

    // current timestamp in ms
    let timestamp = match SystemTime::now().duration_since(UNIX_EPOCH) {
        Ok(duration) => duration.as_millis() as u64,
        Err(_) => return Err(Box::from("Failed to get system time")),
    };

    // timestamp
    value[0] = (timestamp >> 40) as u8;
    value[1] = (timestamp >> 32) as u8;
    value[2] = (timestamp >> 24) as u8;
    value[3] = (timestamp >> 16) as u8;
    value[4] = (timestamp >> 8) as u8;
    value[5] = timestamp as u8;

    // version and variant
    value[6] = (value[6] & 0x0F) | 0x70;
    value[8] = (value[8] & 0x3F) | 0x80;

    Ok(value)
}

fn main() {
    match uuidv7() {
        Ok(uuid_val) => {
            for byte in &uuid_val {
                print!("{:02x}", byte);
            }
            println!();
        },
        Err(e) => eprintln!("Error: {}", e),
    }
}

Kotlin
使用 SecureRandom.nextBytes() 初始化隨機陣列,使用 Instant.now() 獲取當前時間戳,根據時間戳填充陣列,設定版本和變數。

import java.security.SecureRandom
import java.time.Instant

object UUIDv7 {
    private val random = SecureRandom()

    fun generate(): ByteArray {
        // random bytes
        val value = ByteArray(16)
        random.nextBytes(value)

        // current timestamp in ms
        val timestamp = Instant.now().toEpochMilli()

        // timestamp
        value[0] = ((timestamp shr 40) and 0xFF).toByte()
        value[1] = ((timestamp shr 32) and 0xFF).toByte()
        value[2] = ((timestamp shr 24) and 0xFF).toByte()
        value[3] = ((timestamp shr 16) and 0xFF).toByte()
        value[4] = ((timestamp shr 8) and 0xFF).toByte()
        value[5] = (timestamp and 0xFF).toByte()

        // version and variant
        value[6] = (value[6].toInt() and 0x0F or 0x70).toByte()
        value[8] = (value[8].toInt() and 0x3F or 0x80).toByte()

        return value
    }

    @JvmStatic
    fun main(args: Array<String>) {
        val uuidVal = generate()
        uuidVal.forEach { b -> print("%02x".format(b)) }
        println()
    }
}

Ruby
使用 SecureRandom.random_bytes() 初始化隨機陣列,使用 Time.now 獲取當前時間戳,根據時間戳填充陣列,設定版本和變數。

require 'securerandom'
require 'time'

def uuidv7
  # random bytes
  value = SecureRandom.random_bytes(16).bytes

# current timestamp in ms
  timestamp = (Time.now.to_f * 1000).to_i

  # timestamp
  value[0] = (timestamp >> 40) & 0xFF
  value[1] = (timestamp >> 32) & 0xFF
  value[2] = (timestamp >> 24) & 0xFF
  value[3] = (timestamp >> 16) & 0xFF
  value[4] = (timestamp >> 8) & 0xFF
  value[5] = timestamp & 0xFF

  # version and variant
  value[6] = (value[6] & 0x0F) | 0x70
  value[8] = (value[8] & 0x3F) | 0x80

  value
end

if __FILE__ == $0
  uuid_val = uuidv7
  puts uuid_val.pack('C*').unpack1('H*')
end

Lua
使用 math.random() 初始化隨機表,使用 os.time() 獲取當前時間戳,根據時間戳填充列表,設定版本和變數。

local function uuidv7()
    -- random bytes
    local value = {}
    for i = 1, 16 do
        value[i] = math.random(0, 255)
    end

    -- current timestamp in ms
    local timestamp = os.time() * 1000

    -- timestamp
    value[1] = (timestamp >> 40) & 0xFF
    value[2] = (timestamp >> 32) & 0xFF
    value[3] = (timestamp >> 24) & 0xFF
    value[4] = (timestamp >> 16) & 0xFF
    value[5] = (timestamp >> 8) & 0xFF
    value[6] = timestamp & 0xFF

    -- version and variant
    value[7] = (value[7] & 0x0F) | 0x70
    value[9] = (value[9] & 0x3F) | 0x80

    return value
end

local uuid_val = uuidv7()
for i = 1, uuid_val do
    io.write(string.format('%02x', uuid_val[i]))
end
print()


Swift
使用 UInt8.random() 初始化隨機陣列,使用 Date() .timeIntervalSince1970 獲取當前時間戳,根據時間戳填充陣列,設定版本和變數。

import Foundation

func uuidv7() -> [UInt8] {
    // random bytes
    var value = (0..<16).map { _ in UInt8.random(in: 0...255) }

    // current timestamp in ms
    let timestamp = Int(Date().timeIntervalSince1970 * 1000)

    // timestamp
    value[0] = UInt8((timestamp >> 40) & 0xFF)
    value[1] = UInt8((timestamp >> 32) & 0xFF)
    value[2] = UInt8((timestamp >> 24) & 0xFF)
    value[3] = UInt8((timestamp >> 16) & 0xFF)
    value[4] = UInt8((timestamp >> 8) & 0xFF)
    value[5] = UInt8(timestamp & 0xFF)

    // version and variant
    value[6] = (value[6] & 0x0F) | 0x70
    value[8] = (value[8] & 0x3F) | 0x80

    return value
}

let uuidVal = uuidv7()
print(uuidVal.map { String(format: "%02x", $0) }.joined())

 

相關文章