← Назад к вопросам

Как спроектировать систему для присвоения уникальных номеров на запрос при масштабировании

3.0 Senior🔥 91 комментариев
#REST API и микросервисы

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как спроектировать систему для присвоения уникальных номеров на запрос при масштабировании

Это архитектурная задача о генерации уникальных ID в распределённой системе. При масштабировании приложения (множество серверов) обычная автоинкрементация БД не работает. Расскажу о проверенных подходах.

Проблема с простой автоинкрементацией

// Плохо в распределённой системе:
@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id; // AUTO_INCREMENT работает только локально!
    
    // ...
}

Проблемы:

  • Коллизии ID если несколько сервисов одновременно создают записи
  • Зависимость от одной БД
  • Единая точка отказа

1. UUID (Universally Unique Identifier)

UUID — самый простой способ генерировать глобально уникальные идентификаторы.

import java.util.UUID;

@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id; // или String
    
    private String orderNumber; // Если нужен читаемый номер
    private LocalDateTime createdAt;
}

@Service
public class OrderService {
    @Autowired
    private OrderRepository orderRepository;
    
    @Transactional
    public Order createOrder(OrderRequest request) {
        Order order = new Order();
        order.setId(UUID.randomUUID()); // Автоматический UUID
        order.setOrderNumber(generateOrderNumber());
        order.setCreatedAt(LocalDateTime.now());
        
        return orderRepository.save(order);
    }
    
    private String generateOrderNumber() {
        // ORD-2024-3a4b5c6d
        return "ORD-" + LocalDate.now().getYear() + "-" + 
               UUID.randomUUID().toString().substring(0, 8).toUpperCase();
    }
}

Преимущества:

  • Генерируется на клиенте, без обращения к БД
  • Гарантированно уникален
  • Стандартный формат

Недостатки:

  • Большой размер (128 бит)
  • Не читаемый человеком
  • Не последовательный (медленнее индексирование)

2. Snowflake ID

Snowflake (от Twitter) — распределённый ID из 64 бит с временной меткой, datacenter ID и sequence ID.

import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;

@Configuration
public class SnowflakeConfig {
    private final Snowflake snowflake = IdUtil.getSnowflake(1, 1); // workerId=1, datacenterId=1
    
    @Bean
    public IdGenerator idGenerator() {
        return () -> snowflake.nextId();
    }
}

@FunctionalInterface
public interface IdGenerator {
    long generateId();
}

@Service
public class OrderService {
    @Autowired
    private IdGenerator idGenerator;
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Transactional
    public Order createOrder(OrderRequest request) {
        Order order = new Order();
        order.setId(idGenerator.generateId()); // Snowflake ID
        order.setOrderNumber("ORD-" + order.getId());
        order.setCreatedAt(LocalDateTime.now());
        
        return orderRepository.save(order);
    }
}

@Entity
@Table(name = "orders")
public class Order {
    @Id
    private Long id; // 64-битный ID от Snowflake
    
    private String orderNumber;
    private LocalDateTime createdAt;
}

Структура Snowflake:

  • 1 бит: знак (всегда 0)
  • 41 бит: timestamp (миллисекунды)
  • 5 бит: datacenter ID (0-31)
  • 5 бит: worker ID (0-31)
  • 12 бит: sequence (0-4095)

Преимущества:

  • Компактный (64 бита)
  • Сортируемый по времени
  • Уникальный в распределённой системе
  • Быстро генерируется

3. Twitter Snowflake на Java

public class SnowflakeIdGenerator {
    private final long epoch = 1609459200000L; // 2021-01-01 in ms
    private final long workerId;
    private final long datacenterId;
    private final long maxWorkerId = 31; // 5 bits
    private final long maxDatacenterId = 31; // 5 bits
    
    private long lastTimestamp = -1L;
    private long sequence = 0L;
    private final long maxSequence = 4095; // 12 bits
    
    private final Object lock = new Object();
    
    public SnowflakeIdGenerator(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException("Worker ID must be between 0 and 31");
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException("Datacenter ID must be between 0 and 31");
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }
    
    public long generateId() {
        synchronized (lock) {
            long currentTimestamp = System.currentTimeMillis();
            
            if (currentTimestamp == lastTimestamp) {
                sequence = (sequence + 1) & maxSequence;
                if (sequence == 0) {
                    // Overflow, wait for next millisecond
                    currentTimestamp = waitUntilNextMillis(lastTimestamp);
                }
            } else if (currentTimestamp > lastTimestamp) {
                sequence = 0;
            } else {
                throw new RuntimeException("Clock moved backwards!");
            }
            
            lastTimestamp = currentTimestamp;
            
            long timeBits = (currentTimestamp - epoch) << 22;
            long datacenterBits = datacenterId << 17;
            long workerBits = workerId << 12;
            
            return timeBits | datacenterBits | workerBits | sequence;
        }
    }
    
