DEV Community

Cristiano Rodrigues for Unhacked

Posted on

Snowflake e UUID v7: Gerando identificadores únicos em sistemas distribuídos

Em sistemas distribuídos, gerar identificadores únicos é um problema surpreendentemente complexo. Como garantir que dois servidores em continentes diferentes não gerem o mesmo ID no mesmo milissegundo? O Twitter enfrentou esse desafio em 2010 e criou o Snowflake, um algoritmo elegante que se tornou referência na indústria. Em 2024, a RFC 9562 padronizou o UUID v7, uma alternativa que resolve o mesmo problema de forma diferente. Vamos explorar ambos.

O problema dos IDs distribuídos

Imagine um sistema de pagamentos rodando em múltiplas regiões: 20 servidores em Frankfurt, 15 em Virginia, 10 em Singapura. Cada servidor precisa gerar IDs para novas transações. Esses IDs precisam ser únicos globalmente, ordenáveis por tempo, gerados sem consultar um banco central, e rápidos o suficiente para milhões de operações por segundo. Esse é exatamente o tipo de problema que o Snowflake foi criado para resolver.

Antes de entender como ele faz isso, vale considerar as alternativas e seus problemas:

Auto-increment do banco de dados: Funciona bem para uma única instância, mas em sistemas distribuídos cria um gargalo central. Cada insert precisa consultar o banco para obter o próximo ID.

UUIDs (v4): Aleatórios e únicos, mas ocupam 128 bits (36 caracteres como string), não são ordenáveis por tempo, e têm péssima performance em índices de banco de dados por causa da fragmentação.

Timestamp puro: Dois servidores podem gerar o mesmo timestamp no mesmo milissegundo.

O Snowflake resolve todos esses problemas com uma estrutura de 64 bits que é única, ordenável, e não requer coordenação central.

Anatomia de um Snowflake ID

Um Snowflake ID é um inteiro de 64 bits dividido em segmentos:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
├─┼─────────────────────────────────────────────────────────────┤
│0│                         Timestamp                           │
├─┼─────────────────────────────────────────────────────────────┤
 0                   4                   5           6
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
├─────────────────────────────────────────┼─────────────────────┤
│                 Timestamp (cont.)       │    Machine ID       │
├─────────────────────────────────────────┼─────────────────────┤
                                          5           6
                                          0 1 2 3 4 5 6 7 8 9 0 1
                                         ├─────────┼─────────────┤
                                         │ Mach.ID │  Sequence   │
                                         ├─────────┼─────────────┤
Enter fullscreen mode Exit fullscreen mode

A estrutura clássica do Twitter:

Segmento Bits Descrição
Sign bit 1 Sempre 0 (garante número positivo)
Timestamp 41 Milissegundos desde epoch customizado
Machine ID 10 Identificador do datacenter/worker
Sequence 12 Contador sequencial por milissegundo

O que cada parte significa

Timestamp (41 bits): Armazena milissegundos desde um epoch customizado (não 1970). Com 41 bits, suporta aproximadamente 69 anos de IDs únicos. O Twitter usa epoch de novembro de 2010.

2^41 milissegundos = 2.199.023.255.552 ms
                   = ~69,7 anos
Enter fullscreen mode Exit fullscreen mode

Machine ID (10 bits): Identifica qual máquina gerou o ID. Pode ser dividido em datacenter (5 bits) + worker (5 bits), permitindo 32 datacenters com 32 workers cada, ou usado como um único identificador para 1024 máquinas.

Sequence (12 bits): Contador que incrementa quando múltiplos IDs são gerados no mesmo milissegundo pela mesma máquina. Reseta para 0 a cada novo milissegundo. Permite 4096 IDs por milissegundo por máquina.

4096 IDs/ms × 1000 ms × 1024 máquinas = ~4 bilhões de IDs/segundo (teórico)
Enter fullscreen mode Exit fullscreen mode

Como o ID é montado na prática

Para entender por que essa estrutura funciona, vamos acompanhar a geração de um ID real passo a passo.

Suponha os seguintes valores:

Timestamp:  1718451000000 ms (desde o epoch customizado)
Machine ID: 42
Sequence:   7
Enter fullscreen mode Exit fullscreen mode

Cada valor é posicionado nos bits corretos usando shift e OR:

timestamp  = 1718451000000                        (41 bits)
machineId  = 42                                   (10 bits)
sequence   = 7                                    (12 bits)

timestamp  << 22  = 7.209.067.135.365.095.424
machineId  << 12  =               172.032
sequence          =                     7

ID final = 7.209.067.135.365.095.424
         |                   172.032
         |                         7
         = 7.209.067.135.365.267.463
Enter fullscreen mode Exit fullscreen mode

Repare no que acontece: como o timestamp ocupa os bits mais altos, ele domina o valor do ID. Isso significa que um ID gerado um milissegundo depois será sempre maior, independente do Machine ID ou do Sequence. É exatamente isso que garante a ordenação temporal sem nenhuma coordenação entre servidores.

Por que essa estrutura é genial

