Cenários de Aplicação
Em sistemas distribuídos como e-commerce, redes sociais, plataformas financeiras, bancos de dados distribuídos e caches, e aplicações de Internet das Coisas, é essencial gerar identificadores únicos globais. Esses IDs garantem a unicidade, consistência, segurança e eficiência dos dados, sendo componentes fundamentais no desenvolvimento de software moderno.
Métodos Comuns para Geração de IDs
As soluções devem ser ajustadas conforme os requisitos específicos do projeto.
1. UUID (Identificador Único Universal)
O UUID é um identificador único representado por 32 dígitos hexadecimais, formando uma string de 36 caracteres, incluindo hífens. A versão 4, baseada em números aleatórios, é a mais utilizada em Java, embora outras versões existam com algoritmos diferentes. Apesar de ser uma solução local sem consumo de rede, o UUID apresenta desvantagens como tamanho elevado, dificuldade de armazenamento, riscos de segurança ao expor endereços MAC (na versão 1), e ineficiência quando usado como chave primária em bancos de dados devido à falta de ordenação e tamanho excessivo.
public class GeradorUUID {
public static void main(String[] args) {
// Gera um UUID aleatório (versão 4)
java.util.UUID idAleatorio = java.util.UUID.randomUUID();
String idFormatado = idAleatorio.toString().replace("-", "");
System.out.println("UUID aleatório: " + idFormatado);
// Gera um UUID baseado em nome (versão 3)
byte[] dados = {10, 20, 30};
java.util.UUID idBaseadoEmNome = java.util.UUID.nameUUIDFromBytes(dados);
System.out.println("UUID baseado em nome: " + idBaseadoEmNome.toString().replace("-", ""));
}
}
2. Auto-incremento do Banco de Dados
Este método é amplamente utilizado em bancos de dados como MySQL, onde colunas são definidas com auto-incremento. É simples de implementar em projetos monolíticos e gera IDs ordenados. No entanto, apresenta limitações significativas em ambientes distribuídos: risco de ponto único de falha, dificuldade de escalabilidade, problemas durante migrações ou divisão de tabelas, e gargalos de desempenho devido à dependência em um único servidor de banco de dados.
3. Geração de IDs com Redis
O Redis pode ser utilizado para criar IDs únicos distribuídos, evitando a previsibilidade de auto-incrementos simples e suportando cenários com múltiplas aplicações. Abaixo, são apresentadas duas abordagens comuns:
Uso de Chaves de Contagem no Redis
A estrutura String do Redis permite incrementos atômicos, servindo como contador distribuído. Esta abordagem é eficiente e segura, pois o Redis opera de forma single-thread.
# Exemplo de comando para incrementar um contador no Redis
# A chave "CONTADOR:PEDIDO" é incrementada em 1, criando-a se não existir.
127.0.0.1:6379> INCR CONTADOR:PEDIDO
(integer) 1
Implementação de uma Ferramenta de ID com Redis
Para uso em produção, é recomendável combinar identificadores de negócio, timestamps e sequências para garantir unicidade. A seguir, uma implementação em Java que utiliza Redis para gerar IDs com chave de contagem diária.
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.time.LocalDate;
import java.util.concurrent.TimeUnit;
@Component
public class GeradorIdRedis {
@Resource
private RedisTemplate<String, Object> clienteRedis;
private final String PREFIXO_CHAVE = "CONTADOR_ID:";
public String criarIdentificador(String nomeNegocio) {
long sequencia = obterSequencia(nomeNegocio);
return nomeNegocio + System.currentTimeMillis() + "-" + sequencia;
}
private long obterSequencia(String nomeNegocio) {
String chaveRedis = construirChave(nomeNegocio);
long valor = 0L;
if (!Boolean.TRUE.equals(clienteRedis.hasKey(chaveRedis))) {
String chaveLock = chaveRedis + "_LOCK";
boolean lockAdquirido = Boolean.TRUE.equals(
clienteRedis.opsForValue().setIfAbsent(chaveLock, 1, 30, TimeUnit.SECONDS)
);
if (!lockAdquirido) {
return obterSequencia(nomeNegocio); // Tentativa recursiva
}
valor = clienteRedis.opsForValue().increment(chaveRedis);
clienteRedis.expire(chaveRedis, 24, TimeUnit.HOURS);
clienteRedis.delete(chaveLock);
} else {
valor = clienteRedis.opsForValue().increment(chaveRedis);
}
return valor;
}
private String construirChave(String nomeNegocio) {
LocalDate hoje = LocalDate.now();
String dataFormatada = String.format("%d%02d%02d", hoje.getYear(), hoje.getMonthValue(), hoje.getDayOfMonth());
return PREFIXO_CHAVE + nomeNegocio + ":" + dataFormatada;
}
}
Este código inclui tratamento para concorrência usando locks distribuídos, embora simplificado para demonstração. Em ambientes de alta demanda, ajustes finos podem ser necessários.
4. Algoritmo Snowflake
O Snowflake gera IDs de 64 bits, compostos por: um bit de sinal (sempre 0), 41 bits para timestamp (em milissegundos, permitindo cerca de 69 anos), 10 bits para identificação de máquina (divididos em datacenter e worker), e 12 bits para sequência (até 4095 IDs por milissegundo por máquina). O principal desafio é a sensibilidade a ajustes de tempo do sistema.
Implemantação Personalizada
Abaixo, uma classe Java que implementa o algoritmo Snowflkae com tratamento para retrocesso de tempo.
public class GeradorSnowflake {
private final long epocaInicial = 1681452025134L; // Timestamp de referência
private final long bitsWorkerId = 5L;
private final long bitsDatacenterId = 5L;
private final long bitsSequencia = 12L;
private final long maxWorkerId = ~(-1L << bitsWorkerId);
private final long maxDatacenterId = ~(-1L << bitsDatacenterId);
private final long mascaraSequencia = ~(-1L << bitsSequencia);
private final long deslocamentoWorkerId = bitsSequencia;
private final long deslocamentoDatacenterId = bitsSequencia + bitsWorkerId;
private final long deslocamentoTimestamp = bitsSequencia + bitsWorkerId + bitsDatacenterId;
private long workerId;
private long datacenterId;
private long sequencia = 0L;
private long ultimoTimestamp = -1L;
public GeradorSnowflake(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException("Worker ID fora do intervalo válido.");
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException("Datacenter ID fora do intervalo válido.");
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
public synchronized long gerarId() {
long timestampAtual = System.currentTimeMillis();
if (timestampAtual < ultimoTimestamp) {
throw new RuntimeException("Retrocesso de tempo detectado.");
}
if (timestampAtual == ultimoTimestamp) {
sequencia = (sequencia + 1) & mascaraSequencia;
if (sequencia == 0) {
timestampAtual = aguardarProximoMilissegundo(ultimoTimestamp);
}
} else {
sequencia = 0;
}
ultimoTimestamp = timestampAtual;
return ((timestampAtual - epocaInicial) << deslocamentoTimestamp) |
(datacenterId << deslocamentoDatacenterId) |
(workerId << deslocamentoWorkerId) |
sequencia;
}
private long aguardarProximoMilissegundo(long ultimoTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= ultimoTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
public static void main(String[] args) {
GeradorSnowflake gerador = new GeradorSnowflake(1, 1);
for (int i = 0; i < 10; i++) {
System.out.println("ID gerado: " + gerador.gerarId());
}
}
}
Uso da Biblioteca Hutool
A biblioteca Hutool oferece uma implementação prática do Snowflake. Exemplo de integração:
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
public class GeradorComHutool {
private final Snowflake snowflake;
public GeradorComHutool(long workerId, long datacenterId) {
this.snowflake = IdUtil.createSnowflake(workerId, datacenterId);
}
public long obterProximoId() {
return snowflake.nextId();
}
public static void main(String[] args) {
GeradorComHutool gerador = new GeradorComHutool(1, 1);
for (int i = 0; i < 5; i++) {
System.out.println("ID do Hutool: " + gerador.obterProximoId());
}
}
}
Perda de Precisão no Frontend
IDs do tipo long gerados em Java podem sofrer perda de precisão em JavaScript, pois números em JS são limitados a 53 bits. Para evitar isso, é recomendável transmitir esses IDs como strings para o frontend.