Backend Development 13 min read

Generating Globally Unique Identifiers (GUID) for Performance Testing: UUID, Distributed Services, Snowflake Algorithm, and Thread‑Local Techniques

This article explains why globally unique identifiers are needed in performance testing, compares several common solutions such as Java UUID, Redis/Zookeeper distributed ID generators, the Snowflake algorithm, and thread‑local or shared counters, and provides complete Java code examples for each approach.

FunTester
FunTester
FunTester
Generating Globally Unique Identifiers (GUID) for Performance Testing: UUID, Distributed Services, Snowflake Algorithm, and Thread‑Local Techniques

UUID (Universally Unique Identifier)

UUID is a standardized 128‑bit identifier widely used in distributed systems to avoid data duplication and conflicts. It is typically represented as 32 hexadecimal characters separated by hyphens, and its uniqueness relies on randomness and length, making collisions extremely unlikely.

Java provides the built‑in class java.util.UUID for generating UUIDs, for example:

UUID uuid = UUID.randomUUID();
String id = uuid.toString();

A typical UUID looks like 245fee40-8b24-47d3-b5e1-09a5e48a08d1 . Different JDK versions may produce the same format.

Advantages of UUID include global uniqueness, unordered generation, high performance, unpredictability, and extensibility. Drawbacks are its relatively long length (36 characters), poor readability, non‑sequential nature, and a very low but non‑zero collision probability.

GUID Generation with Distributed Services (Redis, Zookeeper, etc.)

In distributed systems a global unique ID is essential for identifying records across nodes, tracing transactions, and messaging. Simple database auto‑increment or UUID may not meet the requirements, so services like Redis or Zookeeper are used.

Redis's INCR command provides an atomic counter that can serve as a distributed ID generator. By storing a global key and incrementing it on each request, Redis guarantees uniqueness, though availability and performance must be considered.

Zookeeper can create ordered temporary nodes; each node’s name is an incrementing counter, offering higher reliability for critical systems but at the cost of more network round‑trips.

Both approaches are easy to implement, while MySQL auto‑increment is generally discouraged for high‑performance distributed scenarios.

Snowflake Algorithm

The Snowflake algorithm, originally designed by Twitter, generates 64‑bit unique IDs using a timestamp, datacenter ID, worker ID, and a sequence number. It ensures uniqueness, monotonicity, and high throughput, making it popular for distributed ID generation.

Below is a concise Java implementation of Snowflake:

package com.funtester.utils;

public class SnowflakeUtils {
    private static final long START_TIMESTAMP = 1616489534000L; // start time
    private long datacenterId;
    private long workerId;
    private long sequence = 0L;
    private static final long MAX_WORKER_ID = 31L;
    private static final long MAX_DATA_CENTER_ID = 31L;
    private static final long SEQUENCE_BITS = 12L;
    private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
    private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_SHIFT;
    private static final long TIMESTAMP_LEFT_SHIFT = DATA_CENTER_ID_SHIFT + DATA_CENTER_ID_SHIFT;
    private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
    private long lastTimestamp = -1L;

    public SnowflakeUtils(long datacenterId, long workerId) {
        if (datacenterId > MAX_DATA_CENTER_ID || datacenterId < 0) {
            throw new IllegalArgumentException("Datacenter ID can't be greater than " + MAX_DATA_CENTER_ID + " or less than 0");
        }
        if (workerId > MAX_WORKER_ID || workerId < 0) {
            throw new IllegalArgumentException("Worker ID can't be greater than " + MAX_WORKER_ID + " or less than 0");
        }
        this.datacenterId = datacenterId;
        this.workerId = workerId;
    }

    public synchronized long nextId() {
        long timestamp = System.currentTimeMillis();
        if (timestamp < lastTimestamp) {
            throw new RuntimeException("Clock moved backwards. Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds");
        }
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & SEQUENCE_MASK;
            if (sequence == 0) {
                timestamp = nextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }
        lastTimestamp = timestamp;
        long id = ((timestamp - START_TIMESTAMP) << TIMESTAMP_LEFT_SHIFT)
                | (datacenterId << DATA_CENTER_ID_SHIFT)
                | (workerId << WORKER_ID_SHIFT)
                | sequence;
        return id & Long.MAX_VALUE; // ensure a positive value
    }

    private long nextMillis(long lastTimestamp) {
        long timestamp = System.currentTimeMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = System.currentTimeMillis();
        }
        return timestamp;
    }
}

Usage example:

public static void main(String[] args) {
    SnowflakeUtils snowflake = new SnowflakeUtils(1, 1);
    for (int i = 0; i < 5; i++) {
        System.out.println("Next ID: " + snowflake.nextId());
    }
}

The output shows a series of large positive numbers, each guaranteed to be unique across the cluster.

Thread‑Local Variable Approach

For non‑concurrent scenarios a simple i++ can provide uniqueness, but in multithreaded environments each thread must maintain its own counter. Using ThreadLocal<Integer> each thread increments its own value, avoiding race conditions.

static ThreadLocal
threadLocal = new ThreadLocal
() {
    @Override
    protected Integer initialValue() {
        return 0;
    }
};

public static void main(String[] args) {
    setPoolMax(3);
    for (int i = 0; i < 10; i++) {
        fun {
            increase();
            System.out.println(Thread.currentThread().getName() + " threadLocal.get() = " + threadLocal.get());
        }
    }
}

static void increase() {
    threadLocal.set(threadLocal.get() + 1);
}

The printed results demonstrate each thread’s independent counter. Note that excessive use of ThreadLocal can lead to memory leaks, but in short‑lived performance‑test JVMs this is usually acceptable.

Shared Counter (AtomicInteger) Approach

The simplest solution is a globally shared, thread‑safe counter such as AtomicInteger . Each call to incrementAndGet() atomically increments the value, providing a unique ID with minimal code.

// Define a global atomic counter
static AtomicInteger index = new AtomicInteger(0);

public static void main(String[] args) {
    setPoolMax(3);
    for (int i = 0; i < 10; i++) {
        fun {
            System.out.println("Increment result: " + index.incrementAndGet());
        }
    }
}

The output shows sequential numbers from 1 to 10, confirming the approach’s correctness.

Different projects may prefer different strategies depending on performance requirements, deployment topology, and readability concerns; readers are encouraged to experiment and share their own solutions.

distributed systemsJavaPerformance TestingUUIDsnowflakeGUID
FunTester
Written by

FunTester

10k followers, 1k articles | completely useless

0 followers
Reader feedback

How this landed with the community

login 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.