Ordenação temporal: Como o timestamp ocupa os bits mais significativos, IDs gerados depois são sempre maiores. Isso significa que ordenar por ID é equivalente a ordenar por tempo de criação.

Sem coordenação: Cada máquina gera IDs independentemente. Não há necessidade de consultar um servidor central ou banco de dados.

Compacto: 64 bits cabem em um long, são eficientes em índices B-tree, e ocupam apenas 8 bytes.

Extraível: Você pode extrair o timestamp de qualquer ID para saber quando foi criado.

Implementação em "C#"

Uma implementação completa e thread-safe:

public sealed class SnowflakeIdGenerator
{
    // Epoch customizado: 1 de Janeiro de 2024 00:00:00 UTC
    private static readonly DateTimeOffset CustomEpoch = 
        new(2024, 1, 1, 0, 0, 0, TimeSpan.Zero);

    // Configuração de bits
    private const int TimestampBits = 41;
    private const int MachineIdBits = 10;
    private const int SequenceBits = 12;

    // Shifts para posicionar cada segmento
    private const int MachineIdShift = SequenceBits;
    private const int TimestampShift = SequenceBits + MachineIdBits;

    // Máscaras para extrair valores
    private const long MaxSequence = (1L << SequenceBits) - 1;      // 4095
    private const long MaxMachineId = (1L << MachineIdBits) - 1;    // 1023

    private readonly long _machineId;
    private readonly object _lock = new();

    private long _lastTimestamp = -1L;
    private long _sequence = 0L;

    public SnowflakeIdGenerator(long machineId)
    {
        if (machineId < 0 || machineId > MaxMachineId)
        {
            throw new ArgumentOutOfRangeException(
                nameof(machineId), 
                $"Machine ID must be between 0 and {MaxMachineId}");
        }

        _machineId = machineId;
    }

    public long NextId()
    {
        lock (_lock)
        {
            var timestamp = GetCurrentTimestamp();

            if (timestamp < _lastTimestamp)
            {
                throw new InvalidOperationException(
                    $"Clock moved backwards. Refusing to generate ID for {_lastTimestamp - timestamp}ms");
            }

            if (timestamp == _lastTimestamp)
            {
                // Mesmo milissegundo: incrementa sequence
                _sequence = (_sequence + 1) & MaxSequence;

                if (_sequence == 0)
                {
                    // Sequence overflow: aguarda próximo milissegundo
                    timestamp = WaitNextMillis(_lastTimestamp);
                }
            }
            else
            {
                // Novo milissegundo: reseta sequence
                _sequence = 0;
            }

            _lastTimestamp = timestamp;

            return (timestamp << TimestampShift) |
                   (_machineId << MachineIdShift) |
                   _sequence;
        }
    }

    private static long GetCurrentTimestamp()
    {
        return (long)(DateTimeOffset.UtcNow - CustomEpoch).TotalMilliseconds;
    }

    private static long WaitNextMillis(long lastTimestamp)
    {
        var timestamp = GetCurrentTimestamp();

        while (timestamp <= lastTimestamp)
        {
            Thread.SpinWait(100);
            timestamp = GetCurrentTimestamp();
        }

        return timestamp;
    }

    // Utilitários para extrair informações do ID
    public static DateTimeOffset ExtractTimestamp(long id)
    {
        var timestamp = id >> TimestampShift;
        return CustomEpoch.AddMilliseconds(timestamp);
    }

    public static long ExtractMachineId(long id)
    {
        return (id >> MachineIdShift) & MaxMachineId;
    }

    public static long ExtractSequence(long id)
    {
        return id & MaxSequence;
    }

    public static SnowflakeComponents Decode(long id)
    {
        return new SnowflakeComponents
        {
            Id = id,
            Timestamp = ExtractTimestamp(id),
            MachineId = ExtractMachineId(id),
            Sequence = ExtractSequence(id)
        };
    }
}

public record SnowflakeComponents
{
    public long Id { get; init; }
    public DateTimeOffset Timestamp { get; init; }
    public long MachineId { get; init; }
    public long Sequence { get; init; }

    public override string ToString() =>
        $"ID: {Id}, Created: {Timestamp:O}, Machine: {MachineId}, Seq: {Sequence}";
}
Enter fullscreen mode Exit fullscreen mode

Uso básico

// Cada instância/pod deve ter um machineId único
var generator = new SnowflakeIdGenerator(machineId: 1);

// Gerar IDs
var id1 = generator.NextId();
var id2 = generator.NextId();
var id3 = generator.NextId();

Console.WriteLine(id1); // 123456789012345678
Console.WriteLine(id2); // 123456789012345679
Console.WriteLine(id3); // 123456789012345680

// Decodificar um ID
var components = SnowflakeIdGenerator.Decode(id1);
Console.WriteLine(components);
// ID: 123456789012345678, Created: 2024-06-15T14:30:00Z, Machine: 1, Seq: 0
Enter fullscreen mode Exit fullscreen mode

Integração com ASP.NET Core

Registro como Serviço Singleton

