
com.cedarsoftware.util.UniqueIdGenerator Maven / Gradle / Ivy
The newest version!
package com.cedarsoftware.util;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.Date;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Logger;
import com.cedarsoftware.util.LoggingConfig;
import static java.lang.Integer.parseInt;
import static java.lang.Math.abs;
import static java.lang.System.currentTimeMillis;
/**
* Generates guaranteed unique, time-based, monotonically increasing IDs within a distributed environment.
* Each ID encodes three pieces of information:
*
* - Timestamp - milliseconds since epoch (1970)
* - Sequence number - counter for multiple IDs within same millisecond
* - Server ID - unique identifier (0-99) for machine/instance in cluster
*
*
* Cluster Support
* Server IDs are determined in the following priority order:
*
* - Environment variable JAVA_UTIL_CLUSTERID (0-99)
* - Kubernetes Pod ID (extracted from metadata)
* - VMware Tanzu instance ID
* - Cloud Foundry instance index (CF_INSTANCE_INDEX)
* - Hash of hostname modulo 100
* - Random number (0-99) if all else fails
*
*
* Available APIs
* Two ID generation methods are provided with different characteristics:
*
* getUniqueId()
* - Format: timestampMs(13-14 digits).sequence(3 digits).serverId(2 digits)
* - Rate: Up to 1,000 IDs per millisecond
* - Range: Until year 5138
* - Example: 1234567890123456.789.99
*
* getUniqueId19()
* - Format: timestampMs(13 digits).sequence(4 digits).serverId(2 digits)
* - Rate: Up to 10,000 IDs per millisecond
* - Range: Until year 2286 (positive values)
* - Example: 1234567890123.9999.99
*
*
* Guarantees
* The generator provides the following guarantees:
*
* - IDs are unique across JVM restarts on the same machine
* - IDs are unique across machines when proper server IDs are configured
* - IDs are strictly monotonically increasing (each ID > previous ID)
* - System clock regression is handled gracefully
* - High sequence numbers cause waiting for next millisecond
*
*
* @author John DeRegnaucourt ([email protected])
* @author Roger Judd (@HonorKnight on GitHub) for adding code to ensure increasing order
* Copyright (c) Cedar Software LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* License
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@SuppressWarnings("unchecked")
public final class UniqueIdGenerator {
public static final String JAVA_UTIL_CLUSTERID = "JAVA_UTIL_CLUSTERID";
public static final String KUBERNETES_POD_NAME = "HOSTNAME";
public static final String TANZU_INSTANCE_ID = "VMWARE_TANZU_INSTANCE_ID";
public static final String CF_INSTANCE_INDEX = "CF_INSTANCE_INDEX";
private UniqueIdGenerator() {
}
private static final Lock lock = new ReentrantLock();
private static final Lock lock19 = new ReentrantLock();
private static final Logger LOG = Logger.getLogger(UniqueIdGenerator.class.getName());
static { LoggingConfig.init(); }
private static int count = 0;
private static int count2 = 0;
private static long lastTimeMillis = 0;
private static long lastTimeMillis19 = 0;
private static long lastGeneratedId = 0;
private static long lastGeneratedId19 = 0;
private static final int serverId;
static {
String setVia;
// Try JAVA_UTIL_CLUSTERID first (maintain backward compatibility)
int id = getServerId(JAVA_UTIL_CLUSTERID);
setVia = "environment variable: " + JAVA_UTIL_CLUSTERID;
if (id == -1) {
// Try indirect environment variable
String envName = SystemUtilities.getExternalVariable(JAVA_UTIL_CLUSTERID);
if (StringUtilities.hasContent(envName)) {
String envValue = SystemUtilities.getExternalVariable(envName);
id = getServerId(envValue);
setVia = "environment variable: " + envName;
}
}
if (id == -1) {
// Try Kubernetes Pod ID
String podName = SystemUtilities.getExternalVariable(KUBERNETES_POD_NAME);
if (StringUtilities.hasContent(podName)) {
// Extract ordinal from pod name (typically ends with -0, -1, etc.)
try {
if (podName.contains("-")) {
String ordinal = podName.substring(podName.lastIndexOf('-') + 1);
id = abs(parseInt(ordinal)) % 100;
setVia = "Kubernetes pod name: " + podName;
}
} catch (Exception ignored) {
// Fall through to next strategy if pod name parsing fails
}
}
}
if (id == -1) {
// Try Tanzu instance ID
id = getServerId(TANZU_INSTANCE_ID);
if (id != -1) {
setVia = "VMware Tanzu instance ID";
}
}
if (id == -1) {
// Try Cloud Foundry instance index
id = getServerId(CF_INSTANCE_INDEX);
if (id != -1) {
setVia = "Cloud Foundry instance index";
}
}
if (id == -1) {
// Try hostname hash
String hostName = SystemUtilities.getExternalVariable("HOSTNAME");
if (StringUtilities.hasContent(hostName)) {
String hostnameSha256 = EncryptionUtilities.calculateSHA256Hash(hostName.getBytes(StandardCharsets.UTF_8));
// Convert first 8 characters of the hash to an integer
try {
String hashSegment = hostnameSha256.substring(0, 8);
int hashInt = Integer.parseUnsignedInt(hashSegment, 16);
id = hashInt % 100;
setVia = "hostname hash: " + hostName + " (" + hostnameSha256 + ")";
} catch (Exception ignored) {
// Fall through to next strategy if pod name parsing fails
}
}
}
if (id == -1) {
// Final fallback - use secure random
SecureRandom random = new SecureRandom();
id = abs(random.nextInt()) % 100;
setVia = "SecureRandom";
}
LOG.info("java-util using node id=" + id + " for last two digits of generated unique IDs. Set using " + setVia);
serverId = id;
}
/**
* Generates a unique, monotonically increasing ID with millisecond precision that's cluster-safe.
*
* ID Format
* The returned long value contains three components:
*
* [timestamp: 13-14 digits][sequence: 3 digits][serverId: 2 digits]
* Example: 12345678901234.999.99 (dots for clarity, actual value has no dots)
*
*
* Characteristics
*
* - Supports up to 1,000 unique IDs per millisecond (sequence 000-999)
* - Generates positive values until year 5138
* - Guaranteed monotonically increasing even across millisecond boundaries
* - Thread-safe through internal locking
* - Handles system clock regression gracefully
* - Blocks when sequence number exhausted within a millisecond
*
*
* @return A unique, time-based ID encoded as a long value
* @see #getDate(long) To extract the timestamp from the generated ID
*/
public static long getUniqueId() {
lock.lock();
try {
long currentTime = currentTimeMillis();
if (currentTime < lastTimeMillis) {
// Clock went backwards - use last time
currentTime = lastTimeMillis;
}
if (currentTime == lastTimeMillis) {
count++;
if (count >= 1000) {
// Wait for next millisecond
currentTime = waitForNextMillis(lastTimeMillis);
count = 0;
}
} else {
count = 0;
lastTimeMillis = currentTime;
}
long newId = currentTime * 100_000 + count * 100L + serverId;
if (newId <= lastGeneratedId) {
newId = lastGeneratedId + 1;
}
lastGeneratedId = newId;
return newId;
} finally {
lock.unlock();
}
}
/**
* Generates a unique, monotonically increasing 19-digit ID optimized for higher throughput.
*
* ID Format
* The returned long value contains three components:
*
* [timestamp: 13 digits][sequence: 4 digits][serverId: 2 digits]
* Example: 1234567890123.9999.99 (dots for clarity, actual value has no dots)
*
*
* Characteristics
*
* - Supports up to 10,000 unique IDs per millisecond (sequence 0000-9999)
* - Generates positive values until year 2286 (after which values may be negative)
* - Guaranteed monotonically increasing even across millisecond boundaries
* - Thread-safe through internal locking
* - Handles system clock regression gracefully
* - Blocks when sequence number exhausted within a millisecond
*
*
* Performance Comparison
* This method is optimized for higher throughput compared to {@link #getUniqueId()}:
*
* - Supports 10x more IDs per millisecond (10,000 vs 1,000)
* - Trades timestamp range for increased sequence capacity
* - Recommended for high-throughput scenarios through year 2286
*
*
* @return A unique, time-based ID encoded as a long value
* @see #getDate19(long) To extract the timestamp from the generated ID
*/
public static long getUniqueId19() {
lock19.lock();
try {
long currentTime = currentTimeMillis();
if (currentTime < lastTimeMillis19) {
// Clock went backwards - use last time
currentTime = lastTimeMillis19;
}
if (currentTime == lastTimeMillis19) {
count2++;
if (count2 >= 10_000) {
// Wait for next millisecond
currentTime = waitForNextMillis(lastTimeMillis19);
count2 = 0;
}
} else {
count2 = 0;
lastTimeMillis19 = currentTime;
}
long newId = currentTime * 1_000_000 + count2 * 100L + serverId;
if (newId <= lastGeneratedId19) {
newId = lastGeneratedId19 + 1;
}
lastGeneratedId19 = newId;
return newId;
} finally {
lock19.unlock();
}
}
/**
* Extracts the date-time from an ID generated by {@link #getUniqueId()}.
*
* @param uniqueId A unique ID previously generated by {@link #getUniqueId()}
* @return The Date representing when the ID was generated, accurate to the millisecond
* @throws IllegalArgumentException if the ID was not generated by {@link #getUniqueId()}
*/
public static Date getDate(long uniqueId) {
return new Date(uniqueId / 100_000);
}
/**
* Extracts the date-time from an ID generated by {@link #getUniqueId19()}.
*
* @param uniqueId19 A unique ID previously generated by {@link #getUniqueId19()}
* @return The Date representing when the ID was generated, accurate to the millisecond
* @throws IllegalArgumentException if the ID was not generated by {@link #getUniqueId19()}
*/
public static Date getDate19(long uniqueId19) {
return new Date(uniqueId19 / 1_000_000);
}
/**
* Extracts the date-time from an ID generated by {@link #getUniqueId()}.
*
* @param uniqueId A unique ID previously generated by {@link #getUniqueId()}
* @return The Instant representing when the ID was generated, accurate to the millisecond
* @throws IllegalArgumentException if the ID was not generated by {@link #getUniqueId()}
*/
public static Instant getInstant(long uniqueId) {
if (uniqueId < 0) {
throw new IllegalArgumentException("Invalid uniqueId: must be positive");
}
return Instant.ofEpochMilli(uniqueId / 100_000);
}
/**
* Extracts the date-time from an ID generated by {@link #getUniqueId19()}.
*
* @param uniqueId19 A unique ID previously generated by {@link #getUniqueId19()}
* @return The Instant representing when the ID was generated, accurate to the millisecond
* @throws IllegalArgumentException if the ID was not generated by {@link #getUniqueId19()}
*/
public static Instant getInstant19(long uniqueId19) {
if (uniqueId19 < 0) {
throw new IllegalArgumentException("Invalid uniqueId19: must be positive");
}
return Instant.ofEpochMilli(uniqueId19 / 1_000_000);
}
private static long waitForNextMillis(long lastTimestamp) {
long timestamp = currentTimeMillis();
while (timestamp <= lastTimestamp) {
LockSupport.parkNanos(1000); // small pause to reduce CPU usage
timestamp = currentTimeMillis();
}
return timestamp;
}
private static int getServerId(String externalVarName) {
try {
String id = SystemUtilities.getExternalVariable(externalVarName);
if (StringUtilities.isEmpty(id)) {
return -1;
}
int parsedId = parseInt(id);
return (int) (Math.abs((long) parsedId) % 100);
} catch (NumberFormatException | SecurityException e) {
LOG.fine("Unable to retrieve server id from " + externalVarName + ": " + e.getMessage());
return -1;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy