Mục lục bài viết

Hóa đơn MoMo khi đặt xe Green SM có hai chuỗi trông rất “vô nghĩa”. Thật ra đó là UUID và ULID, và cả hai đều cho biết lúc hệ thống tạo đơn, gần như trùng giờ bạn thanh toán. Chỉ vài dòng Python là giải mã được.

Mục tiêu bài học:

  • Nhận diện được một chuỗi là UUID hay ULID chỉ bằng mắt.
  • Giải mã được thời điểm tạo ID bằng Python.
  • Chọn được loại ID phù hợp cho dự án của mình.

1. UUID và ULID trên hóa đơn MoMo

Đây là phần “Thông tin đơn hàng” của một chuyến Green SM mình thanh toán qua MoMo:

UUID và ULID trên hóa đơn MoMo của Green SM
Mã đơn hàng và mô tả đã được thay bằng mã mẫu cùng cấu trúc
  • Mã đơn hàng: ff0dd6c9-4f31-11f1-ab17-5ea1c3d07f24
  • Mô tả: 01KRJ0NW97G2J53JDWE0F7X90S

Theo tài liệu API của MoMo, orderId và orderInfo đều do đối tác gửi sang. Nên nhiều khả năng hai mã này do Green SM tạo.

2. Mã đơn hàng là UUID phiên bản 1

UUID là số 128 bit, viết thành 32 ký tự hex theo dạng 8-4-4-4-12. Ký tự đầu của nhóm thứ ba cho biết phiên bản: 11f1 → UUID v1, loại tạo từ thời gian.

PhầnGiá trịÝ nghĩa
time_lowff0dd6c932 bit thấp của thời gian
time_mid4f3116 bit giữa của thời gian
version + time_hi1 + 1f1Phiên bản 1 và 12 bit cao của thời gian
variant + clock_seqab172 bit variant + 14 bit bộ đếm chống trùng (clock_seq = 0x2b17 = 11031)
node5ea1c3d07f24Địa chỉ MAC của máy chủ tạo mã, không phải điện thoại của bạn

Thời gian = số khoảng 100 nano giây tính từ 15/10/1582 (ngày bắt đầu lịch Gregory).

3. Mô tả là ULID

ULID cũng 128 bit nhưng chỉ có 26 ký tự, dùng bảng chữ Crockford Base32:

  • 01KRJ0NW97 (10 ký tự đầu): 48 bit thời gian Unix, tính bằng mili giây. Vì vậy ký tự đầu luôn từ 0 đến 7.
  • G2J53JDWE0F7X90S (16 ký tự sau): 80 bit ngẫu nhiên.

Crockford Base32 là cách viết số theo cơ số 32: dùng 0–9 và A–Z nhưng bỏ I, L, O, U để khỏi đọc nhầm với số 1 và 0. Mỗi ký tự là một “chữ số” có giá trị từ 0 đến 31 (A = 10, Z = 31). Thời gian nằm ở đầu, nên sắp ULID theo chữ cái cũng là sắp theo thời gian.

4. Giải mã UUID và ULID bằng Python

Python có sẵn module uuid. Với ULID, chỉ cần một hàm nhỏ đổi 10 ký tự đầu ra mili giây.

import uuid
from datetime import datetime, timedelta, timezone

VN = timezone(timedelta(hours=7))  # Múi giờ Việt Nam

# ----- 1. Giải mã UUID v1 (Mã đơn hàng) -----
ma_don = uuid.UUID("ff0dd6c9-4f31-11f1-ab17-5ea1c3d07f24")
print("Phiên bản UUID:", ma_don.version)