    private long waitUntilNextMillis(long lastTimestamp) {
        long timestamp = System.currentTimeMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = System.currentTimeMillis();
        }
        return timestamp;
    }
}

@Configuration
public class IdGeneratorConfig {
    @Bean
    public SnowflakeIdGenerator snowflakeIdGenerator() {
        // Server 1: workerId=1, Server 2: workerId=2, etc.
        long workerId = getWorkerId();
        long datacenterId = getDatacenterId();
        return new SnowflakeIdGenerator(workerId, datacenterId);
    }
    
    private long getWorkerId() {
        // Из конфига или переменной окружения
        return Long.parseLong(System.getenv().getOrDefault("WORKER_ID", "1"));
    }
    
    private long getDatacenterId() {
        return Long.parseLong(System.getenv().getOrDefault("DATACENTER_ID", "1"));
    }
}

4. ULID (Universally Unique Lexicographically Sortable Identifier)

ULID — современная альтернатива UUID, более компактная и сортируемая.

import de.huxhorn.sulky.ulid.ULID;
import de.huxhorn.sulky.ulid.ULIDGenerator;

@Configuration
public class UlidConfig {
    @Bean
    public ULIDGenerator ulidGenerator() {
        return new ULIDGenerator();
    }
}

@Service
public class OrderService {
    @Autowired
    private ULIDGenerator ulidGenerator;
    
    @Transactional
    public Order createOrder(OrderRequest request) {
        Order order = new Order();
        order.setId(ulidGenerator.generateULID()); // ULID
        order.setCreatedAt(LocalDateTime.now());
        
        return orderRepository.save(order);
    }
}

@Entity
public class Order {
    @Id
    private String id; // ULID как String
    
    // ULID формат: 01ARZ3NDEKTSV4RRFFQ69G5FAV (26 символов)
}

ULID структура:

  • 48 бит: timestamp (миллисекунды)
  • 80 бит: случайные данные
  • Base32 кодирование

5. Centralized ID Service (Redis)

Для очень высоконагруженных систем используют отдельный сервис для ID.

@Service
public class CentralizedIdService {
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    public Long getNextOrderId() {
        // Атомарный INCREMENT в Redis
        return redisTemplate.opsForValue().increment("order:id:counter");
    }
    
    public String getNextOrderNumber() {
        Long id = getNextOrderId();
        LocalDate today = LocalDate.now();
        return String.format("ORD-%04d-%06d", 
            today.getYear() % 100, 
            id % 1000000);
    }
}

@Service
public class OrderService {
    @Autowired
    private CentralizedIdService idService;
    
    @Transactional
    public Order createOrder(OrderRequest request) {
        Order order = new Order();
        order.setOrderNumber(idService.getNextOrderNumber());
        order.setCreatedAt(LocalDateTime.now());
        
        return orderRepository.save(order);
    }
}

6. Segment Tree / Range Allocation

Для очень высоких нагрузок сервер периодически выделяет себе диапазон ID.

@Service
public class RangeBasedIdService {
    @Autowired
    private IdRangeRepository idRangeRepository;
    
    private volatile IdRange currentRange;
    private final Object lock = new Object();
    
    public Long getNextId() {
        if (currentRange == null || currentRange.isExhausted()) {
            synchronized (lock) {
                if (currentRange == null || currentRange.isExhausted()) {
                    // Выделяем новый диапазон из БД (например, 10000 ID)
                    currentRange = allocateNewRange();
                }
            }
        }
        
        return currentRange.getNextId();
    }
    
    private IdRange allocateNewRange() {
        // Атомарная операция в БД
        return idRangeRepository.allocateRange(10000);
    }
}

@Entity
public class IdRange {
    private Long startId;
    private Long endId;
    private Long currentId;
    
    public synchronized Long getNextId() {
        if (currentId >= endId) {
            throw new RuntimeException("Range exhausted");
        }
        return currentId++;
    }
    
    public boolean isExhausted() {
        return currentId >= endId;
    }
}

Сравнение подходов

ПодходРазмерСортируемыйСкоростьМасштабируемость
AUTO_INCREMENT8 байтДаБыстроПлохая
UUID16 байтНетБыстроОтличная
Snowflake8 байтДаОчень быстроОтличная
ULID10 байтДаБыстроОтличная
Centralized (Redis)8 байтДаМедленноХорошая

Best Practices

  1. Для новых проектов используй UUID или Snowflake
  2. Snowflake если нужна сортируемость и компактность
  3. UUID если простота и стандартизация
  4. Не полагайся на AUTO_INCREMENT в распределённых системах
  5. Кэшируй диапазоны ID если нужна экстремальная производительность
  6. Мониторь skew ID (когда на одном сервере генерируется больше ID)
  7. Документируй выбранную схему для всей команды

В production коде я обычно использую Snowflake для новых систем с высокой нагрузкой и UUID для CRUD приложений. Redis-based сервис использую только для гиперскалированных систем.

Как спроектировать систему для присвоения уникальных номеров на запрос при масштабировании | PrepBro