Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.
01Bagaimana cara membuat ID unik di distributed system?
Hilangkan koordinasi atau bagi biayanya. UUIDv7 dan ULID tidak butuh koordinasi karena 74 atau 80 bit acak membuat tabrakan dapat diabaikan, Snowflake butuh worker ID unik per generator, dan block allocation memesan rentang ID dari counter pusat dalam satu round trip. Pilih berdasarkan ukuran key, urutan sort, dan koordinasi yang sanggup Anda tanggung.
02Apa beda Snowflake dan UUIDv7?
Snowflake ID adalah integer 64-bit dengan 41 bit timestamp, 10 bit machine ID, dan 12 bit sequence, sehingga butuh worker ID unik per generator. UUIDv7 adalah nilai 128-bit dengan timestamp milidetik 48-bit dan 74 bit yang bisa acak, jadi tanpa koordinasi tetapi 8 byte lebih besar per key. Keduanya terurut kira-kira menurut waktu pembuatan.
03Berapa ID yang bisa dihasilkan generator Snowflake per detik?
Sequence 12-bit mengizinkan 2^12 = 4.096 ID per milidetik per worker, yaitu 4.096.000 per detik. Dengan 10 bit worker ada 1.024 worker, sehingga seluruh layout maksimal 4.194.304.000 ID per detik. Timestamp 41-bit bertahan 2^41 milidetik, sekitar 69,7 tahun sejak epoch yang Anda pilih.
04PostgreSQL versi berapa yang mendukung UUIDv7?
Dokumentasi PostgreSQL 18 mencantumkan fungsi bawaan uuidv7() dengan argumen interval opsional untuk menggeser timestamp. Halaman dokumentasi versi 17 tidak mencantumkannya, jadi pada versi lama Anda membuat UUIDv7 di aplikasi dan mengirimnya lewat INSERT. Dokumentasi menyebut hasilnya time-ordered tetapi tidak menjanjikan monotonicity yang ketat.
05Apa yang terjadi pada generator Snowflake jika jam mundur?
Generator bisa menerbitkan ulang milidetik yang sudah dipakai sehingga menghasilkan ID duplikat dengan worker dan sequence yang sama. Generator yang aman menunggu langkah mundur kecil dan melempar error untuk yang besar, bukan memercayai jam. RFC 9562 juga menyatakan implementasi harus memutuskan cara menangani jam yang mundur.
Distributed ID Generator: Snowflake vs UUIDv7 vs ULID
Cara membuat ID unik di banyak server: batas auto-increment, dampak UUIDv4 pada index, UUIDv7 di RFC 9562, layout Snowflake 41/10/12, clock rollback, dan block allocation.
Pakai UUIDv7 untuk ID 128-bit yang terurut waktu tanpa koordinasi, atau Snowflake ID bila butuh key 64-bit yang ringkas. Snowflake memuat 41 bit timestamp, 10 bit worker ID, dan 12 bit sequence, sehingga satu worker bisa menerbitkan 4.096 ID per milidetik. UUIDv4 memecah index B-tree, dan auto-increment butuh satu writer pusat.
Skema yang dimulai dengan bigserial berjalan mulus sampai hari writer kedua muncul. Bayangkan sistem ERP atau POS di mana aplikasi mobile lapangan harus membuat record saat offline, atau node database kedua ikut menerima write: sekarang dua mesin sama-sama ingin memberi nomor berikutnya, dan counter yang hanya hidup di satu tempat tidak lagi menjadi jawaban.
Post ini menjawab pertanyaan yang benar-benar dicari orang: bagaimana membuat ID unik di distributed system? Kita bandingkan auto-increment, UUIDv4, UUIDv7, ULID, dan Snowflake, menurunkan kapasitas tiap layout dengan hitungan yang ditunjukkan, dan menyertakan generator Snowflake TypeScript yang bisa dijalankan. Setiap angka spesifikasi berasal dari RFC 9562, dokumentasi PostgreSQL, spec ULID, atau Wikipedia, dengan tautan di akhir.
Kenapa tidak pakai auto-increment saja?
Auto-increment adalah default yang tepat untuk satu database, dan key-nya paling kecil dan paling cepat. Tetapi ia tidak cukup karena tiga alasan. Sequence adalah satu counter di satu tempat, jadi dua writer independen harus berbagi counter itu atau diberi rentang terpisah. Counter 32-bit habis lebih cepat dari perkiraan orang. Dan nilai berurutan membocorkan jumlah baris: invoice bernomor 48.213 memberi tahu kompetitor kira-kira berapa banyak yang sudah Anda terbitkan.
Dokumentasi PostgreSQL menyebut batas atas integer 2147483647, sedangkan bigint mencapai 9223372036854775807. Contoh hitungan di bawah menunjukkan kenapa selisih itu penting pada tabel yang sibuk. Bigint menyelesaikan masalah kehabisan angka, tetapi bukan masalah single counter, yang justru diciptakan oleh sistem terdistribusi.
# How long does a 32-bit auto-increment last?
integer max = 2,147,483,647
insert rate = 1,000 rows/s
seconds until overflow = 2,147,483,647 / 1,000 = 2,147,483 s
days until overflow = 2,147,483 / 86,400 = about 24.9 days
# bigint max = 9,223,372,036,854,775,807
# At the same rate that is about 9.2e15 s, so exhaustion stops being the problem.
# The single shared counter does not go away, though.
Kenapa UUIDv4 merusak performa index database?
UUIDv4 menyelesaikan koordinasi sepenuhnya: mesin mana pun bisa membuatnya tanpa menghubungi mesin lain. Harganya dibayar di index. RFC 9562 menjelaskan bahwa versi yang tidak terurut waktu seperti UUIDv4 menyebabkan insert acak dan locality B-tree yang buruk, sehingga tiap baris baru mendarat di leaf page sembarang, bukan yang paling kanan. Working set index menjadi seluruh index, bukan hanya tepi yang aktif.
Key-nya juga dua kali lebih lebar. Sebagai nilai mentah, 100 juta baris memakan 100.000.000 x 8 byte = 800 MB data key untuk bigint dan 100.000.000 x 16 byte = 1,6 GB untuk UUID, belum termasuk overhead page, dan setiap secondary index mengulang primary key. Itu hitungan lebar key, bukan benchmark; RFC menyebut selisih kecepatan antara insert acak dan insert terurut waktu bisa satu orde besaran atau lebih, dan itulah angka yang layak dikutip.
Simpan UUID di kolom uuid native, bukan string 36 karakter. RFC 9562 merekomendasikan bentuk biner 128-bit bila memungkinkan, dan bentuk string membuat ukuran key lebih dari dua kali lipat.
Apa itu UUIDv7 dan apa yang diubah RFC 9562?
RFC 9562, terbit Mei 2024, menggantikan RFC 4122 dan mendefinisikan UUIDv7 di bagian 5.7. 48 bit pertama adalah timestamp Unix big-endian dalam milidetik, diikuti 4 bit versi, 12 bit rand_a, 2 bit variant, dan 62 bit rand_b. Tersisa 12 + 62 = 74 bit yang bisa acak, atau berisi pecahan sub-milidetik atau counter. Karena timestamp berada di depan, nilai UUIDv7 terurut sesuai waktu pembuatan sebagai byte biasa, dan itulah yang mengembalikan locality B-tree.
RFC menyatakan bahwa satu node cukup memastikan timestamp maju sebelum setiap UUID baru, dan menjelaskan counter untuk batch generation. Jadi monotonicity yang ketat dalam satu milidetik adalah pilihan implementasi, bukan janji format. PostgreSQL 18 menyertakan fungsi bawaan uuidv7(): ia muncul di dokumentasi versi 18 dan tidak ada di halaman versi 17. Dokumentasinya menyebut hasilnya time-ordered dan tidak menyinggung monotonicity, jadi jangan membangun di atas jaminan yang lebih kuat dari itu.
-- UUIDv7 layout, RFC 9562 section 5.7 (128 bits)
-- 48 bits unix_ts_ms milliseconds since 1970, big-endian
-- 4 bits ver 0111
-- 12 bits rand_a random, or sub-ms fraction, or counter
-- 2 bits var 10
-- 62 bits rand_b random
-- random-capable bits = 12 + 62 = 74
-- PostgreSQL 18 or later: built in
CREATE TABLE invoices (
id uuid PRIMARY KEY DEFAULT uuidv7(),
total_idr bigint NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
SELECT uuidv7(); -- time-ordered uuid
SELECT uuidv7(interval '-1 hour'); -- optional shift argument
-- PostgreSQL 17 and earlier: no uuidv7(); generate it in the application
-- and pass the value in the INSERT, keeping the native uuid column type.
ULID adalah saudara yang lebih tua. Ia memakai timestamp 48-bit milidetik dan 80 bit acak, ditulis sebagai string Crockford base32 sepanjang 26 karakter, dan spec-nya menjelaskan mode monotonic yang menaikkan bagian acak dalam milidetik yang sama dan gagal bila bagian itu overflow. Pilih ULID bila butuh string pendek yang terurut di URL; pilih UUIDv7 bila butuh kolom uuid native dan tooling standar.
Bagaimana Snowflake ID bekerja dan berapa banyak ID yang bisa dibuat?
Snowflake ID adalah integer 64-bit: satu sign bit yang selalu 0, 41 bit milidetik sejak epoch pilihan, 10 bit machine ID, dan 12 bit sequence per mesin. Library asli Twitter membagi 10 bit itu menjadi 5 bit data centre dan 5 bit worker, tetapi layout ini hanya konvensi yang boleh Anda ubah selama semua generator sepakat. Artikel Wikipedia mencatat epoch aslinya 1288834974657, dan tidak ada yang melarang Anda memakai epoch sendiri.
Kapasitasnya mengikuti lebar field. Blok kode menunjukkan tiga angka yang perlu diketahui: berapa lama timestamp bertahan, secepat apa satu worker, dan secepat apa seluruh fleet. Memilih epoch kustom yang baru itu gratis dan mengembalikan tahun-tahun yang akan terbuang bila menghitung dari 1970.
# Snowflake: 1 + 41 + 10 + 12 = 64 bits
sign 1 bit always 0, so the value fits a signed bigint
timestamp 41 bits ms since your epoch
worker 10 bits 2^10 = 1,024 generators
sequence 12 bits 2^12 = 4,096 IDs per ms per generator
# Lifetime of the timestamp field
2^41 ms = 2,199,023,255,552 ms
/ 31,557,600,000 ms per year (365.25 days)
= about 69.7 years from the epoch
# Throughput
per worker : 4,096 per ms x 1,000 = 4,096,000 IDs/s
whole fleet: 4,096,000 x 1,024 = 4,194,304,000 IDs/s
# Largest possible value is 2^63 - 1 = 9,223,372,036,854,775,807.
# A JavaScript Number is exact only to 2^53 - 1 = 9,007,199,254,740,991.
Bagian tersulit bukan bit shifting, melainkan worker ID. Dua generator dengan worker ID yang sama pada akhirnya akan bertabrakan. Pada satu VPS dengan beberapa container Docker, satu environment variable WORKER_ID per container sudah cukup; untuk fleet Anda butuh registry seperti baris database, atau lease yang kedaluwarsa, yang memberi tiap proses nomor yang tidak dipegang proses lain.
Bagaimana menangani clock skew dan clock rollback?
Generator Snowflake memercayai jam, jadi jam adalah titik kegagalannya. Bila NTP memundurkan waktu, generator bisa menerbitkan ulang milidetik yang sudah dipakai, dengan worker dan sequence yang sama, dan itu adalah duplicate key. RFC 9562 terus terang bahwa implementasi harus memutuskan apa yang dilakukan saat jam sistem mundur, dan menyebut memakai ulang timestamp sebelumnya lalu menaikkan counter, atau melaporkan error, sebagai pilihan yang masuk akal.
const EPOCH = 1767225600000n; // 2026-01-01T00:00:00Z, our own epoch
const WORKER_BITS = 10n;
const SEQ_BITS = 12n;
const MAX_WORKER = (1n << WORKER_BITS) - 1n; // 1023
const SEQ_MASK = (1n << SEQ_BITS) - 1n; // 4095
const MAX_BACKWARD_MS = 5n; // tolerate small NTP steps, refuse anything bigger
export class Snowflake {
private lastMs = -1n;
private seq = 0n;
private readonly workerId: bigint;
private readonly now: () => bigint;
constructor(workerId: bigint, now: () => bigint = () => BigInt(Date.now())) {
if (workerId < 0n || workerId > MAX_WORKER) {
throw new RangeError("workerId must be 0..1023");
}
this.workerId = workerId;
this.now = now; // injectable so the rollback path is testable
}
next(): bigint {
let ms = this.now();
if (ms < this.lastMs) {
const behind = this.lastMs - ms;
// Wrong: issue IDs anyway - they can collide with ones already handed out.
// Right: wait out a small step, fail loudly on a large one.
if (behind > MAX_BACKWARD_MS) throw new Error("clock moved back " + behind + " ms");
while (ms < this.lastMs) ms = this.now();
}
if (ms === this.lastMs) {
this.seq = (this.seq + 1n) & SEQ_MASK;
if (this.seq === 0n) {
// 4096 IDs used in this millisecond: spin until the next one.
while (ms <= this.lastMs) ms = this.now();
}
} else {
this.seq = 0n;
}
this.lastMs = ms;
return ((ms - EPOCH) << (WORKER_BITS + SEQ_BITS)) | (this.workerId << SEQ_BITS) | this.seq;
}
}
// Trace with a fake clock: worker 7, one second after the epoch.
// first ID = (1000 << 22) | (7 << 12) | 0 = 4194304000 + 28672 = 4194332672
// second ID = 4194332673 (same ms, sequence 1)
// Send these to browsers as strings: JSON.stringify cannot serialise a BigInt.
const gen = new Snowflake(7n, () => EPOCH + 1000n);
console.log(gen.next().toString(), gen.next().toString());
Generator di bawah membuat keputusan itu eksplisit. Langkah mundur kecil hingga 5 ms ditunggu; yang lebih besar melempar error, karena menerbitkan ID diam-diam dari jam yang tidak bisa dipercaya lebih buruk daripada request yang gagal. Saat 4.096 nilai sequence habis dalam satu milidetik, generator berputar menunggu milidetik berikutnya, sehingga satu worker dibatasi 4.096 ID per milidetik dan tidak pernah mengulang satu pun.
Simpan juga timestamp terakhir yang diterbitkan di suatu tempat bila restart bisa jatuh ke masa lalu. Proses yang restart setelah koreksi jam mulai dengan lastMs di -1 dan tidak ingat ID yang baru saja diterbitkannya.
Apa itu ticket server dan block allocation?
Bila Anda ingin integer kecil yang benar-benar naik tanpa urusan worker ID ala Snowflake, pusatkan counter-nya tetapi bagi biayanya. Ticket server adalah baris database yang membagikan nomor; block allocation memperbaikinya dengan membagikan satu rentang sekaligus, sehingga tiap instance aplikasi memesan, misalnya, 1000 ID dalam satu round trip lalu menerbitkannya dari memori.
-- One row per ID space
CREATE TABLE id_blocks (
name text PRIMARY KEY,
next_id bigint NOT NULL
);
INSERT INTO id_blocks VALUES ('invoice', 1);
-- Reserve 1000 IDs in ONE atomic statement; concurrent callers serialise on the row.
UPDATE id_blocks
SET next_id = next_id + 1000
WHERE name = 'invoice'
RETURNING next_id - 1000 AS block_start; -- caller owns block_start .. block_start + 999
-- First call returns 1 (owns 1..1000), second returns 1001 (owns 1001..2000).
-- The application hands these out from memory and reserves a new block when it runs dry.
Hitungannya adalah argumennya. Pada 1.000.000 insert per hari, meminta satu ID setiap kali berarti 1.000.000 round trip; memesan blok 1000 berarti 1.000.000 / 1000 = 1.000. Harganya dibayar di dua tempat: instance yang crash membuang hingga 999 ID yang belum terpakai, yang tidak masalah karena gap itu wajar, dan ID dari instance berbeda saling berselang-seling, jadi key-nya kira-kira terurut, bukan terurut sempurna.
Kelemahan yang tersisa adalah allocator itu sendiri, single point of failure di jalur write. Jalankan di Postgres yang sudah Anda operasikan dan jaga agar reservasi berupa satu statement atomik, maka desainnya sederhana, membosankan, dan layak dipertahankan untuk sistem yang masih satu database.
Skema ID mana yang sebaiknya dipilih?
Putuskan berdasarkan tiga hal: apakah Anda butuh key 64-bit, apakah ada koordinasi yang bisa ditoleransi, dan seberapa penting urutan sort. Tabel merangkum skema yang dibahas di atas hanya dengan properti yang diturunkan atau dikutip di post ini.
Skema
Ukuran key
Terurut waktu pembuatan
Butuh koordinasi
Risiko utama
Auto-increment bigint
8 byte
Ya, ketat
Satu counter bersama
Satu writer, membocorkan jumlah baris
UUIDv4
16 byte
Tidak
Tidak ada
Insert acak, locality B-tree buruk
UUIDv7
16 byte
Ya, per milidetik
Tidak ada
Urutan dalam satu milidetik tergantung implementasi
ULID
16 byte, 26 karakter sebagai teks
Ya, mode monotonic tersedia
Tidak ada
Bukan tipe uuid native, tooling database lebih sedikit
Snowflake
8 byte
Ya, per milidetik
Worker ID unik per generator
Clock rollback, worker ID ganda
Untuk kebanyakan aplikasi berbasis Postgres di satu VPS, sequence bigint masih benar dan UUIDv7 adalah jalur upgrade ketika ID harus ada sebelum baris sampai ke database. Pilih Snowflake hanya bila 64 bit penting, misalnya karena ID melewati klien JavaScript, di mana Number biasa hanya presisi hingga 9007199254740991 dan ID 63-bit harus dikirim sebagai string.
Butuh ID yang dibuat offline atau oleh banyak writer tanpa koordinasi: pilih UUIDv7, dan pakai kolom uuid native.
Butuh key 64-bit yang ringkas dan bisa memberi tiap generator worker ID unik: pilih Snowflake, dengan kebijakan rollback yang eksplisit.
Masih satu database dan satu writer: pertahankan sequence bigint, dan tambahkan block allocation hanya bila round trip menjadi biaya.
Jangan pilih UUIDv4 sebagai primary key di tabel besar yang sering ditulis bila ada alternatif yang terurut waktu.
Aturan yang saya bawa dari sini: koordinasi adalah biaya yang sedang Anda pilih. Sequence membayarnya di setiap insert, block allocation sekali per blok, Snowflake sekali per proses lewat worker ID, dan UUIDv7 tidak membayar sama sekali tetapi mengorbankan 8 byte lebar key. Pilih skema yang biayanya sanggup Anda tanggung, dan tulis kebijakan clock rollback sebelum duplikat pertama mengajarkannya.