public static class SnowflakeServiceExtensions
{
    public static IServiceCollection AddSnowflakeIdGenerator(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        var machineId = GetMachineId(configuration);
        var generator = new SnowflakeIdGenerator(machineId);

        services.AddSingleton(generator);
        services.AddSingleton<IIdGenerator>(provider => 
            new SnowflakeIdGeneratorAdapter(provider.GetRequiredService<SnowflakeIdGenerator>()));

        return services;
    }

    private static long GetMachineId(IConfiguration configuration)
    {
        // Prioridade: variável de ambiente > configuração > hash do hostname
        var envMachineId = Environment.GetEnvironmentVariable("SNOWFLAKE_MACHINE_ID");
        if (!string.IsNullOrEmpty(envMachineId) && long.TryParse(envMachineId, out var envId))
        {
            return envId;
        }

        var configMachineId = configuration.GetValue<long?>("Snowflake:MachineId");
        if (configMachineId.HasValue)
        {
            return configMachineId.Value;
        }

        // Fallback: usa hash do hostname (cuidado com colisões em produção!)
        var hostname = Environment.MachineName;
        return Math.Abs(hostname.GetHashCode()) % 1024;
    }
}

public interface IIdGenerator
{
    long NextId();
    string NextIdAsString();
}

public class SnowflakeIdGeneratorAdapter : IIdGenerator
{
    private readonly SnowflakeIdGenerator _generator;

    public SnowflakeIdGeneratorAdapter(SnowflakeIdGenerator generator)
    {
        _generator = generator;
    }

    public long NextId() => _generator.NextId();
    public string NextIdAsString() => _generator.NextId().ToString();
}
Enter fullscreen mode Exit fullscreen mode

Configuração no Program.cs

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSnowflakeIdGenerator(builder.Configuration);

var app = builder.Build();
Enter fullscreen mode Exit fullscreen mode

Com appsettings.json:

{
  "Snowflake": {
    "MachineId": 1
  }
}
Enter fullscreen mode Exit fullscreen mode

Ou via variável de ambiente no Kubernetes:

env:
  - name: SNOWFLAKE_MACHINE_ID
    valueFrom:
      fieldRef:
        fieldPath: metadata.annotations['snowflake-machine-id']
Enter fullscreen mode Exit fullscreen mode

Uso em entidades

public class Order
{
    public long Id { get; private set; }
    public string CustomerName { get; set; } = string.Empty;
    public decimal Total { get; set; }
    public DateTimeOffset CreatedAt { get; set; }

    public static Order Create(IIdGenerator idGenerator, string customerName, decimal total)
    {
        return new Order
        {
            Id = idGenerator.NextId(),
            CustomerName = customerName,
            Total = total,
            CreatedAt = DateTimeOffset.UtcNow
        };
    }
}

// No controller ou service
public class OrderService
{
    private readonly IIdGenerator _idGenerator;
    private readonly AppDbContext _context;

    public OrderService(IIdGenerator idGenerator, AppDbContext context)
    {
        _idGenerator = idGenerator;
        _context = context;
    }

    public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
    {
        var order = Order.Create(_idGenerator, request.CustomerName, request.Total);

        _context.Orders.Add(order);
        await _context.SaveChangesAsync();

        return order;
    }
}
Enter fullscreen mode Exit fullscreen mode

Implementação de alta performance

Para cenários de altíssima throughput, podemos eliminar o lock usando operações atômicas:

public sealed class LockFreeSnowflakeGenerator
{
    private static readonly DateTimeOffset CustomEpoch = 
        new(2024, 1, 1, 0, 0, 0, TimeSpan.Zero);

    private const int SequenceBits = 12;
    private const int MachineIdBits = 10;
    private const int MachineIdShift = SequenceBits;
    private const int TimestampShift = SequenceBits + MachineIdBits;
    private const long MaxSequence = (1L << SequenceBits) - 1;

    private readonly long _machineId;

    // Combina timestamp e sequence em um único valor atômico
    // [timestamp: 41 bits][sequence: 12 bits] = 53 bits, cabe em long
    private long _state;

    public LockFreeSnowflakeGenerator(long machineId)
    {
        if (machineId < 0 || machineId > 1023)
            throw new ArgumentOutOfRangeException(nameof(machineId));

        _machineId = machineId;
        _state = GetCurrentTimestamp() << SequenceBits;
    }

    public long NextId()
    {
        long oldState, newState, timestamp, sequence;

        do
        {
            oldState = Interlocked.Read(ref _state);
            var currentTimestamp = GetCurrentTimestamp();
            var oldTimestamp = oldState >> SequenceBits;
            var oldSequence = oldState & MaxSequence;

            if (currentTimestamp > oldTimestamp)
            {
                // Novo milissegundo
                timestamp = currentTimestamp;
                sequence = 0;
            }
            else if (currentTimestamp == oldTimestamp)
            {
                // Mesmo milissegundo
                sequence = oldSequence + 1;

                if (sequence > MaxSequence)
                {
                    // Overflow: força espera pelo próximo ms via spin
                    Thread.SpinWait(10);
                    continue;
                }

                timestamp = currentTimestamp;
            }
            else
            {
                // Clock regression: usa timestamp antigo + sequence
                timestamp = oldTimestamp;
                sequence = oldSequence + 1;

                if (sequence > MaxSequence)
                {
                    timestamp++;
                    sequence = 0;
                }
            }

            newState = (timestamp << SequenceBits) | sequence;

        } while (Interlocked.CompareExchange(ref _state, newState, oldState) != oldState);

        return (timestamp << TimestampShift) | (_machineId << MachineIdShift) | sequence;
    }

