Spring Boot High-Concurrency Distributed ID Generation: Snowflake Algorithm, Clock Drift Handling & Performance Optimization

This article walks through implementing Twitter's Snowflake algorithm in Spring Boot for distributed ID generation, covering 64-bit structure, clock drift mitigation strategies (wait, error, historical compensation), pre-generated ID pools with double buffering for 120k QPS, MyBatis-Plus integration, and Meituan Leaf design insights.

Xiaolin Talks Programming
Xiaolin Talks Programming
Xiaolin Talks Programming
Spring Boot High-Concurrency Distributed ID Generation: Snowflake Algorithm, Clock Drift Handling & Performance Optimization

1. Business Scenarios and Solution Selection

Global unique IDs are needed for order numbers, message IDs, trace IDs, log IDs, and payment serial numbers. Requirements: high concurrency, global uniqueness, trend-increasing, no generation bottleneck.

Common approaches compared:

UUID : Unordered, long strings hurt database index performance and sorting; advantage is local generation without network overhead.

Database auto-increment : Ordered but depends on database, single point in distributed environments, bottleneck under high concurrency.

Segment mode : Batch fetch from database, good performance but depends on DB; restart may lose segments.

Snowflake algorithm : No external dependencies, trend-increasing, extremely high performance; drawbacks are clock drift causing duplicate IDs and workerId management.

Snowflake is the mainstream choice but requires clock drift enhancements.

2. Snowflake Algorithm Structure Analysis

Twitter's Snowflake splits a 64-bit integer into:

0  1 - 41       42 - 51       52 - 63
 ┌─┬─────────────┬─────────────┬─────────────┐
 │0│ timestamp(ms) │ machineID(10) │ sequence(12)│
 └─┴─────────────┴─────────────┴─────────────┘

Sign bit: always 0, ensures positive ID.

Timestamp: 41 bits milliseconds. 2^41 ms ≈ 69 years; with custom epoch (e.g., 2024-01-01) usable until 2093.

Machine ID: 10 bits, can split into 5-bit datacenter + 5-bit machine, supporting 32 datacenters × 32 machines = 1024 nodes.

Sequence: 12 bits, max 4096 IDs per millisecond; when exhausted, wait for next millisecond.

Benefits: global uniqueness if clock doesn't drift and workerId doesn't conflict; trend-increasing due to millisecond + sequence increment; pure in-memory computation, no network overhead.

3. Spring Boot Snowflake Generator Implementation

Core bit-operation logic:

public class SnowflakeIdGenerator {
    private static final long START_TIMESTAMP = 1704067200000L; // 2024-01-01
    private static final long SEQUENCE_BITS = 12L;
    private static final long WORKER_ID_BITS = 10L;
    private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
    private static final long WORKER_ID_MASK = ~(-1L << WORKER_ID_BITS);
    private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
    private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;

    private final long workerId;
    private long lastTimestamp = -1L;
    private long sequence = 0L;

    public SnowflakeIdGenerator(long workerId) {
        if (workerId < 0 || workerId > WORKER_ID_MASK) {
            throw new IllegalArgumentException("workerId must be between 0 and " + WORKER_ID_MASK);
        }
        this.workerId = workerId;
    }

    public synchronized long nextId() {
        long currentTimestamp = System.currentTimeMillis();

        if (currentTimestamp < lastTimestamp) {
            // clock drift handling placeholder
            throw new IllegalStateException("Clock moved backwards");
        }

        if (currentTimestamp == lastTimestamp) {
            sequence = (sequence + 1) & SEQUENCE_MASK;
            if (sequence == 0) {
                currentTimestamp = waitNextMillis(currentTimestamp);
            }
        } else {
            sequence = 0L;
        }

        lastTimestamp = currentTimestamp;
        return ((currentTimestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT)
                | (workerId << WORKER_ID_SHIFT)
                | sequence;
    }

    private long waitNextMillis(long currentTimestamp) {
        while (currentTimestamp <= lastTimestamp) {
            currentTimestamp = System.currentTimeMillis();
        }
        return currentTimestamp;
    }
}

Configuration splits 10-bit workerId into 5-bit datacenter + 5-bit machine: workerId = (dataCenterId << 5) | machineId.

@Component
@ConfigurationProperties(prefix = "snowflake")
public class SnowflakeProperties {
    private long dataCenterId = 1;
    private long machineId = 1;
    // getters/setters omitted
    public long getWorkerId() {
        return (dataCenterId << 5) | machineId;
    }
}
@Configuration
public class IdGeneratorConfig {
    @Bean
    public SnowflakeIdGenerator snowflakeIdGenerator(SnowflakeProperties properties) {
        return new SnowflakeIdGenerator(properties.getWorkerId());
    }
}

Inject via @Autowired. Clock drift handling still needed.

4. Clock Drift Handling

Snowflake fails if system clock moves backward, risking duplicate IDs in same millisecond. Three common strategies:

4.1 Wait

If drift ≤ 5 seconds, sleep thread until clock catches up:

if (currentTimestamp < lastTimestamp) {
    long offset = lastTimestamp - currentTimestamp;
    if (offset <= 5000) {
        try { Thread.sleep(offset * 2); }
        catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        currentTimestamp = System.currentTimeMillis();
        while (currentTimestamp < lastTimestamp) {
            currentTimestamp = System.currentTimeMillis();
        }
    } else {
        throw new IllegalStateException("Clock moved backwards too much");
    }
}

Simple but frequent drifts cause thread blocking and request pile-up.

4.2 Throw Error

Drift > threshold (e.g., 100ms) throws exception; upper layer decides fallback or reject:

if (currentTimestamp < lastTimestamp) {
    long offset = lastTimestamp - currentTimestamp;
    if (offset > 100) {
        throw new IllegalStateException("Clock moved backwards. Refusing to generate ID for " + offset + " ms");
    }
    // small drift, continue
}

Guarantees no duplicates but may cause service unavailability.

4.3 Historical Time Compensation

Pretend time stays at last generation timestamp, increment sequence until 4096 exhausted:

public synchronized long nextId() {
    long currentTimestamp = System.currentTimeMillis();
    if (currentTimestamp < lastTimestamp) {
        sequence = (sequence + 1) & SEQUENCE_MASK;
        if (sequence == 0) {
            throw new IllegalStateException("Sequence exhausted while clock moved backwards");
        }
        // continue using lastTimestamp, sequence increments
        return ((lastTimestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT)
                | (workerId << WORKER_ID_SHIFT)
                | sequence;
    }
    // normal flow...
}

Only compensates short drifts; long drifts exhaust sequence and throw error.

4.4 Combined Wait + Compensation (Production Preferred)

public synchronized long nextId() {
    long currentTimestamp = System.currentTimeMillis();
    long offset = lastTimestamp - currentTimestamp;

    if (offset > 0) {
        if (offset > 5000) {
            throw new IllegalStateException("Clock moved backwards, offset=" + offset + "ms");
        }
        try { Thread.sleep(offset + 1); }
        catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        currentTimestamp = System.currentTimeMillis();
    }

    if (currentTimestamp == lastTimestamp) {
        sequence = (sequence + 1) & SEQUENCE_MASK;
        if (sequence == 0) {
            currentTimestamp = waitNextMillis(currentTimestamp);
        }
    } else {
        sequence = 0L;
    }

    lastTimestamp = currentTimestamp;
    return ((currentTimestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT)
            | (workerId << WORKER_ID_SHIFT)
            | sequence;
}

Alternative: store last generated timestamp in Redis/ZooKeeper; if local time < global max, use global timestamp. Adds network overhead, generally not used.

5. Pre-generated ID Pool and Double Buffering

System.currentTimeMillis()

has syscall overhead; synchronized lock adds contention. Pre-generate IDs into local queue:

public class IdPool {
    private final SnowflakeIdGenerator generator;
    private final BlockingQueue<Long> queue = new LinkedBlockingQueue<>(10000);
    private final ExecutorService executor = Executors.newSingleThreadExecutor();

    public IdPool(SnowflakeIdGenerator generator) {
        this.generator = generator;
        fill();
    }

    private void fill() {
        for (int i = 0; i < 10000; i++) {
            queue.offer(generator.nextId());
        }
    }

    public Long nextId() throws InterruptedException {
        if (queue.size() < 5000) {
            executor.submit(this::fill); // async refill, prevent duplicate submission
        }
        return queue.take();
    }
}

Production: add AtomicBoolean to prevent duplicate fill tasks. Double buffering uses two queues, swap when half consumed:

public class DoubleBufferIdPool {
    private final SnowflakeIdGenerator generator;
    private final int bufferSize;
    private volatile Queue<Long> currentBuffer;
    private Queue<Long> nextBuffer;
    private final ExecutorService executor = Executors.newSingleThreadExecutor();

    public DoubleBufferIdPool(SnowflakeIdGenerator generator, int bufferSize) {
        this.generator = generator;
        this.bufferSize = bufferSize;
        this.currentBuffer = new ArrayDeque<>();
        this.nextBuffer = new ArrayDeque<>();
        fillBuffer(currentBuffer);
    }

    private void fillBuffer(Queue<Long> buffer) {
        for (int i = 0; i < bufferSize; i++) {
            buffer.offer(generator.nextId());
        }
    }

    public Long nextId() {
        Long id = currentBuffer.poll();
        if (id == null) {
            synchronized (this) {
                if (currentBuffer.isEmpty()) {
                    Queue<Long> tmp = currentBuffer;
                    currentBuffer = nextBuffer;
                    nextBuffer = tmp;
                    fillBuffer(nextBuffer);
                }
                id = currentBuffer.poll();
            }
        } else {
            if (currentBuffer.size() <= bufferSize / 2 && nextBuffer.size() < bufferSize) {
                executor.submit(() -> fillBuffer(nextBuffer));
            }
        }
        return id;
    }
}

Pre-generation wastes some IDs but worth the performance gain. Batch interface can return multiple IDs per call.

6. MyBatis-Plus Integration

MyBatis-Plus default ASSIGN_ID uses Snowflake; replace with custom generator by implementing IdentifierGenerator:

@Component
public class MyBatisPlusIdGenerator implements IdentifierGenerator {
    @Autowired
    private IdPool idPool; // or SnowflakeIdGenerator directly

    @Override
    public Number nextId(Object entity) {
        return idPool.nextId();
    }
}

Entity primary key uses @TableId(type = IdType.ASSIGN_ID). For string order IDs, separate service:

@Service
public class OrderIdService {
    @Autowired
    private SnowflakeIdGenerator generator;

    public String generateOrderId() {
        return "ORDER" + new SimpleDateFormat("yyyyMMdd").format(new Date()) + generator.nextId();
    }
}

7. Meituan Leaf Design Insights

Leaf offers two modes:

Segment mode : Fetch ID segment (e.g., 1~1000) from DB table with biz_tag, max_id, step. Background async loads next segment when near exhaustion. Low DB pressure, trend-increasing; requires segment table maintenance, DB failure breaks service.

Snowflake mode : Solves two pain points: (1) dynamic workerId allocation via ZooKeeper node registration, acquiring incremental sequence as workerId, releasing on shutdown; (2) clock drift via checkTimestamp mechanism — detects drift, throws exception, alerts for manual handling.

Reference Leaf: consider Redis for dynamic workerId allocation in production, but config file suffices for most teams if managed properly.

8. Load Testing and Monitoring

Benchmarks on 8C16G machine (JMeter 100 threads, 60 seconds):

Basic Snowflake: ~35,000 QPS, avg latency 2.8ms, P99 8.1ms.

With ID pool double buffering: ~120,000 QPS, avg latency 0.8ms, P99 2.3ms.

Key production metrics to monitor:

Generation latency: average and TP99.

Pool water level: remaining ID count, alert when below threshold.

Clock drift occurrences: record time delta and frequency.

QPS: capacity planning and scaling.

Implement with Micrometer + Prometheus + Grafana; instrument Timer and Counter in code.

9. Summary Recommendations

Set epoch far enough (e.g., 2024-01-01) for decades of ID space.

Ensure workerId uniqueness; ops must maintain registry.

Enable NTP sync with -x flag to avoid time jumps.

Log clock drift details for troubleshooting.

Add rate limiting and circuit breaking to ID generation service to protect against upstream bursts.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Performance OptimizationMyBatis-Plussnowflake-algorithmclock-driftspring-bootdistributed-id-generationdouble-bufferingid-pool
Xiaolin Talks Programming
Written by

Xiaolin Talks Programming

Focuses on sharing original technical insights. Senior architect at a top tech company with years of experience in technical architecture and management, and extensive interview experience. Offers one-on-one technical coaching, guiding you from beginner to architecture design to technical management. Follow for free learning resources. Free one-on-one interview coaching to help you land offers quickly.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.