Ngừng Dùng UUIDv4 Làm Khóa Chính DB: Hãy Chuyển Sang UUIDv7

Ngừng Dùng UUIDv4 Làm Khóa Chính DB: Hãy Chuyển Sang UUIDv7

Khóa chính UUIDv4 gây phân mảnh B-Tree index và làm chậm tốc độ ghi database. Khám phá lý do UUIDv7 và ULID là giải pháp thay thế hoàn hảo cho hệ thống.

Có bao giờ bạn thiết kế một bảng cơ sở dữ liệu (database) mới và gõ ngay id: uuidv4() một cách vô thức chưa? Mình cá là rất nhiều anh em developer từng làm vậy. Trong suốt nhiều năm qua, UUID version 4 (UUIDv4) đã trở thành "tiêu chuẩn vàng" không thể tranh cãi cho các hệ thống phân tán. Nó đảm bảo tính duy nhất trên toàn cầu, che giấu số lượng bản ghi thực tế của hệ thống (điều mà ID tự tăng không làm được), và cho phép client tự tạo ID trước khi gửi request lên server.

Nhưng có một sự thật phũ phàng: nếu bạn đang dùng UUIDv4 làm khóa chính (Primary Key) trong các cơ sở dữ liệu quan hệ (Relational Database) như PostgreSQL hay MySQL, bạn có thể đang âm thầm "bóp nghẹt" hiệu năng ghi dữ liệu (write performance) của hệ thống.

Hôm nay, hãy cùng mổ xẻ lý do tại sao UUIDv4 lại là "kẻ hủy diệt" hiệu suất âm thầm, và tại sao các kỹ sư phần mềm đang rủ nhau chuyển sang các giải pháp thay thế có tính tuần tự thời gian như UUIDv7 và ULID.

Nỗi niềm giữa Auto-Increment và UUID

Về mặt lịch sử, cách phổ biến nhất để định danh một dòng dữ liệu là dùng số nguyên tự tăng (SERIAL trong Postgres hoặc AUTO_INCREMENT trong MySQL). Chúng cực kỳ nhanh và tối ưu. Vì các số này tăng dần đều (1, 2, 3...), chúng rất thân thiện với cơ chế lưu trữ của database.

Tuy nhiên, ID kiểu số nguyên mang lại nhiều rủi ro trong các ứng dụng hiện đại. Nếu ID của bạn là 500, một hacker có thể dễ dàng đoán rằng user 501 có tồn tại và thực hiện tấn công IDOR. Hơn nữa, đối thủ cạnh tranh có thể biết chính xác bạn có bao nhiêu khách hàng chỉ bằng cách tạo một tài khoản hôm nay và một tài khoản vào tuần sau. Cuối cùng, trong kiến trúc Microservices, việc đảm bảo tạo số tự tăng tuần tự trên nhiều database độc lập mà không bị trùng lặp là một cơn ác mộng thực sự.

Đó là lúc UUIDv4 xuất hiện. Một chuỗi 128-bit ngẫu nhiên hoàn toàn. Nó giải quyết triệt để bài toán bảo mật và phân tán. Nhưng bù lại, nó sinh ra một khiếm khuyết kiến trúc chết người ở tầng lưu trữ.

Cơn ác mộng phân mảnh B-Tree (Index Fragmentation)

Giá sách lộn xộn tượng trưng cho sự phân mảnh B-Tree index

Để hiểu vấn đề của UUIDv4, chúng ta cần nói về B-Tree (Balanced Tree). Hầu hết các database quan hệ sử dụng cấu trúc B-Tree để lưu trữ index. Trong MySQL (engine InnoDB), khóa chính (Primary Key) chính là clustered index, nghĩa là dữ liệu thực tế của một dòng được lưu vật lý cùng với vị trí của index đó trên cây B-Tree.

