מחולל UUID

יצירת מזהי UUID — v4 אקראי או v7 ממוין לפי זמן — בודדים או במנות.

גרסה

122 ביט אקראיים. לא חושף דבר על מועד היצירה.

כמה

יוצר…

מהו UUID ולמה הוא קיים

UUID — ראשי תיבות של Universally Unique Identifier, ובתיעוד של מיקרוסופט מכונה GUID — הוא ערך בן 128 סיביות שנכתב כ-32 ספרות הקסדצימליות בקיבוץ המוכר 8-4-4-4-12. כל תכליתו היא לאפשר למערכות נפרדות לייצר מזהים באופן עצמאי, בלי שום תיאום ביניהן, ועדיין להיות בטוחות שהתוצאות לא יתנגשו. זה מה שמבדיל אותו מעמודת auto-increment במסד נתונים: שני שרתים, שני לקוחות ניידים במצב לא-מקוון במטוס, ומשימת רקע — כולם יכולים ליצור רשומות באותו רגע בלי לבקש רשות מאיש.

הייחודיות כאן היא הסתברותית ולא מובטחת. גרסה 4 מותירה 122 סיביות לאקראיות, וזה מרחב גדול מספיק כך שגם ייצור של מיליארדי ערכים משאיר את הסיכוי לחזרה הרחק מתחת לסיכוי שדיסק ישבש את הנתונים בשקט במקום. בפועל אפשר להתייחס לזה כאל ייחודי; המתמטיקה אינה החוליה החלשה.

גרסה 4 מול גרסה 7 — ההחלטה שבאמת חשובה

גרסה 4 אקראית מקצה לקצה. היא אינה נושאת שום מידע: לא מתי נוצרה, לא בידי מי, ולא באיזה סדר. זו או עוצמתה הגדולה ביותר או פגמה המרכזי, תלוי איפה שמים אותה.

גרסה 7, שתוקננה ב-RFC 9562 ב-2024, מחליפה את 48 הסיביות הראשונות בחותמת זמן Unix במילישניות וממלאת את השאר באקראיות. מכיוון שהזמן בא ראשון והערך נקרא משמאל לימין, מיון של מזהי v7 כטקסט רגיל ממיין אותם גם כרונולוגית.

זה אינו הבדל קוסמטי. מסדי נתונים שומרים מפתחות ראשיים באינדקס B-tree ממוין. הכנס מפתחות v7 וכל שורה חדשה נוחתת בקצה הימני של העץ, ליד קודמתה — הדפים שאליהם כותבים נשארים בזיכרון והאינדקס גדל בצורה מסודרת. הכנס מפתחות v4 וכל כתיבה נוחתת במיקום אקראי, כך שהמסד נוגע בדף אחר בכל פעם, שיעורי הפגיעה במטמון יורדים והאינדקס מתפצל. בטבלה גדולה ועמוסה ההבדל בקצב ההכנסה משמעותי, ולכן v7 אומץ כל כך מהר למפתחות ראשיים.

  • בחר v7 למפתחות ראשיים במסד נתונים, למזהי אירועים ולוגים, ולכל דבר שתרצה למיין או לתשאל לפי טווח זמן יצירה.
  • בחר v4 כשהמזהה מופיע במקום לא מהימן ואסור שידלוף ממנו דבר — כולל העובדה שרשומה אחת נוצרה רגע לפני אחרת.
  • שתיהן מתאימות ל-correlation ID, למפתח אידמפוטנטיות או לשם קובץ, שם הסדר אינו משנה.

התמורה היא בדיוק הדליפה הזאת: מזהה v7 מספר לכל מי שרואה אותו, ברזולוציה של מילישנייה, מתי הוא נוצר. עבור מזהה שורה זה בדרך כלל לא מזיק ולעיתים קרובות מועיל. עבור אסימון איפוס סיסמה או קישור שיתוף ציבורי זה מידע שלא התכוונת לפרסם — ואלה בכלל לא צריכים להיות UUID, אלא אסימונים אקראיים ממחולל סודות ייעודי.

סדר בתוך אותה מילישנייה

דקוּת שתופסת מימושי v7 רבים: חומרה מודרנית מייצרת הרבה יותר ממזהה אחד למילישנייה. אם שני ערכים חולקים חותמת זמן, הסדר ביניהם נקבע על ידי הסיביות האקראיות שאחריה — כלומר, באקראי. צור חמישים בלולאה צפופה ותקבל חמישים ערכים שמסודרים בקירוב בלבד, ותאבד בדיוק את התכונה שבגללה בחרת ב-v7.

RFC 9562 פותר זאת באמצעות מונה מונוטוני, והכלי הזה מממש אותו: 12 הסיביות שמיד אחרי חותמת הזמן סופרות כלפי מעלה בתוך המילישנייה, כך שמנה עולה בהחלט ולא רק בקירוב. אם המונה מתמלא — יותר מ-4096 ערכים באותה מילישנייה — המחולל לווה את המילישנייה הבאה במקום לגלוש ולייצר ערך שממוין לפני קודמו. הוא מטפל באותה רוח גם בשעון שנע אחורה, מה שקורה בתיקוני NTP.

