عنوان:

‫پشتیبانی از تولید UUID v7 در دات‌نت 9


نویسنده: وحید نصیری
تاریخ: ۱۴۰۳/۰۹/۰۶ ۰۰:۰۰
آدرس: www.dntips.ir
نوع Guid، از ابتدای ارائه‌ی دات‌نت به همراه آن‌ بوده و با استفاده از متد NewGuid آن، قابل تولید است که یک UUID Version 4 را تولید می‌کند (UUID = Universally unique identifier).
var guid = Guid.NewGuid(); // v4 UUID

اغلب بانک‌های اطلاعاتی از کلیدهای اصلی عددی در جداول خود استفاده می‌کنند؛ اما استفاده از GUIDs‌ برای اینکار، مزایای ویژه‌ای را به‌همراه دارد:
- رسیدن به یک مقدار منحصربفرد سراسری. برای مثال اگر چندین بانک‌ اطلاعاتی را دارید و در نهایت قرار است اطلاعات جداول این‌‌ها با هم یکی شوند، استفاده از کلیدهای اصلی از نوع GUIDs برای اینکار بسیار مناسب هستند.
- نیاز است ID مدنظر را پیش از موعد داشته باشیم. این مورد در سیستم‌های توزیع شده بسیار مهم‌است. خصوصا جائیکه قرار است چندین عمل باهم انجام شوند؛ مانند ثبت در بانک‌اطلاعاتی، فراخوانی وب سرویس و ... و در این بین نمی‌خواهیم (یا نمی‌توانیم) که منتظر دریافت این ID از بانک اطلاعاتی شویم (فیلدهای Identity را نمی‌توان توسط کدها تعریف کرد؛ چون باید منتظر دریافت مقدار آن‌ها پس از ثبت نهایی داده‌ها در بانک اطلاعاتی شویم). اگر این ID را در کدهای #C خود تولید کنیم، می‌توانیم عملیات زیر مجموعه‌ای متعددی را خیلی سریع انجام داده و سپس توسط این ID، آن‌ها را به هم مرتبط کنیم.
- می‌خواهیم ID ای را که به دنیای خارج معرفی می‌کنیم، قابل حدس زدن نباشد. برای مثال نتوان لیست مقالات سایت را بر اساس ID ترتیبی آن‌ها به سادگی پیمایش کرد.

هرچند استفاده‌ی از GUID v4 در بانک‌های اطلاعاتی، می‌تواند به index fragmentation به‌علت درج آن‌ها در موقعیت‌هایی اتفاقی در ایندکس منتهی شود که در نهایی می‌تواند اندکی بر روی کارآیی تاثیرگذار باشد؛ چون ترتیبی نیستند.


Guid V7
دات‌نت 9 به همراه پیاده سازی جدیدی از GUID است که بر اساس timestamp و اعداد اتفاقی کار می‌کند و اینبار با استفاده از متد جدید CreateVersion7 نوع Guid قابل تولید است:
Guid uuidV7 = Guid.CreateVersion7(); // uses Datetime.UtcNow
و یا می‌توان زمان تولید را هم مشخص کرد (که بیشتر برای انجام آزمون‌ها مفید است):
var guidWithTimestamp = Guid.CreateVersion7(DateTimeOffset.UtcNow);

همچنین نوع Guid در دات‌نت 9، به همراه خاصیت جدید Version نیز شده‌است تا نگارش Guid تولیدی را مشخص کند:
var guid = Guid.CreateVersion7();
Console.WriteLine($"V{guid.Version}: {guid}");

یک UUID v7 متشکل از این اجزا است:
- یک 48bit Timestamp، که بیانگر تعداد میلی ثانیه‌ی گذشته از زمان Unix epoch است.
- یک 12bit Random Segment. وجود آن سبب می‌شود تا در میلی‌ثانیه‌ای مشخص، خروجی نهایی اتفاقی باشد.
- یک 62bit Random Segment که میزان entropy منحصربفرد بودن را افزایش می‌دهد.

+------------------+---------------+----------------------+
| 48-bit timestamp | 12-bit random |    62-bit random     |
+------------------+---------------+----------------------+
برای تولید یک چنین خروجی، متد ()Guid.CreateVersion7 جدید، از همان متد NewGuid قبلی استفاده می‌کند (این مورد را در شبه کد زیر که تولید Guid V7 را شبیه سازی می‌کند، مشاهده می‌کنید). ابتدا یک 48bit timestamp را تولید کرده و سپس مابقی آن‌را به کمک بایت‌های NewGuid قبلی، بر اساس استاندارد UUIDv7 تکمیل می‌کند. یعنی تولید آن نسبت به بکارگیری متد NewGuid، اندکی کندتر است؛ هرچند اگر نیاز به چند میلیون Guid نداشته باشید، اهمیتی ندارد.
public static class GuidV7
{
    private const byte Variant10xxMask = 0xC0;
    private const byte Variant10xxValue = 0x80;
    private const ushort VersionMask = 0xF000;
    private const ushort Version7Value = 0x7000;