Khi bạn dùng một khóa chính tuần tự như số nguyên, các bản ghi mới sẽ được "đắp" gọn gàng vào mép ngoài cùng bên phải của cây B-Tree. Database chỉ việc cấp phát các block bộ nhớ (page) mới khi cần. Mọi thứ diễn ra trơn tru với chi phí I/O (đọc/ghi ổ cứng) cực kỳ thấp.

Tuy nhiên, UUIDv4, đúng như tên gọi của nó, là ngẫu nhiên. Khi bạn chèn hàng triệu dòng với khóa chính là UUIDv4, database liên tục phải cân bằng lại cây. Bản ghi đầu tiên có thể nằm ở nhánh trái, bản ghi thứ hai tít bên phải, và bản ghi thứ ba lại chen ngang vào giữa.

Kiểu chèn dữ liệu lộn xộn này dẫn đến một hiện tượng thảm họa gọi là Page Split (Phân trang).

Database lưu dữ liệu trong các khối có kích thước cố định gọi là page (thường là 8KB hoặc 16KB). Khi một page đã đầy mà có một UUID ngẫu nhiên cần chèn vào giữa, database buộc phải "xé" page đó ra làm hai, sao chép một nửa dữ liệu sang page mới để lấy chỗ trống.

Quá trình này gây ra hiện tượng Write Amplification (Khuếch đại thao tác ghi). Database của bạn phải làm việc với ổ cứng nhiều hơn mức cần thiết rất nhiều. Qua thời gian, các page split này tạo ra "Index Bloat"—những block bộ nhớ chỉ chứa một nửa dữ liệu, làm lãng phí trầm trọng RAM và dung lượng đĩa cứng. Khi bảng dữ liệu lớn đến mức index không thể nhét vừa vào bộ nhớ RAM nữa, mọi thao tác insert mới đều ép hệ thống phải đọc từ ổ đĩa vật lý, khiến tốc độ ghi giảm thê thảm.

Giải pháp: Định danh có tính thời gian (Time-Sorted Identifiers)

Bánh răng đồng hồ tượng trưng cho tính tuần tự dựa trên thời gian của UUIDv7

Thứ chúng ta thực sự cần là một ID kết hợp được ưu điểm của cả hai thế giới: tính ngẫu nhiên, khó đoán của UUID và tính tuần tự, thân thiện với database của số nguyên tự tăng.

Đó là lý do Time-Sorted Identifiers ra đời.

Bằng cách mã hóa timestamp (dấu thời gian) vào ngay phần đầu của ID và lấp đầy phần còn lại bằng dữ liệu ngẫu nhiên, chúng ta có một ID hoàn toàn duy nhất trên toàn cầu nhưng lại có khả năng tự động sắp xếp theo thứ tự thời gian.

UUIDv7: Tiêu chuẩn chính thức mới

Vừa được chính thức hóa trong tài liệu RFC 9562, UUID version 7 chính là thứ mà ngành phần mềm đã chờ đợi từ lâu. Nó được thiết kế đặc biệt để khắc phục điểm yếu của v4.

Cấu trúc của một UUIDv7 (128-bit) vô cùng đơn giản:

  • 48 bit đầu: Thời gian Unix tính bằng mili-giây.
  • 74 bit tiếp theo: Dữ liệu ngẫu nhiên an toàn (cryptographically secure).
  • 6 bit: Các cờ đánh dấu version.

Vì các bit quan trọng nhất đại diện cho thời gian tạo, các UUIDv7 sinh ra trong cùng một mili-giây sẽ ngẫu nhiên, nhưng qua các mili-giây khác nhau, chúng sẽ tuân thủ nghiêm ngặt tính tuần tự. Khi được đưa vào database, chúng tự động nối vào phía bên phải của cây B-Tree một cách ngoan ngoãn. Tình trạng phân mảnh page (page split) giảm đáng kể, và tốc độ insert được duy trì ổn định ngay cả với big data.