# UUID v1 đếm số khoảng 100 ns kể từ 15/10/1582
moc_1582 = datetime(1582, 10, 15, tzinfo=timezone.utc)
thoi_gian_uuid = moc_1582 + timedelta(microseconds=ma_don.time // 10)
print("Thời điểm UUID:", thoi_gian_uuid.astimezone(VN))
print("Clock sequence:", ma_don.clock_seq)
print("Node:", format(ma_don.node, "012x"))

# ----- 2. Giải mã ULID (Mô tả) -----
BANG_CHU = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"  # Crockford Base32

def ulid_sang_thoi_gian(ulid):
    mili_giay = 0
    for ky_tu in ulid[:10]:  # 10 ký tự đầu = 48 bit thời gian
        mili_giay = mili_giay * 32 + BANG_CHU.index(ky_tu)
    return datetime.fromtimestamp(mili_giay / 1000, tz=VN)

mo_ta = "01KRJ0NW97G2J53JDWE0F7X90S"
thoi_gian_ulid = ulid_sang_thoi_gian(mo_ta)
print("Thời điểm ULID:", thoi_gian_ulid)

# ----- 3. So sánh hai mốc thời gian -----
chenh = (thoi_gian_uuid - thoi_gian_ulid).total_seconds() * 1000
print(f"UUID tạo sau ULID: {chenh:.0f} ms")

Hai chỗ cần để ý:

  • ma_don.time // 10: ma_don.time là số khoảng 100 ns tính từ 15/10/1582. Dấu // là phép chia lấy phần nguyên trong Python (ví dụ 25 // 10 = 2). Vì 1 micro giây = 10 × 100 ns, chia 10 sẽ ra số micro giây, đúng đơn vị mà timedelta nhận.
  • Vòng for trong hàm ULID: mỗi ký tự Base32 mang 5 bit, nên nhân 32 rồi cộng dồn.

Kết quả

Phiên bản UUID: 1
Thời điểm UUID: 2026-05-14 08:12:37.628487+07:00
Clock sequence: 11031
Node: 5ea1c3d07f24
Thời điểm ULID: 2026-05-14 08:12:37.415000+07:00
UUID tạo sau ULID: 213 ms

Và… khớp luôn! Cả hai đều chỉ về 08:12:37 ngày 14/05/2026, ULID tạo trước UUID khoảng 213 ms. Nhiều khả năng ULID là mã chuyến đi nội bộ, còn UUID là mã giao dịch gửi sang MoMo.

5. So sánh UUID và ULID

LoạiĐộ dàiCó thời gian?Sắp xếp theo thời gian?
UUID v136 ký tựCó (100 ns)Không theo chuỗi
UUID v436 ký tựKhông, ngẫu nhiên hoàn toànKhông
UUID v736 ký tựCó (mili giây)Có
ULID26 ký tựCó (mili giây)Có

ID tăng theo thời gian giúp index cơ sở dữ liệu chèn gọn, vì bản ghi mới luôn nằm cuối. UUID v4 thì chèn lung tung khắp index. Vì vậy nhiều hệ thống mới chọn ULID hoặc UUID v7.

Lưu ý: UUID v1, UUID v7 và ULID đều để lộ thời điểm tạo. Không dùng chúng làm token bí mật như mã đặt lại mật khẩu. Trường hợp đó hãy dùng chuỗi ngẫu nhiên, ví dụ module secrets của Python.

6. Bài tập

  1. Mở lịch sử giao dịch MoMo của bạn, tìm một mã đơn hàng rồi xác định nó có phải UUID không, nếu có thì là phiên bản mấy.
  2. Viết hàm ngược lại: nhận một thời điểm và trả về 10 ký tự đầu của ULID.
  3. Dùng uuid.uuid1() tạo một UUID ngay trên máy bạn, giải mã bằng hàm trong bài rồi so với giờ hệ thống. Nếu chưa quen import, xem lại bài Sử dụng module và package trong Python.

Tóm lại, một mã đơn hàng nhìn “vô hại” vẫn có thể tiết lộ giờ giao dịch. Biết cách đọc ID sẽ giúp bạn chọn đúng loại ID khi tự thiết kế hệ thống.

Tác giả

HV Lee

500+ bài viết về lập trình, phần cứng, mạng máy tính và công nghệ. Chia sẻ kiến thức từ những gì đã học, đã dạy và đã trải nghiệm — để người học CNTT hiểu sâu hơn và làm được nhiều hơn.

Gợi ý tiếp theo

Thuật toán sắp xếp đổi chổ trực tiếp (Interchange Sort)

Học tiếp