Как спроектировать систему для присвоения уникальных номеров на запрос при масштабировании
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как спроектировать систему для присвоения уникальных номеров на запрос при масштабировании
Это архитектурная задача о генерации уникальных 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_INCREMENT | 8 байт | Да | Быстро | Плохая |
| UUID | 16 байт | Нет | Быстро | Отличная |
| Snowflake | 8 байт | Да | Очень быстро | Отличная |
| ULID | 10 байт | Да | Быстро | Отличная |
| Centralized (Redis) | 8 байт | Да | Медленно | Хорошая |
Best Practices
- Для новых проектов используй UUID или Snowflake
- Snowflake если нужна сортируемость и компактность
- UUID если простота и стандартизация
- Не полагайся на AUTO_INCREMENT в распределённых системах
- Кэшируй диапазоны ID если нужна экстремальная производительность
- Мониторь skew ID (когда на одном сервере генерируется больше ID)
- Документируй выбранную схему для всей команды
В production коде я обычно использую Snowflake для новых систем с высокой нагрузкой и UUID для CRUD приложений. Redis-based сервис использую только для гиперскалированных систем.