2 điểm bởi GN⁺ 2023-06-29 | 1 bình luận | Chia sẻ qua WhatsApp
  • TypeID là định dạng định danh toàn cục duy nhất an toàn kiểu, mở rộng từ UUIDv7 và lấy cảm hứng từ cách dùng prefix trong Stripe API
  • Chuỗi chính thức là chuỗi chữ thường, gồm 3 phần: type prefix ASCII snake_case chữ thường dài tối đa 63 ký tự, dấu phân cách _, và hậu tố UUIDv7 dài 26 ký tự được mã hóa bằng base32 đã chỉnh sửa
  • Dùng type prefix giúp ngăn việc vô tình dùng ID user ở nơi cần ID post, đồng thời khi debug có thể nhận ra ngay định danh đang trỏ tới loại thực thể nào
  • TypeID là tập cha của UUID; khi bỏ thông tin kiểu và giải mã, sẽ thu được một UUIDv7 hợp lệ
  • Có thể sắp xếp theo K nên dùng được làm khóa chính trong cơ sở dữ liệu, với mục tiêu giảm vấn đề suy giảm locality của cơ sở dữ liệu so với UUIDv4 là ID toàn cục hoàn toàn ngẫu nhiên
  • Mã hóa base32 có các đặc tính: an toàn với URL, không phân biệt chữ hoa chữ thường, tránh ký tự dễ gây nhầm lẫn, có thể chọn bằng double-click, và ngắn hơn so với mã hóa hex truyền thống của UUID
  • Bản triển khai chính thức được cung cấp cho Go, SQL, TypeScript, và phiên bản spec mới nhất là v0.3.0
  • Có thể dùng công cụ dòng lệnh để tạo TypeID, giải mã UUID từ TypeID hiện có, và mã hóa UUID thành TypeID
    • typeid new prefix để tạo TypeID mới
    • typeid decode prefix_01h2xcejqtf2nbrexx3vqjhp41 để xuất type và uuid
    • typeid encode prefix 0188bac7-4afa-78aa-bc3b-bd1eef28d881 để tạo TypeID
  • Có thể dùng TypeID Converter của Jetify để chuyển chuỗi TypeID sang UUID hoặc chuyển UUID sang TypeID theo định dạng prefix:UUID