Lợi thế "ăn tiền" nhất của UUIDv7 là tính tương thích ngược. Nó có định dạng 128-bit y hệt UUIDv4 (ví dụ: 018e6b12-9c9c-7000-a123-456789abcdef). Điều này có nghĩa là bạn có thể dùng thẳng nó vào các cột uuid hiện có trong PostgreSQL mà không cần phải migrate hay đổi schema!

Đây là cách bạn có thể áp dụng nó trong Node.js chỉ với một nốt nhạc:

import { v7 as uuidv7 } from 'uuid';

const createNewUser = async (email: string) => {
  const user = {
    id: uuidv7(), // Sinh ra UUID tuần tự theo thời gian
    email,
    createdAt: new Date()
  };
  
  await db.users.insert(user);
  return user;
};

ULID: Kẻ thay thế hoàn hảo cho URL

Trước khi UUIDv7 được chuẩn hóa, ULID (Universally Unique Lexicographically Sortable Identifier) từng là ngôi sao sáng trong việc xử lý time-sorted ID.

Giống như UUIDv7, một ULID cũng là sự kết hợp giữa 48-bit thời gian và 80-bit ngẫu nhiên. Điểm khác biệt là ULID được mã hóa bằng bảng mã Base32 đặc chế.

Một ULID thực tế trông sẽ như thế này: 01ARZ3NDEKTSV4RRFFQ69G5FAV.

Ưu điểm của ULID:

  • Ngắn gọn hơn (chỉ 26 ký tự so với 36 ký tự có dấu gạch ngang của UUID).
  • Cực kỳ thân thiện với URL. Không có dấu gạch ngang, user có thể nhấp đúp chuột để bôi đen và copy toàn bộ rất dễ dàng.
  • Không phân biệt chữ hoa chữ thường.

Nhược điểm của ULID:

  • Các database quan hệ thường không có sẵn kiểu dữ liệu ulid nguyên bản. Bạn sẽ phải lưu chúng dưới dạng VARCHAR(26) hoặc CHAR(26), tiêu tốn nhiều bộ nhớ hơn so với kiểu uuid (16 byte) cực kỳ nhỏ gọn được tối ưu hóa trong Postgres.

Lời kết

Nếu bạn đang khởi tạo một dự án mới hoặc tạo thêm bảng mới, lời khuyên của mình là: Hãy ngừng ngay việc dùng UUIDv4.

  1. Nếu database của bạn hỗ trợ kiểu UUID nguyên bản (như PostgreSQL): Hãy dùng ngay UUIDv7. Nó là một tiêu chuẩn quốc tế chính thức, không yêu cầu thay đổi cấu trúc bảng, và giải quyết triệt để cơn ác mộng phân mảnh B-Tree.
  2. Nếu bạn đề cao tính thẩm mỹ và cần đưa ID lên URL: ULID vẫn là một lựa chọn tuyệt vời, đặc biệt nếu bạn dùng NoSQL hoặc chấp nhận việc lưu ID dưới định dạng chuỗi (string).
  3. Nếu hệ thống của bạn đang có sẵn hàng triệu dòng UUIDv4: Đừng hoảng sợ, bạn không cần phải cắm đầu viết script migrate (điều đó còn nguy hiểm hơn). Vì v4 và v7 có cùng chung định dạng, bạn chỉ cần sửa code ở tầng application để tạo UUIDv7 cho các bản ghi mới. Database của bạn sẽ vui vẻ chứa cả hai loại, và các truy vấn insert mới sẽ lập tức được tối ưu.

Đã đến lúc giải phóng database của bạn khỏi những gánh nặng không đáng có. Hãy nâng cấp lên các thế hệ ID mới, hệ thống của bạn (và cả thẻ tín dụng trả tiền server) sẽ cảm ơn bạn rất nhiều!

NT

viết bởi

Nguyên Tech

0

Phản hồi

Đang tải bình luận…

Lattice.

Một không gian để viết dài, đọc chậm, và trò chuyện thật — không thuật toán, không quảng cáo.

© 2026 · Lattice · Đà Nẵng (16°03′ N, 108°12′ E) · v0.1 · system + ink + indigo