    private static long GetCurrentTimestamp()
    {
        return (long)(DateTimeOffset.UtcNow - CustomEpoch).TotalMilliseconds;
    }
}
Enter fullscreen mode Exit fullscreen mode

Benchmark Comparativo

[MemoryDiagnoser]
public class SnowflakeBenchmarks
{
    private readonly SnowflakeIdGenerator _locked = new(1);
    private readonly LockFreeSnowflakeGenerator _lockFree = new(1);
    private readonly Guid _guid = Guid.NewGuid();

    [Benchmark(Baseline = true)]
    public long SnowflakeLocked() => _locked.NextId();

    [Benchmark]
    public long SnowflakeLockFree() => _lockFree.NextId();

    [Benchmark]
    public Guid GuidNewGuid() => Guid.NewGuid();
}
Enter fullscreen mode Exit fullscreen mode

Resultados típicos (depende do hardware):

Método Média Alocação
SnowflakeLocked ~50 ns 0 B
SnowflakeLockFree ~25 ns 0 B
Guid.NewGuid ~35 ns 0 B

Atribuição de Machine ID em Kubernetes

O maior desafio do Snowflake em ambientes containerizados é garantir que cada pod tenha um Machine ID único. Existem várias estratégias:

1. StatefulSet com Ordinal Index