1 bình luận

 
GN⁺ 2023-06-29
Ý kiến trên Hacker News
  • Có vài đề xuất: chuỗi prefix nên được cố định và ghi vào tài liệu trước khi quá muộn
    Xem implementation Go thì có vẻ ổn vì là ASCII chữ thường, nhưng với các kiểu phức hợp như article-comment thì hơi mơ hồ
    Với các dự án phức tạp hoặc ORM, rất khó tránh dấu phân cách nên đáng để cân nhắc cho phép một dấu phân cách duy nhất
    Implementation Go hiện chưa có test, nhưng đây là loại code rất dễ viết unit test nên nhất định nên bổ sung
    Trong Go, có lẽ nên dùng hàm parse tử tế và mảng byte nội bộ giống implementation UUID của Google, còn chuỗi chỉ nên dùng cho render và prefix
    Cách parse hiện tại quá dễ dãi và có vẻ nếu suffix rỗng thì sẽ chuyển sang chế độ tạo mới, còn việc index sau SplitN có thể gây panic nếu không có dấu gạch dưới
    Bản thân thiết kế thì tôi đã cố tìm điểm để chê, nhưng cuối cùng có vẻ nó đã chọn được sweet spot của các trade-off khá tốt

    • Phần phức tạp nhất như mã hóa base32 thì đã có test: https://github.com/jetpack-io/typeid-go/blob/main/base32/bas...
      Dù vậy góp ý này là đúng, và khi số implementation ở nhiều ngôn ngữ tăng lên thì cần xác minh chúng có tương thích với nhau hay không, nên dự định sẽ bổ sung bộ test nghiêm ngặt hơn
      Về prefix, tôi muốn hỏi liệu điều đáng lo là chưa định nghĩa tập ký tự được phép trong đặc tả hay không
    • README đang kêu gọi các implementation khác, nên nếu có bộ test thì cũng sẽ tốt cho mã của bên thứ ba
    • Sau đó đã triển khai một bộ test khá kỹ: https://github.com/jetpack-io/typeid-go/blob/main/typeid_tes...
      Và cũng đã làm rõ prefix trong đặc tả
    • Tôi sẽ thử viết test cho việc này
    • Có vẻ bạn đã hiểu nhầm rằng prefix là thứ cố định
      Prefix được quyết định tùy theo domain sử dụng
  • Tôi đã dùng kiểu này nhiều năm rồi, và có hai điểm khác biệt
    Thứ nhất, tôi không cho rằng mọi người thực sự nhập giá trị này bằng tay, nên không quá bận tâm đến việc nhầm l với 1
    Thay vào đó, để giảm khả năng vô tình tạo thành từ ngữ, đặc biệt là chửi thề, tôi dùng base32 bỏ các nguyên âm eiou
    Thứ hai, tôi thêm hai ký tự base32 có salt làm checksum
    Nhờ vậy khi giá trị có vấn đề do vô ý hoặc ác ý, không cần truy vấn tới kho dữ liệu; tôi không rõ vì sao các implementation khác lại không làm điều này

    • Vài năm trước từng có mật khẩu tự sinh ra là analrita và tôi đã nhận được khiếu nại
      Có thể nên cân nhắc thêm a vào danh sách ký tự bị loại bỏ
    • Tôi đồng ý với việc thêm checksum, nhưng hơi tò mò về phần “vô ý hoặc ác ý”
      Nếu cho rằng con người không nhập tay, thì tôi thắc mắc đường nào dẫn đến việc đưa vào giá trị sai do vô ý
      Có thể là lỗi copy/paste hoặc trang hiển thị thành chữ hoa, nhưng nếu là base32 thì chỉ cần chuẩn hóa hoa/thường là được
      Ngoài ra tôi cũng tò mò checksum 2 byte, không an toàn về mặt mật mã, sẽ giúp gì trong trường hợp có ác ý
    • Ý tưởng checksum khá thú vị
      Tôi đang xem xét liệu có nên đưa nó vào đặc tả TypeID hay không
    • Vài tháng trước tôi đã triển khai cách thứ hai như một phần của phương thức mã hóa
      Tôi không rõ nó giảm được bao nhiêu lần truy vấn cơ sở dữ liệu, nhưng thật tốt khi trong lúc giải mã không rơi vào các lỗi mơ hồ hơn
  • Ngoài nội dung chính còn có một liên kết tới “Crockford's alphabet”: https://www.crockford.com/base32.html
    Hệ base32 này loại trừ I, L dễ nhầm với 1, O dễ nhầm với 0, và cả U
    Trong trang đó nói lý do bỏ U là để tránh “tục tĩu ngoài ý muốn”, nhưng tôi thực sự không hiểu ý đó là gì

    • Đó là cách Crockford viết đùa thôi
      Để tạo ra một bảng chữ cái base32 ổn, dùng chữ và số mà không gây nhầm lẫn, thực ra chỉ cần bỏ O, I, L
      Nhưng như vậy vẫn còn 33 ký tự nên phải bỏ thêm một ký tự nữa; bỏ ký tự nào cũng được, nên ông ấy chỉ gán cho ký tự cuối cùng bị loại một lý do mang tính tùy hứng
      Nếu là ID hiển thị cho người dùng thì đây cũng không hẳn là một lý do quá vô lý, nhưng dĩ nhiên hoàn toàn không thể ngăn chặn triệt để
    • Nếu đã loại IO, rồi bỏ thêm U, thì cũng loại được khá nhiều tổ hợp ba hoặc bốn chữ cái trông thô tục
      Tất nhiên vẫn còn một số tổ hợp khả dĩ vì vẫn có A, nhưng xác suất rất thấp
    • Với một người theo chủ nghĩa Latin đích thực, U vốn không tồn tại vào thời Cicero, nên đó là một chữ cái thô thiển đến mức gần như tục tĩu
    • Còn có những thứ khác nữa: base58 theo cách của Satoshi/Bitcoin https://en.wikipedia.org/wiki/Binary-to-text_encoding#Base58, “base62” của saltpack bên Keybase https://github.com/keybase/saltpack, “Adobe 85” nổi tiếng https://en.wikipedia.org/wiki/Ascii85, và basE91 https://base91.sourceforge.net
      Ở công ty, chúng tôi từng định nghĩa thêm vài “cơ số” mới cho mã QR, và cá nhân tôi thấy đây là một mảng còn ít được khai thác trong khoa học máy tính
    • Khoảng 4 năm trước Crockford, tôi từng cùng một đồng nghiệp nghĩ ra gần như cùng một tập ký tự
      Khi đó chúng tôi đang giải một bài toán về URL slug, và vì chuỗi khá dài nên nghĩ rằng 5 bit mỗi byte có thể giúp giảm khó khăn khi chép tay
      Cuối cùng vẫn còn dư chỗ để bỏ thêm vài ký tự, và sau khi mọi người tan làm, hai chúng tôi ngồi lại xếp hạng mức độ xúc phạm của các từ chửi để quyết định bỏ chữ nào
      Sau đó tôi thuyết phục rằng các từ mang tính miệt thị mới là vấn đề lớn hơn, nên chuyển sang tập trung vào việc bỏ n thay vì u
      tggr thì chỉ buồn cười thôi, nhưng n**r lại dễ dẫn tới những cuộc trao đổi khó xử với bộ phận HR
      Tôi giờ đã nhớ hơi mơ hồ về tập ký tự cuối cùng, nhưng khi đó chúng tôi xử lý kiểu [a-z][0-9], và có khá nhiều ký hiệu không dùng được trong URL hoặc khó đọc thành tiếng
      Nếu nhớ không lầm thì chúng tôi đã bỏ cả 0, l, 1, và giả định việc chép tay sẽ diễn ra hoàn toàn bằng chữ hoa hoặc hoàn toàn bằng chữ thường
      0o không thành vấn đề, và 1L cũng vậy
  • Tôi không thích mã hóa Crockford lắm
    Trên thực tế, khi phải hỗ trợ kỹ thuật hoặc phân tích các giá trị được mã hóa theo kiểu này, tôi đã thấy đó rõ ràng là một sai lầm
    Bảng chữ cái này được thiết kế cho một mục tiêu thực tế khá hiếm gặp, như đọc định danh qua điện thoại, và vì thế lại đưa thêm tính mơ hồ vào
    Trong những tình huống cần grep log hoặc đối chiếu chéo bằng chính chuỗi mã hóa, nó là thảm họa; việc còn cho phép cả dấu gạch nối khiến nó trở thành nguồn lớn gây lỗi sao chép/dán và lỗi xuống dòng
    Trường hợp phải tự gõ trực tiếp mã định danh đối tượng là hiếm, nhưng việc sao chép/dán giữa ứng dụng, chat, diễn đàn, gửi qua email, rồi tìm kiếm trong file log thì rất thường xuyên
    Trong những điều kiện đó, khả năng đọc thành tiếng là vô nghĩa, việc không phân biệt hoa thường lại gây cản trở, còn điều cần thiết là tính nhất quán và khả năng chống lỗi khi dán hoặc xuống dòng
    base58 là một kiểu mã hóa song ánh phù hợp với các yêu cầu này hơn, lại còn ngắn hơn
    Lấy cảm hứng từ Stripe, tôi đã dùng UUID mã hóa base58 kèm tiền tố kiểu làm mã định danh đối tượng trong nhiều năm, dạng như user_1BzGURpnHGn6oNru84B3Ri
    Tuy vậy cũng nên tính đến việc base32 của Douglas Crockford được thiết kế từ 20 năm trước, trong một bối cảnh sử dụng khá khác hiện nay

    • Tôi cũng từng cân nhắc base58 và base64url, và đúng là tôi thích các kiểu mã hóa ngắn hơn
      Nhưng tôi không muốn mặc định dựa vào việc phân biệt hoa thường, nên cuối cùng lại nghiêng về base32
      Ví dụ, bạn có thể muốn dùng ID làm tên tệp, và môi trường đó có thể bị ràng buộc bởi hệ thống tệp không phân biệt hoa thường
      TypeID chỉ dùng bảng chữ cái Crockford luôn ở chữ thường, chứ không dùng toàn bộ quy tắc mã hóa Crockford
      TypeID không cho phép dấu gạch nối, và cũng không cho phép mã hóa cùng một ID theo nhiều cách khác nhau bằng cách dùng các ký tự mơ hồ thay thế
    • Tôi đồng ý rằng việc đọc mã định danh qua điện thoại là hiếm
      Nhưng đôi lúc vẫn có trường hợp phải tự nhập các mã hiện trong ảnh chụp màn hình hoặc chia sẻ màn hình, hay trên một thiết bị khác nơi không thể dễ dàng lấy được mã định danh
    • base62 mà base58 hoặc KSUID sử dụng có nhiều ưu điểm
      Crockford base32 vẫn dùng được, nhưng tôi cũng không thực sự thích nó lắm
      Cá nhân tôi thì lựa chọn số một vẫn là dùng KSUID có thêm tiền tố kiểu
      Như vậy sẽ có được ID 160 bit sắp xếp được theo K với mã hóa base62, và sẽ rất phù hợp trừ khi vì lý do tương thích mà bạn bắt buộc phải dùng ID 128 bit
    • Tôi không rõ dấu gạch nối nằm ở đâu trong Crockford Base32: https://en.wikipedia.org/wiki/Base32#Crockford's_Base32
      Kiểu mã hóa base32 tôi thích là z-base-32, vì cảm thấy dễ nhìn hơn: https://philzimmermann.com/docs/human-oriented-base-32-encod...
      Vấn đề lớn nhất của base58 là nó hợp với số nguyên hơn, nhưng kém phù hợp hơn với dữ liệu nhị phân tùy ý như khóa mật mã, và chuỗi có phân biệt hoa thường thì không đẹp mắt lắm
  • Gọn gàng
    Tôi thích tiền tố “an toàn kiểu”
    Trong ORM của chúng tôi, bọn tôi cũng có cách tương tự là tự động gắn thẻ theo từng entity vào ID số nguyên trong DB, và gọi đó là tagged ids: https://joist-orm.io/docs/advanced/tagged-ids
    Bọn tôi dùng : làm dấu phân cách, nhưng hơi tiếc là đã không dùng _ vì xét về việc copy/paste bằng cách double-click thì _ tiện hơn
    Về lý thuyết thì để Joist biến “cột UUID trong DB” thành “TypeID trong mô hình domain” cũng sẽ khá dễ, nhưng hiện tại có vẻ chưa thể làm chỉ bằng cấu hình ở phía người dùng
    Dù vậy đây vẫn là một ý tưởng hay

    • Reddit cũng tương tự, nhưng dùng một cách tối ưu hơn cho độ dài chuỗi
      ID của phần tử có dạng như t3_15bfi0, trong đó t3_ là tiền tố kiểu
      t3 là bài đăng, t1 là bình luận, t5 là subreddit, còn phần còn lại là mã hóa base36 của khóa chính tự tăng
    • Hệ thống ID nội bộ có kiểu của công ty tôi ban đầu cũng dùng dấu hai chấm làm dấu phân cách, nhưng rồi nhanh chóng đổi sang dấu chấm (.)
      Dấu hai chấm bị mã hóa thành % trong URL encoding, làm ID dài hơn, URL xấu hơn và dài hơn nên rất khó chịu
  • UUIDv7 đã được ưa chuộng trên HN nhiều năm nay, nên tôi khá tò mò không biết bao giờ nó mới trở thành một tiêu chuẩn thực sự, và khi nào thư viện, cơ sở dữ liệu cùng phần còn lại của hệ sinh thái sẽ hỗ trợ native

    • Tôi không rõ bạn đang mong chờ kiểu hỗ trợ nào
      Phần lớn phần mềm không quan tâm đến các bit cụ thể bên trong UUID, nên hôm nay cũng đã có thể dùng rồi
      Nếu có phần mềm nào quan tâm đến các bit cụ thể đó thì cứ làm cho nó trông giống UUIDv4 là được, mà các bit ấy cũng có thể được tạo ngẫu nhiên
      Nếu cần quy trình tạo thì cứ tự viết, việc đó không khó
    • Có vẻ nó đang ở giai đoạn cuối của quá trình chuẩn hóa tại IETF: https://datatracker.ietf.org/doc/draft-ietf-uuidrev-rfc4122b...
    • Nó đã trải qua các bản nháp và cải tiến, hiện đã rất gần với chuẩn hóa, và đã có nhiều thư viện hỗ trợ hoặc thêm tính năng mới
      Ví dụ, tôi quản lý thư viện UUID cho Dart, và bản phát hành major beta mới nhất đã có v6, v7 và v8 tùy biến
      Hình như ở đâu đó có danh sách các triển khai, và tôi có tên trên trang đó với vai trò maintainer thư viện, nên mỗi khi có bản nháp mới thì tôi đều nhận được thông báo gửi tới các tác giả
  • Tôi thích đoạn “có thể copy/paste bằng double-click”
    Chi tiết mới là thứ quan trọng

  • Tôi hơi có vấn đề với UUID
    Tôi không hiểu tại sao lại phải làm nghi thức kiểu versioning UUID, thay vì chỉ đơn giản ghép thời gian + số ngẫu nhiên lại với nhau
    Nếu tính cục bộ không quan trọng thì cứ dùng luôn số ngẫu nhiên 128-bit là được
    Theo kinh nghiệm của tôi, có vẻ đa số mọi người nghĩ rằng UUID phải được lưu dưới dạng biểu diễn hex 16 ký tự dễ đọc cho con người, thậm chí còn kèm cả dấu gạch nối
    Quá lãng phí không gian trong cơ sở dữ liệu, mạng và bộ nhớ

    • Nhiều người cũng đã nghĩ như vậy
      Ví dụ như ULID https://github.com/ulid/spec ngắn hơn và có lưu thời gian nên sắp xếp được theo thứ tự từ điển
    • Phía UUID thường có xu hướng nói về nó như thể một use case có thể đại diện cho toàn bộ, và vì vậy tồn tại một định dạng UUID tốt nhất duy nhất
      Thực tế cũng như các bài toán kỹ thuật khác thôi: hãy viết ra yêu cầu rồi xem cái gì giải quyết được chúng
      Đọc về ưu điểm của nhiều định dạng khác nhau rất hữu ích, vì nó giúp bạn tận dụng những bài học mà người khác đã phải mất công mới rút ra được
      Khả năng sắp xếp và tính cục bộ dựa trên thời gian là những điều không phải lúc nào bạn cũng tự nhiên nghĩ đến, nhưng nếu có nhu cầu thì biết trước vẫn tốt hơn nhiều so với việc 4 năm sau mới nhận ra dự án đã vứt bỏ dữ liệu đó
      Một số định dạng UUID còn tự đưa vào những vấn đề bảo mật nhỏ, chẳng hạn như việc UUID v1 làm lộ địa chỉ MAC, và tốt nhất là nên tránh
      Nếu giải pháp sẵn có phù hợp với use case của bạn thì cứ dùng thôi
      Nếu không thì cũng không cần quá lo lắng
      Cá nhân tôi, ở nơi tôi dùng UUID nhiều nhất, tôi chỉ quy định là “hãy gửi một chuỗi duy nhất, nếu thích thì dùng bất kỳ thư viện UUID nào bạn thấy yên tâm”
      Hệ thống này có vẻ có một định dạng riêng cho từng nguồn dữ liệu, nhưng điều đó vẫn ổn
      Quy mô chỉ ở mức vài chục nghìn bản ghi mỗi ngày nên cũng không có vấn đề về scale, và cũng không cần sắp xếp theo UUID
      Nó không hẳn là một định danh thực sự, mà giống một token duy nhất do phía gửi tạo ra cho mỗi thông điệp, dùng để phát hiện các lần giao trùng lặp ở downstream trong một hệ thống không đồng nhất có nhiều hàng đợi
      Cũng không cần tính duy nhất toàn cục, chỉ cần duy nhất trong một shard nhỏ là đủ, và về nguyên tắc thì sau một thời gian lặp lại cũng không sao
      Chỉ là để đơn giản nên chúng tôi vẫn duy trì hệ thống theo hướng duy nhất trong toàn bộ thời gian tồn tại
      Khi tự tạo, chúng tôi đọc dữ liệu từ /dev/urandom rồi mã hóa bằng base64, và chọn kích thước sao cho không kết thúc bằng ==
      Chuyện này cũng không phải vì vấn đề thực tế gì, mà chỉ vì nhìn cho đẹp
    • Ghép thời gian với số ngẫu nhiên không thể đảm bảo tính duy nhất toàn cục
    • Cá nhân tôi thấy tiêu chuẩn UUID đáng giá duy nhất là v4, tức loại ngẫu nhiên đơn thuần
      Dù vậy tôi vẫn không hiểu vì sao cần dấu gạch nối, còn các phiên bản khác thì tôi thấy không có nhiều ý nghĩa
    • Điều tệ nhất của dấu gạch nối là không thể dễ dàng copy/paste toàn bộ chỉ bằng double-click
  • Nếu không nhất thiết phải dùng UUIDv7 hay UUID, thì https://github.com/segmentio/ksuid cung cấp không gian khóa lớn hơn nhiều
    Nếu cần namespace thì chỉ việc thêm tiền tố chuỗi, và khả năng va chạm của KSUID nhỏ hơn rất nhiều so với bất kỳ phiên bản UUID nào
    Trong số các bộ tạo ID đa dụng có dấu thời gian có thể sắp xếp được, ksuid là lựa chọn tốt nhất, và có thư viện cho hầu hết các ngôn ngữ
    UUID v1~v7 khá lãng phí

    • Tôi đã phát hành một gói JavaScript mỏng để tạo KSUID có gắn tiền tố kiểu
      Nếu bạn chịu được số bit bổ sung, mà đa số trường hợp là được, thì KSUID là một định dạng tuyệt vời
      [1] https://github.com/sophiabits/resource-id
    • Lý do chuyển sang ULID là vì nó luôn viết thường nên có thể xử lý theo kiểu không phân biệt hoa thường
    • Tôi đang bảo trì vài thư viện ksuid phổ biến và thực sự đang dùng chúng, nên tôi thích ksuid
      Tuy vậy, vấn đề lớn của ksuid là nó dài 160 bit nên không khớp với kiểu UUID native của cơ sở dữ liệu, ví dụ như PostgreSQL, và vì thế phát sinh chi phí hiệu năng
      1: https://github.com/svix/rust-ksuid
      2: https://github.com/svix/python-ksuid
  • Có thể sắp xếp theo K là một khái niệm rất hay, và các khóa có thứ tự lỏng giúp giải quyết nhiều trường hợp sử dụng
    Tôi cũng thích biểu diễn chuỗi nén có kèm kiểu
    Tuy nhiên, tôi lo rằng một tác dụng phụ ngoài ý muốn của UUID v7 sẽ là phát sinh nhiều vấn đề bảo mật
    Mọi người không nên dùng UUID làm token, và cũng không nên dùng như khóa chính của DB, nhưng trên thực tế họ vẫn làm vậy
    UUID v4 về cơ bản là số ngẫu nhiên mật mã, nên tôi nghĩ đến nay nhiều lỗ hổng bảo mật đã vô tình thoát nạn nhờ điều đó
    Tôi từng nghĩ ở UUID v7 dữ liệu ngẫu nhiên thực tế chỉ còn 32 bit, và cần giáo dục kỹ cho lập trình viên rằng UUID có thể bị đoán ra
    Sửa lại: có vẻ tôi đã hiểu sai về khả năng bị đoán của UUID v7
    Bản nháp khuyến nghị dùng CSPRNG cho các bit ngẫu nhiên, entropy tối thiểu là 74 bit, và được thiết kế để “không thể đoán được”
    Dù có nói rằng với mục đích liên quan đến bảo mật thì nên dùng “UUID v4”, nhưng có lẽ điều đó mang hàm ý liên quan đến dấu thời gian

    • Có một vấn đề về tính khả dụng khá thú vị
      Khi tên là UUIDv4 và UUIDv7, liệu có nảy sinh mãi sự nhầm lẫn rằng cái nào tốt cho cơ sở dữ liệu và cái nào phù hợp cho token dùng một lần, vì phải luôn ghi nhớ điều đó không
      Tôi cũng không rõ có giải pháp tương thích ngược nào hay không
      Trong Elixir, khi tạo UUID v4 thì dùng hàm UUID.uuid4()
      Về lý thuyết có thể quét mã nguồn để xem cách dùng, nhưng những chuyện như vậy đều làm tăng khả năng lỗi
    • Tôi có thể thấy một vài trường hợp đáng dùng, nhưng trước đây mỗi lần gặp các UUID tuần tự một phần như v1 hay v5 thì chúng đều đang bị lạm dụng
      Các loại dựa trên băm như v3 cũng tương tự
      v4 thì đơn giản và ít khả năng bị dùng sai hơn