    public static Guid Create() => Create(DateTimeOffset.UtcNow);

    public static Guid Create(DateTimeOffset timestamp)
    {
        var unixTsMs = timestamp.ToUnixTimeMilliseconds();
        ArgumentOutOfRangeException.ThrowIfNegative(unixTsMs, nameof(timestamp));

        var result = Guid.NewGuid().ToByteArray();

        var a = result.AsSpan(..4);
        BitConverter.TryWriteBytes(a, (int)(unixTsMs >> 16));
        var b = result.AsSpan(4..6);
        BitConverter.TryWriteBytes(b, (short)unixTsMs);
        var c = result.AsSpan(6..8);
        BitConverter.TryWriteBytes(c, (short)((BitConverter.ToInt16(c) & ~VersionMask) | Version7Value));
        var d = result.AsSpan(8..9);
        d[0] = (byte)((d[0] & ~Variant10xxMask) | Variant10xxValue);
        return new Guid(result);
    }
}

در اینجا برای مقایسه، چندین Guid با نگارش‌های مختلف آن، تولید شده‌اند:
for (int i = 0; i < 5; i++)
{
    var guid = Guid.NewGuid();
    Console.WriteLine($"V{guid.Version}: {guid}");
}
Console.WriteLine();
for (int i = 0; i < 5; i++)
{
    var guid = Guid.CreateVersion7();
    Console.WriteLine($"V{guid.Version}: {guid}");
}
با این خروجی:
V4: 0557b321-abcf-4390-abee-4b8fbf93ff34
V4: 21a98165-af1e-477e-9dee-7eb9c79e6c77
V4: 7dbbf973-c55a-4917-87a5-95c16f356262
V4: b13892f2-334f-409a-b9de-d90dea21eed4
V4: 52dc44f7-76e0-4689-a5e6-1a0f1c5f37a3

V7: 01917bbe-d973-7beb-a813-106fcb4eff98
V7: 01917bbe-d973-703c-8365-b7596740ac82
V7: 01917bbe-d973-7234-a580-5f07730a3ad7
V7: 01917bbe-d973-7751-b8ba-bb73afab4a5d
V7: 01917bbe-d973-7d36-9be0-2e6317919153
همانطور که مشاهده می‌کنید، GUID V7 به دلیل وجود قسمت timestamp آن، ترتیبی و قابل مرتب سازی است و برای ذخیره سازی در بانک‌های اطلاعاتی، انتخاب بهتری به‌شمار می‌رود.


UUID v4 برای امنیت بیشتر و UUID v7 برای کلیدهای اصلی

نکته‌ی مهمی را که باید در اینجا درنظر داشت، میزان تخمین‌پذیری این دو نوع UUID است:
Guid قدیمی دات‌نت، همان UUID v4 است که مهم‌ترین مزیت آن، کم بودن میزان تخمین‌پذیری آن است. هر UUID v4 مستقل از دیگری تولید می‌شود و هیچگونه ارتباطی بین آن‌ها خصوصا از لحاظ زمان تولید آن‌ها، وجود ندارد. اما UUID v7، به دلیل قسمت timestamp آن، میزان تخمین پذیری بالایی دارد که می‌تواند سبب لو رفتن و یا حدس UUIDs بالقوه‌ای شود؛ هرچند این شانس حدود 1:1000000000 است! چون بیت‌های پایانی آن، اتفاقی تولید می‌شوند، اما باز هم میزان تخمین پذیری نهایی آن بیشتر است از نمونه‌های UUID v4.
بنابراین UUID v4 نسبت به UUID v7 امن‌تر است و حدس زدن آن مشکل‌تر. به همین جهت از UUID v7 برای تولید کلمات عبور در در حین عملیات بازیابی کلمات عبور، Idهای سشن‌ها و امثال آن استفاده نکنید و کاربرد آن‌را محدود به کلیدهای اصلی بانک‌های اطلاعاتی کنید.

یک نکته: Guilds قدیمی دات‌نت هم cryptographically secure نیستند! هرچند روش‌هایی برای تولید یک چنین Guids با استفاده از کلاس RandomNumberGenerator وجود دارد:
public static Guid CreateCryptographicallySecureGuid()
{
    using (var provider = System.Security.Cryptography.RandomNumberGenerator.Create())
    {
        var bytes = new byte[16];
        provider.GetBytes(bytes);

        return new Guid(bytes);
    }
}

نظرات