public static long GetMachineIdFromPodName()
{
    // Pod name em StatefulSet: myapp-0, myapp-1, myapp-2...
    var podName = Environment.GetEnvironmentVariable("POD_NAME") ?? "pod-0";
    var parts = podName.Split('-');

    if (parts.Length > 0 && int.TryParse(parts[^1], out var ordinal))
    {
        return ordinal % 1024;
    }

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Kubernetes manifest:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: myapp
spec:
  replicas: 3
  template:
    spec:
      containers:
        - name: app
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
Enter fullscreen mode Exit fullscreen mode

2. Registro Dinâmico com Redis

Para Deployments (não StatefulSet), pods podem "alugar" um Machine ID:

public class RedisMachineIdProvider
{
    private readonly IDatabase _redis;
    private readonly string _serviceName;
    private readonly TimeSpan _leaseDuration;
    private readonly string _instanceId;

    private long _machineId = -1;
    private Timer? _renewalTimer;

    public RedisMachineIdProvider(
        IConnectionMultiplexer connection,
        string serviceName,
        TimeSpan? leaseDuration = null)
    {
        _redis = connection.GetDatabase();
        _serviceName = serviceName;
        _leaseDuration = leaseDuration ?? TimeSpan.FromMinutes(5);
        _instanceId = Guid.NewGuid().ToString("N");
    }

    public async Task<long> AcquireMachineIdAsync()
    {
        for (long candidateId = 0; candidateId < 1024; candidateId++)
        {
            var key = $"snowflake:{_serviceName}:machine:{candidateId}";

            var acquired = await _redis.StringSetAsync(
                key,
                _instanceId,
                _leaseDuration,
                When.NotExists);

            if (acquired)
            {
                _machineId = candidateId;
                StartLeaseRenewal();
                return candidateId;
            }

            // Verifica se o lease expirou (cleanup de instância morta)
            var currentHolder = await _redis.StringGetAsync(key);
            if (!currentHolder.HasValue)
            {
                continue; // Tenta novamente
            }
        }

        throw new InvalidOperationException(
            "No available machine ID. All 1024 slots are in use.");
    }

    private void StartLeaseRenewal()
    {
        var renewalInterval = _leaseDuration / 2;

        _renewalTimer = new Timer(async _ =>
        {
            var key = $"snowflake:{_serviceName}:machine:{_machineId}";
            await _redis.KeyExpireAsync(key, _leaseDuration);
        }, null, renewalInterval, renewalInterval);
    }

    public async Task ReleaseMachineIdAsync()
    {
        _renewalTimer?.Dispose();

        if (_machineId >= 0)
        {
            var key = $"snowflake:{_serviceName}:machine:{_machineId}";
            await _redis.KeyDeleteAsync(key);
        }
    }
}

// Uso com IHostedService para lifecycle management
public class SnowflakeInitializerService : IHostedService
{
    private readonly RedisMachineIdProvider _machineIdProvider;
    private readonly SnowflakeIdGeneratorHolder _holder;

    public SnowflakeInitializerService(
        RedisMachineIdProvider machineIdProvider,
        SnowflakeIdGeneratorHolder holder)
    {
        _machineIdProvider = machineIdProvider;
        _holder = holder;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        var machineId = await _machineIdProvider.AcquireMachineIdAsync();
        _holder.Generator = new SnowflakeIdGenerator(machineId);
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        await _machineIdProvider.ReleaseMachineIdAsync();
    }
}

// Holder para permitir inicialização tardia
public class SnowflakeIdGeneratorHolder
{
    public SnowflakeIdGenerator? Generator { get; set; }

    public long NextId() => Generator?.NextId() 
        ?? throw new InvalidOperationException("Generator not initialized");
}
Enter fullscreen mode Exit fullscreen mode

3. Baseado em IP do Pod

Uma abordagem simples que funciona bem para clusters pequenos:

public static long GetMachineIdFromPodIp()
{
    var podIp = Environment.GetEnvironmentVariable("POD_IP");

    if (string.IsNullOrEmpty(podIp))
    {
        // Fallback: obtém IP da interface de rede
        podIp = Dns.GetHostAddresses(Dns.GetHostName())
            .FirstOrDefault(ip => ip.AddressFamily == AddressFamily.InterNetwork)?
            .ToString() ?? "127.0.0.1";
    }

    // Usa os últimos 10 bits do IP (último octeto + parte do penúltimo)
    var parts = podIp.Split('.');
    if (parts.Length == 4)
    {
        var lastOctet = int.Parse(parts[3]);
        var secondLastOctet = int.Parse(parts[2]);

        // Combina para formar um ID de 10 bits
        return ((secondLastOctet & 0x03) << 8) | lastOctet;
    }

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Variações do Snowflake

Diferentes empresas adaptaram o Snowflake para suas necessidades:

Discord (Epoch: 2015)

public class DiscordSnowflake
{
    // Discord usa epoch de 1 de Janeiro de 2015
    private static readonly DateTimeOffset DiscordEpoch = 
        new(2015, 1, 1, 0, 0, 0, TimeSpan.Zero);

    // Estrutura: timestamp(42) + worker(5) + process(5) + sequence(12)
    private const int WorkerIdBits = 5;
    private const int ProcessIdBits = 5;
    private const int SequenceBits = 12;

    // ... implementação similar
}
Enter fullscreen mode Exit fullscreen mode

Instagram (Sharded)

public class InstagramSnowflake
{
    // Instagram: timestamp(41) + shard_id(13) + sequence(10)
    // Mais shards, menos IDs por milissegundo por shard
    private const int ShardIdBits = 13;    // 8192 shards
    private const int SequenceBits = 10;   // 1024 IDs/ms/shard

    private readonly long _shardId;

    public InstagramSnowflake(long shardId)
    {
        // Shard ID tipicamente derivado do user_id % num_shards
        _shardId = shardId & ((1L << ShardIdBits) - 1);
    }
}
Enter fullscreen mode Exit fullscreen mode

Sony (Sonyflake)

public class Sonyflake
{
    // Sonyflake: timestamp em unidades de 10ms para durar mais tempo
    // timestamp(39) + sequence(8) + machine_id(16)
    // Dura ~174 anos, mas apenas 256 IDs por 10ms por máquina

    private static readonly DateTimeOffset SonyEpoch = 
        new(2014, 9, 1, 0, 0, 0, TimeSpan.Zero);

    private const int SequenceBits = 8;
    private const int MachineIdBits = 16;

    private static long GetCurrentTimestamp()
    {
        // Unidades de 10ms ao invés de 1ms
        return (long)(DateTimeOffset.UtcNow - SonyEpoch).TotalMilliseconds / 10;
    }
}
Enter fullscreen mode Exit fullscreen mode

UUID v7: O Padrão Moderno

Em 2024, a RFC 9562 formalizou o UUID v7, uma alternativa padronizada que resolve os mesmos problemas do Snowflake. Se você está começando um projeto novo, vale considerar essa opção.

Anatomia do UUID v7

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
├───────────────────────────────────────────────────────────────┤
│                          unix_ts_ms                           │
├───────────────────────────────────────────────────────────────┤
│           unix_ts_ms          │  ver  │       rand_a          │
├───────────────────────────────────────────────────────────────┤
│var│                        rand_b                             │
├───────────────────────────────────────────────────────────────┤
│                            rand_b                             │
└───────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode
Segmento Bits Descrição
unix_ts_ms 48 Timestamp Unix em milissegundos
ver 4 Versão (sempre 7)
rand_a 12 Dados aleatórios ou sub-milissegundo
var 2 Variante (sempre 10)
rand_b 62 Dados aleatórios ou contador

Total: 128 bits (vs 64 do Snowflake)

Implementação em C

A partir do .NET 9, UUID v7 está disponível nativamente:

// .NET 9+
var id = Guid.CreateVersion7();
var idWithTimestamp = Guid.CreateVersion7(DateTimeOffset.UtcNow);
Enter fullscreen mode Exit fullscreen mode

Para versões anteriores do .NET, uma implementação manual:

public static class UuidV7
{
    private static long _lastTimestamp;
    private static int _counter;
    private static readonly object _lock = new();
    private static readonly Random _random = Random.Shared;

    public static Guid NewUuid()
    {
        Span<byte> bytes = stackalloc byte[16];

        lock (_lock)
        {
            var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

            // Monotonic counter para mesmo milissegundo
            if (timestamp == _lastTimestamp)
            {
                _counter++;
            }
            else
            {
                _lastTimestamp = timestamp;
                _counter = _random.Next(0, 0xFFF); // Randomiza início do counter
            }

            // Bytes 0-5: timestamp (48 bits, big-endian)
            bytes[0] = (byte)(timestamp >> 40);
            bytes[1] = (byte)(timestamp >> 32);
            bytes[2] = (byte)(timestamp >> 24);
            bytes[3] = (byte)(timestamp >> 16);
            bytes[4] = (byte)(timestamp >> 8);
            bytes[5] = (byte)timestamp;

            // Bytes 6-7: version (4 bits) + rand_a/counter (12 bits)
            var randA = (ushort)((_counter & 0xFFF) | 0x7000); // Version 7
            bytes[6] = (byte)(randA >> 8);
            bytes[7] = (byte)randA;

            // Bytes 8-15: variant (2 bits) + rand_b (62 bits)
            _random.NextBytes(bytes.Slice(8, 8));
            bytes[8] = (byte)((bytes[8] & 0x3F) | 0x80); // Variant 10
        }

        return new Guid(bytes, bigEndian: true);
    }

    public static DateTimeOffset ExtractTimestamp(Guid uuid)
    {
        Span<byte> bytes = stackalloc byte[16];
        uuid.TryWriteBytes(bytes, bigEndian: true, out _);

        long timestamp = ((long)bytes[0] << 40) |
                         ((long)bytes[1] << 32) |
                         ((long)bytes[2] << 24) |
                         ((long)bytes[3] << 16) |
                         ((long)bytes[4] << 8) |
                         bytes[5];

        return DateTimeOffset.FromUnixTimeMilliseconds(timestamp);
    }

    public static bool IsVersion7(Guid uuid)
    {
        Span<byte> bytes = stackalloc byte[16];
        uuid.TryWriteBytes(bytes, bigEndian: true, out _);

        var version = (bytes[6] >> 4) & 0x0F;
        var variant = (bytes[8] >> 6) & 0x03;

        return version == 7 && variant == 2;
    }
}
Enter fullscreen mode Exit fullscreen mode

Implementação de Alta Performance

Para cenários que exigem máxima performance, eliminando alocações:

public sealed class UuidV7Generator
{
    private long _lastTimestamp;
    private long _counter;

    public Guid NewUuid()
    {
        var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
        long counter;

        // Operação atômica para timestamp + counter
        while (true)
        {
            var oldTimestamp = Interlocked.Read(ref _lastTimestamp);
            var oldCounter = Interlocked.Read(ref _counter);

            if (timestamp > oldTimestamp)
            {
                if (Interlocked.CompareExchange(ref _lastTimestamp, timestamp, oldTimestamp) == oldTimestamp)
                {
                    counter = Random.Shared.NextInt64(0, 0xFFF);
                    Interlocked.Exchange(ref _counter, counter);
                    break;
                }
            }
            else
            {
                counter = Interlocked.Increment(ref _counter) & 0xFFF;
                if (counter != 0)
                {
                    timestamp = oldTimestamp;
                    break;
                }
                // Counter overflow: aguarda próximo ms
                Thread.SpinWait(100);
                timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
            }
        }

        return CreateUuid(timestamp, counter);
    }

    private static Guid CreateUuid(long timestamp, long counter)
    {
        Span<byte> bytes = stackalloc byte[16];

        // Timestamp (48 bits)
        bytes[0] = (byte)(timestamp >> 40);
        bytes[1] = (byte)(timestamp >> 32);
        bytes[2] = (byte)(timestamp >> 24);
        bytes[3] = (byte)(timestamp >> 16);
        bytes[4] = (byte)(timestamp >> 8);
        bytes[5] = (byte)timestamp;

        // Version 7 + counter
        bytes[6] = (byte)(0x70 | ((counter >> 8) & 0x0F));
        bytes[7] = (byte)counter;

        // Variant + random
        Span<byte> randomBytes = bytes.Slice(8, 8);
        Random.Shared.NextBytes(randomBytes);
        bytes[8] = (byte)((bytes[8] & 0x3F) | 0x80);

        return new Guid(bytes, bigEndian: true);
    }
}
Enter fullscreen mode Exit fullscreen mode

Uso Prático

// Geração simples
var id1 = UuidV7.NewUuid();
var id2 = UuidV7.NewUuid();

Console.WriteLine(id1); // 018f5e4c-7b2a-7123-8456-789abcdef012
Console.WriteLine(id2); // 018f5e4c-7b2a-7124-8def-123456789abc

// IDs são ordenáveis!
Console.WriteLine(id1 < id2); // True (na maioria dos casos)

// Extrair timestamp
var created = UuidV7.ExtractTimestamp(id1);
Console.WriteLine(created); // 2024-06-15T14:30:00.000+00:00

// Validar versão
Console.WriteLine(UuidV7.IsVersion7(id1)); // True
Console.WriteLine(UuidV7.IsVersion7(Guid.NewGuid())); // False (v4)
Enter fullscreen mode Exit fullscreen mode

Entity Framework Core com UUID v7

public class Order
{
    public Guid Id { get; private set; }
    public string CustomerName { get; set; } = string.Empty;
    public decimal Total { get; set; }

    private Order() { }

    public static Order Create(string customerName, decimal total)
    {
        return new Order
        {
            Id = UuidV7.NewUuid(),
            CustomerName = customerName,
            Total = total
        };
    }
}

// DbContext configuration
public class AppDbContext : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Order>(entity =>
        {
            entity.HasKey(e => e.Id);

            // UUID v7 é ordenável, então funciona bem como clustered index
            entity.HasIndex(e => e.Id).IsClustered();
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Snowflake vs UUID v7: Comparação Detalhada

Aspecto Snowflake (64 bits) UUID v7 (128 bits)
Tamanho 8 bytes 16 bytes
Ordenação Perfeita (timestamp nos bits altos) Quase perfeita (mesmo ms pode variar)
Unicidade Requer Machine ID único Aleatoriedade garante unicidade
Coordenação Precisa distribuir Machine IDs Nenhuma
Timestamp Epoch customizado (69 anos) Unix epoch (até ano 10889)
Extração de metadata Timestamp + Machine + Sequence Apenas timestamp
Padrão Proprietário (Twitter) RFC 9562
Suporte nativo Não .NET 9+
Performance índice DB Excelente Bom (2x tamanho)
JavaScript safe Não (>53 bits) Sim (string nativa)

Quando Escolher Cada Um

Escolha Snowflake quando:

  • Tamanho do ID é crítico (8 vs 16 bytes faz diferença em bilhões de registros)
  • Precisa identificar a origem do ID (Machine ID)
  • Quer garantia absoluta de ordenação monotônica
  • Já tem infraestrutura para distribuir Machine IDs

Escolha UUID v7 quando:

  • Não quer gerenciar Machine IDs
  • Prefere usar padrões estabelecidos (RFC)
  • Interoperabilidade com sistemas que esperam UUIDs
  • Está no .NET 9+ e quer solução nativa
  • Precisa de compatibilidade com JavaScript sem conversão

Migração: Snowflake para UUID v7

Se você tem um sistema com Snowflake e quer migrar:

public static class SnowflakeToUuidV7Converter
{
    public static Guid ConvertToUuidV7(long snowflakeId, DateTimeOffset snowflakeEpoch)
    {
        // Extrai timestamp do Snowflake
        var timestampMs = snowflakeId >> 22; // Assumindo 22 bits para machine+sequence
        var unixTimestampMs = (long)(snowflakeEpoch.ToUnixTimeMilliseconds() + timestampMs);

        // Extrai sequence para usar como parte do UUID
        var sequence = snowflakeId & 0xFFF;

        Span<byte> bytes = stackalloc byte[16];

        // Timestamp
        bytes[0] = (byte)(unixTimestampMs >> 40);
        bytes[1] = (byte)(unixTimestampMs >> 32);
        bytes[2] = (byte)(unixTimestampMs >> 24);
        bytes[3] = (byte)(unixTimestampMs >> 16);
        bytes[4] = (byte)(unixTimestampMs >> 8);
        bytes[5] = (byte)unixTimestampMs;

        // Version 7 + sequence original
        bytes[6] = (byte)(0x70 | ((sequence >> 8) & 0x0F));
        bytes[7] = (byte)sequence;

        // Variant + Machine ID nos bytes restantes (preserva rastreabilidade)
        var machineId = (snowflakeId >> 12) & 0x3FF;
        bytes[8] = (byte)(0x80 | ((machineId >> 4) & 0x3F));
        bytes[9] = (byte)((machineId << 4) & 0xF0);

        // Preenche resto com zeros (ou hash determinístico)
        bytes[10] = bytes[11] = bytes[12] = bytes[13] = bytes[14] = bytes[15] = 0;

        return new Guid(bytes, bigEndian: true);
    }
}
Enter fullscreen mode Exit fullscreen mode

Benchmark: Snowflake vs UUID v7 vs UUID v4

[MemoryDiagnoser]
public class IdGeneratorBenchmarks
{
    private readonly SnowflakeIdGenerator _snowflake = new(1);
    private readonly UuidV7Generator _uuidV7 = new();

    [Benchmark(Baseline = true)]
    public long Snowflake() => _snowflake.NextId();

    [Benchmark]
    public Guid UuidV7() => _uuidV7.NewUuid();

    [Benchmark]
    public Guid UuidV4() => Guid.NewGuid();

    // .NET 9+ apenas
    // [Benchmark]
    // public Guid UuidV7Native() => Guid.CreateVersion7();
}
Enter fullscreen mode Exit fullscreen mode

Resultados típicos:

Método Média Alocação
Snowflake ~25 ns 0 B
UuidV7 (custom) ~45 ns 0 B
UuidV4 ~35 ns 0 B
UuidV7 (.NET 9) ~30 ns 0 B

O Snowflake é mais rápido, mas UUID v7 tem a vantagem de não precisar de coordenação.

Considerações e Limitações

Clock Drift

O maior inimigo do Snowflake é o clock do sistema voltar no tempo (NTP sync, VM migration, etc.):

// Estratégias para lidar com clock regression:

// 1. Lançar exceção (implementação padrão)
if (timestamp < _lastTimestamp)
{
    throw new InvalidOperationException("Clock moved backwards");
}

// 2. Aguardar o tempo "alcançar" (pode bloquear por muito tempo)
while (timestamp < _lastTimestamp)
{
    Thread.Sleep(1);
    timestamp = GetCurrentTimestamp();
}

// 3. Usar timestamp antigo e incrementar sequence (nossa implementação lock-free)
if (currentTimestamp < oldTimestamp)
{
    timestamp = oldTimestamp;
    sequence = oldSequence + 1;
}
Enter fullscreen mode Exit fullscreen mode

Limite de 69 Anos

Com epoch em 2024, os IDs funcionam até ~2093. Para sistemas que precisam durar mais:

// Opção 1: Usar epoch mais recente
private static readonly DateTimeOffset CustomEpoch = 
    new(2024, 1, 1, 0, 0, 0, TimeSpan.Zero); // Válido até ~2093

// Opção 2: Reduzir bits de sequence/machine para mais timestamp
// timestamp(43) + machine(8) + sequence(12) = ~278 anos
Enter fullscreen mode Exit fullscreen mode

IDs em URLs

Snowflake IDs são números grandes que podem ser problemáticos em JavaScript (limite de 53 bits para inteiros seguros):

// Solução 1: Retornar como string na API
[JsonConverter(typeof(LongToStringConverter))]
public long Id { get; set; }

// Solução 2: Usar Base62 encoding
public static class Base62
{
    private const string Alphabet = 
        "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

    public static string Encode(long value)
    {
        if (value == 0) return "0";

        var result = new StringBuilder();

        while (value > 0)
        {
            result.Insert(0, Alphabet[(int)(value % 62)]);
            value /= 62;
        }

        return result.ToString();
    }

    public static long Decode(string encoded)
    {
        return encoded.Aggregate(0L, (current, c) => 
            current * 62 + Alphabet.IndexOf(c));
    }
}

// 123456789012345678 -> "2aZrWMN8Hy" (11 chars vs 18 dígitos)
Enter fullscreen mode Exit fullscreen mode

Quando Usar Cada Abordagem

Use Snowflake quando:

  • Tamanho do ID é crítico (8 vs 16 bytes importa em bilhões de registros)
  • Precisa identificar a origem do ID (Machine ID embutido)
  • Quer garantia absoluta de ordenação monotônica
  • Já tem infraestrutura para distribuir Machine IDs
  • Performance de geração é o fator mais crítico

Use UUID v7 quando:

  • Não quer gerenciar Machine IDs
  • Prefere usar padrões estabelecidos (RFC 9562)
  • Interoperabilidade com sistemas que esperam UUIDs
  • Está no .NET 9+ e quer solução nativa
  • Precisa de compatibilidade com JavaScript sem conversão
  • Simplicidade de implementação é prioridade

Use UUID v4 quando:

  • Não precisa de ordenação temporal
  • Quer IDs completamente aleatórios
  • Segurança é prioridade (IDs não revelam timestamp)

Use auto-increment quando:

  • Sistema é simples com um único banco de dados
  • Não precisa gerar IDs fora do banco
  • Sequencialidade absoluta é necessária

Conclusão

O Snowflake é uma solução elegante para um problema complexo. Sua estrutura simples de 64 bits resolve simultaneamente unicidade, ordenação e performance, sem necessidade de coordenação central.

A implementação não é difícil, mas os detalhes importam: tratamento de clock drift, atribuição de Machine ID em containers, e configuração do epoch. Com esses cuidados, você terá um sistema de geração de IDs que escala para bilhões de registros.

O UUID v7, padronizado em 2024, oferece uma alternativa interessante. Troca o tamanho compacto do Snowflake pela simplicidade de não precisar gerenciar Machine IDs. Para novos projetos, especialmente no .NET 9+, é uma opção a considerar seriamente.

A escolha entre Snowflake e UUID v7 depende do contexto: se você precisa de IDs menores e pode gerenciar a infraestrutura de Machine IDs, Snowflake é imbatível. Se prefere simplicidade e padronização, UUID v7 entrega praticamente os mesmos benefícios com menos complexidade operacional.

Ambos os algoritmos têm mais de uma década de validação em produção (Snowflake no Twitter, UUID v7 baseado em implementações similares) e continuam relevantes porque acertaram nos trade-offs fundamentais. Às vezes, as melhores soluções são as mais simples.

Top comments (0)