BindProject
[Backend] primary-id-provider(snowflake)
dding-shark
2025. 6. 17. 00:36
728x90
Snowflake 기반 분산 ID 생성기 구현기
대규모 트래픽 환경에서 충돌 없이, 정렬 가능한 64비트 고유 ID를 생성하기 위해 구현
여러 MSA에서 사용하기 위해 가장먼저 primary-id 생성기를 구현
구현 배경
기존 시스템에서 UUID를 Primary Key로 사용하고 있었지만, 다음과 같은 한계에 직면했다:
- 정렬되지 않아서 인덱스 삽입 비용 증가
- 16바이트 문자열로 비효율적인 저장/전송
- 멀티 인스턴스 환경에서 충돌 방지 전략 필요
그래서 Twitter의 Snowflake 알고리즘을 참고해, 시간순 정렬 가능한 64비트 정수형 ID 생성기를 직접 구현했다.
구현 목표
- ULID처럼 정렬 가능한 ID
- 수천 개 동시 요청에서도 중복 없는 ID 보장
- 여러 노드에서 실행 시에도 충돌 방지 가능
- 최소한의 외부 의존성으로 단독으로 작동
아키텍처 및 구성
✔ ID 구성 구조 (64비트)
| 필드 | 비트 수 | 설명 |
|---|---|---|
| timestamp | 41 bits | 커스텀 epoch 기준 밀리초 |
| nodeId | 10 bits | 인스턴스 구분을 위한 노드 ID |
| sequence | 12 bits | 동일 밀리초 내 시퀀스 |
| 합계 | 63 bits | 부호 비트 없이 사용 가능 |
코드 구조
primary-id-provider/
├── Snowflake.java
├── SnowflakeProperties.java
├── SnowflakeConfig.java
└── application.yaml
구현 코드 (Snowflke.java)
/**
* Snowflake ID Generator
* - Time-ordered 64-bit unique ID
* - Custom epoch
* - Supports multiple nodes
*
* Author: MyungJoo
* Date: 2025-06-17
*/
public class Snowflake {
// ===== Bit Allocation =====
private static final int NODE_ID_BITS = 10;
private static final int SEQUENCE_BITS = 12;
private static final long MAX_NODE_ID = (1L << NODE_ID_BITS) - 1;
private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1;
private static final int NODE_ID_SHIFT = SEQUENCE_BITS;
private static final int TIMESTAMP_SHIFT = NODE_ID_BITS + SEQUENCE_BITS;
// ===== Custom Epoch: 2024-01-01T00:00:00Z =====
private static final long CUSTOM_EPOCH = 1704067200000L;
// ===== Instance Variables =====
private final long nodeId;
private long lastTimestamp = -1L;
private long sequence = 0L;
/**
* @param nodeId [0, 1023]
*/
public Snowflake(long nodeId) {
if (nodeId < 0 || nodeId > MAX_NODE_ID) {
throw new IllegalArgumentException("Node ID must be between 0 and " + MAX_NODE_ID);
}
this.nodeId = nodeId;
}
/**
* Generate next unique ID
*/
public synchronized long nextId() {
long currentTimestamp = currentTime();
// Clock rollback handling
if (currentTimestamp < lastTimestamp) {
// Option 1: wait for time to catch up (soft fail)
currentTimestamp = waitNextMillis(lastTimestamp);
}
if (currentTimestamp == lastTimestamp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == 0) {
// Sequence overflow: wait for next millisecond
currentTimestamp = waitNextMillis(currentTimestamp);
}
} else {
sequence = 0;
}
lastTimestamp = currentTimestamp;
return ((currentTimestamp - CUSTOM_EPOCH) << TIMESTAMP_SHIFT)
| (nodeId << NODE_ID_SHIFT)
| sequence;
}
/**
* Busy-wait for next millisecond
*/
private long waitNextMillis(long lastTimestamp) {
long timestamp = currentTime();
while (timestamp <= lastTimestamp) {
Thread.yield(); // Reduce CPU waste
timestamp = currentTime();
}
return timestamp;
}
private long currentTime() {
return System.currentTimeMillis();
}
}
테스트
테스트: 동시성 검증
JUnit + ExecutorService 기반의 멀티스레드 테스트를 작성했다:
/**
Author: MyungJoo
Date: 2025-06-17
/
public class SnowflakeConcurrentTest {private final Snowflake snowflake = new Snowflake(1L);
@Test
void generateIdsConcurrently_withoutDuplicates() throws InterruptedException {int threadCount = 100; int idsPerThread = 1000; ExecutorService executor = Executors.newFixedThreadPool(threadCount); Set<Long> allIds = Collections.synchronizedSet(new HashSet<>()); CountDownLatch latch = new CountDownLatch(threadCount); for (int i = 0; i < threadCount; i++) { executor.submit(() -> { for (int j = 0; j < idsPerThread; j++) { long id = snowflake.nextId(); allIds.add(id); } latch.countDown(); }); } latch.await(); // 모든 스레드 종료까지 대기 executor.shutdown(); int expected = threadCount * idsPerThread; assertEquals(expected, allIds.size(), "ID 중복 발생 가능성 있음"); System.out.println("Generated " + allIds.size() + " unique IDs successfully.");}
}
```
결과: 100,000개의 동시 요청에서도 충돌 없이 안전하게 ID 생성
728x90