קריאה של ערך v7 משמאל לימין, כשהדוגמה היא 0190a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b:

  • 0190a1b2-c3d4 — חותמת זמן Unix בת 48 סיביות במילישניות. מכיוון שהיא באה ראשונה, סדר הטקסט הוא סדר הזמן.
  • 7 — ניבל הגרסה, שהוא מה שהופך את זה ל-v7 ולא ל-v4.
  • e5f — המונה המונוטוני בן 12 הסיביות, שעולה בתוך מילישנייה אחת.
  • 8 — סיביות ה-variant, שנקבעו ב-RFC 9562 לכל UUID מודרני (תמיד 8, 9, a או b).
  • a9b-0c1d2e3f4a5b — 62 הסיביות הנותרות, אקראיות לחלוטין.

אחסון ושימוש ב-UUID

הצורה הקנונית היא אותיות קטנות עם מקפים, ו-RFC 9562 קובע שמחוללים צריכים להוציא בדיוק את זה — ולכן הכלי הזה אינו מציע אפשרויות פורמט. עדיין תפגוש כתיבים אחרים בשטח: אותיות גדולות עטופות בסוגריים מסולסלים בכלים של מיקרוסופט, וללא מקפים היכן שמישהו רצה עמודה קצרה יותר. כולם אותן 128 סיביות, וההשוואה ביניהם צריכה להתעלם מגודל האותיות.

המקום שבו זה באמת נחשב הוא האחסון. UUID הוא 16 בתים, אבל צורת הטקסט שלו היא 36 תווים — כך שאחסון כמחרוזת יותר מכפיל את הנפח בשורה, וחשוב מכך בכל אינדקס שכולל אותה. השתמש בטיפוס מובנה היכן שקיים כזה:

uuid                      -- PostgreSQL: טיפוס מובנה של 16 בתים
BINARY(16)                -- MySQL: קומפקטי; CHAR(36) מבזבז 20 בתים לשורה
uniqueidentifier          -- SQL Server
crypto.randomUUID()       // ג׳אווהסקריפט: v4 בלבד, דורש הקשר מאובטח
uuid.uuid4() / uuid7()    # פייתון: v4 בספרייה התקנית; v7 דרך ספרייה

אזהרה אחרונה: UUID מזהה, הוא אינו מרשה. מכיוון שאי אפשר לנחש אותם, מפתה להתייחס לכתובת לא-מפורסמת שמכילה אחד כאילו היא פרטית. אבל מזהים דולפים — דרך לוגים, היסטוריית דפדפן, כותרות referrer וצילומי מסך — ולכן כל דבר שבאמת צריך הגנה עדיין זקוק לבדיקת הרשאות אמיתית מאחוריו.

שאלות נפוצות

להשתמש ב-v4 או ב-v7?
השתמש ב-v7 למפתחות ראשיים במסד נתונים ולכל דבר שתמיין לפי זמן יצירה: חותמת הזמן המובילה שומרת על הכנסות מקובצות בקצה האינדקס במקום מפוזרות עליו. השתמש ב-v4 כשהמזהה לא אמור לחשוף דבר, כולל מתי נוצר.
האם שני UUID יכולים לצאת זהים?
זה אפשרי אבל בלתי סביר עד אפסיות. לגרסה 4 יש 122 סיביות אקראיות, כך שגם אחרי ייצור מיליארדי ערכים ההסתברות להתנגשות נשארת הרחק מתחת להסתברות שהאחסון ישבש אותם בשקט במקום.
מה קרה לגרסאות 1, 3 ו-5?
גרסה 1 מקודדת חותמת זמן ואת כתובת ה-MAC של המכונה, מה שמדליף זהות חומרה ואי אפשר לייצר אותו בדפדפן כלל. גרסאות 3 ו-5 גוזרות UUID דטרמיניסטית ממרחב שמות ומשם באמצעות MD5 או SHA-1 — שימושי כשאותו קלט חייב תמיד לייצר את אותו מזהה, אבל זו משימה אחרת מיצירת מזהה חדש.
האם UUID מאובטח מספיק כדי לשמש כאסימון סודי?
UUID מגרסה 4 אינו ניתן לניחוש, אבל גרסה 7 מקודדת בגלוי את זמן היצירה, ואף אחת מהן לא נועדה לשמש כאישור גישה. לאיפוס סיסמאות, לאסימוני סשן או לקישורי שיתוף — צור סוד אקראי ייעודי ובדוק הרשאות בשרת, במקום להסתמך על כך שקשה לנחש את המזהה.
למה מנת v7 שלי אינה ממוינת לגמרי בכלים אחרים?
מפני שמימושים רבים מדלגים על המונה המונוטוני. כששני ערכים או יותר חולקים מילישנייה, הסדר ביניהם נופל לסיביות האקראיות שאחריה. הכלי הזה מממש את המונה של RFC 9562, ולכן מנה שנוצרה כאן עולה בהחלט.
איך כדאי לאחסן UUID במסד נתונים?
בטיפוס מובנה של 16 בתים היכן שקיים כזה — uuid ב-PostgreSQL, uniqueidentifier ב-SQL Server, BINARY(16) ב-MySQL. אחסון צורת הטקסט בת 36 התווים במקום זאת יותר מכפיל את הנפח בשורה ובכל אינדקס שכולל את העמודה.
האם הם נוצרים בשרתים שלכם?
לא. הם נוצרים בדפדפן שלך באמצעות crypto.getRandomValues, מקור האקראיות הקריפטוגרפי של הפלטפורמה — אותו מקור ש-crypto.randomUUID משתמש בו. שום ערך אינו נשלח לשום מקום, וטעינה מחדש של העמוד מייצרת מערכת חדשה לגמרי.