feat(guid): added facade guid creator and snowflake guid generator
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2023 CodeCraftersCN.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0"
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package cn.org.codecrafters.guid;
|
||||
|
||||
/**
|
||||
* The `GuidCreator` is a generic interface for generating globally unique
|
||||
* identifiers (GUIDs) of a specific type.
|
||||
* <p>
|
||||
* The type of ID is determined by the class implementing this interface.
|
||||
*
|
||||
* @param <IdType> this represents the type of the Global Unique Identifier
|
||||
*/
|
||||
public interface GuidCreator<IdType> {
|
||||
|
||||
/**
|
||||
* Generates and returns the next globally unique ID.
|
||||
* The exact implementation of how the globally unique ID is generated and
|
||||
* returned will depend on the class implementing this method.
|
||||
*
|
||||
* @return the next globally unique ID
|
||||
*/
|
||||
IdType nextId();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* Copyright (C) 2023 CodeCraftersCN.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0"
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package cn.org.codecrafters.guid;
|
||||
|
||||
import cn.org.codecrafters.guid.exceptions.TimingException;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
/**
|
||||
* SnowflakeGuidCreator is a GUID generator based on the Snowflake algorithm.
|
||||
* It generates unique identifiers using a combination of timestamp, worker ID,
|
||||
* and data center ID.
|
||||
*
|
||||
* <p>The Snowflake algorithm allows for the generation of 64-bit long integers,
|
||||
* with the following bit distribution:
|
||||
* - 1 bit for sign
|
||||
* - 41 bits for timestamp (in milliseconds)
|
||||
* - 5 bits for data center ID
|
||||
* - 5 bits for worker ID
|
||||
* - 12 bits for sequence number (per millisecond)
|
||||
*
|
||||
* <p>The worker ID and data center ID must be specified during initialization,
|
||||
* and they must be within the valid range defined by the bit size.
|
||||
* The generator maintains an internal sequence number that increments for IDs
|
||||
* generated within the same millisecond. If the system clock moves backwards,
|
||||
* an exception is thrown to avoid generating IDs with repeated timestamps.
|
||||
*
|
||||
* @author Zihlu Wang
|
||||
* @version 1.0.0
|
||||
* @since 14 Jul 2023
|
||||
*/
|
||||
public final class SnowflakeGuidCreator implements GuidCreator<Long> {
|
||||
|
||||
/**
|
||||
* Default Custom Epoch (January 1, 2015, Midnight UTC = 2015-01-01T00:00:00Z)
|
||||
*/
|
||||
private static final long DEFAULT_CUSTOM_EPOCH = 1_420_070_400_000L;
|
||||
|
||||
/**
|
||||
* The start epoch timestamp to generate IDs from.
|
||||
*/
|
||||
private final long startEpoch;
|
||||
|
||||
/**
|
||||
* The number of bits reserved for the worker ID.
|
||||
*/
|
||||
private final long workerIdBits = 5L;
|
||||
|
||||
/**
|
||||
* The number of bits reserved for the data center ID.
|
||||
*/
|
||||
private final long dataCentreIdBits = 5L;
|
||||
|
||||
/**
|
||||
* The worker ID assigned to this generator.
|
||||
*/
|
||||
private final long workerId;
|
||||
|
||||
/**
|
||||
* The data center ID assigned to this generator.
|
||||
*/
|
||||
private final long dataCentreId;
|
||||
|
||||
/**
|
||||
* The current sequence number.
|
||||
*/
|
||||
private long sequence = 0L;
|
||||
|
||||
/**
|
||||
* The timestamp of the last generated ID.
|
||||
*/
|
||||
private long lastTimestamp = -1L;
|
||||
|
||||
/**
|
||||
* Constructs a SnowflakeGuidGenerator with the default start epoch and custom worker ID, data center ID.
|
||||
*
|
||||
* @param workerId the worker ID (between 0 and 31).
|
||||
* @param dataCentreId the data center ID (between 0 and 31).
|
||||
*/
|
||||
public SnowflakeGuidCreator(long workerId, long dataCentreId) {
|
||||
this(DEFAULT_CUSTOM_EPOCH, workerId, dataCentreId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a SnowflakeGuidGenerator with a custom epoch, worker ID, and data center ID.
|
||||
*
|
||||
* @param startEpoch the custom epoch timestamp (in milliseconds) to start generating IDs from
|
||||
* @param workerId the worker ID (between 0 and 31)
|
||||
* @param dataCentreId the data center ID (between 0 and 31)
|
||||
* @throws IllegalArgumentException if the start epoch is greater than the current timestamp,
|
||||
* or if the worker ID or data center ID is out of range
|
||||
*/
|
||||
public SnowflakeGuidCreator(long startEpoch, long workerId, long dataCentreId) {
|
||||
if (startEpoch > currentTimestamp()) {
|
||||
throw new IllegalArgumentException("Start Epoch can not be greater than current timestamp!");
|
||||
}
|
||||
|
||||
var maxWorkerId = ~(-1L << workerIdBits);
|
||||
if (workerId > maxWorkerId || workerId < 0) {
|
||||
throw new IllegalArgumentException(String.format("Worker Id can't be greater than %d or less than 0",
|
||||
maxWorkerId));
|
||||
}
|
||||
|
||||
var maxDataCentreId = ~(-1L << dataCentreIdBits);
|
||||
if (dataCentreId > maxDataCentreId || dataCentreId < 0) {
|
||||
throw new IllegalArgumentException(String.format("Data Centre Id can't be greater than %d or less than 0",
|
||||
maxDataCentreId));
|
||||
}
|
||||
|
||||
this.startEpoch = startEpoch;
|
||||
this.workerId = workerId;
|
||||
this.dataCentreId = dataCentreId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the next unique ID.
|
||||
*
|
||||
* @return the generated unique ID
|
||||
* @throws TimingException if the system clock moves backwards,
|
||||
* indicating an invalid sequence of timestamps.
|
||||
*/
|
||||
@Override
|
||||
public synchronized Long nextId() {
|
||||
var timestamp = currentTimestamp();
|
||||
|
||||
// If the current time is less than the timestamp of the last ID generation, it means that the system clock
|
||||
// has been set back and an exception should be thrown.
|
||||
if (timestamp < lastTimestamp) {
|
||||
throw new TimingException("Clock moved backwards. Refusing to generate id for %d milliseconds"
|
||||
.formatted(lastTimestamp - timestamp));
|
||||
}
|
||||
|
||||
// If generated at the same time, perform intra-millisecond sequences
|
||||
long sequenceBits = 12L;
|
||||
if (lastTimestamp == timestamp) {
|
||||
long sequenceMask = ~(-1L << sequenceBits);
|
||||
sequence = (sequence + 1) & sequenceMask;
|
||||
// Sequence overflow in milliseconds
|
||||
if (sequence == 0) {
|
||||
// Block to the next millisecond, get a new timestamp
|
||||
timestamp = awaitToNextMillis(lastTimestamp);
|
||||
}
|
||||
}
|
||||
// Timestamp change, sequence reset in milliseconds
|
||||
else {
|
||||
sequence = 0L;
|
||||
}
|
||||
|
||||
// Timestamp of last ID generation
|
||||
lastTimestamp = timestamp;
|
||||
|
||||
// shifted and put together by or operations to form a 64-bit ID
|
||||
var timestampLeftShift = sequenceBits + workerIdBits + dataCentreIdBits;
|
||||
var dataCentreIdShift = sequenceBits + workerIdBits;
|
||||
return ((timestamp - startEpoch) << timestampLeftShift)
|
||||
| (dataCentreId << dataCentreIdShift)
|
||||
| (workerId << sequenceBits)
|
||||
| sequence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Blocks until the next millisecond to obtain a new timestamp.
|
||||
*
|
||||
* @param lastTimestamp the timestamp when the last ID was generated
|
||||
* @return the current timestamp
|
||||
*/
|
||||
private long awaitToNextMillis(long lastTimestamp) {
|
||||
var timestamp = currentTimestamp();
|
||||
while (timestamp <= lastTimestamp) {
|
||||
timestamp = currentTimestamp();
|
||||
}
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current timestamp in milliseconds.
|
||||
*
|
||||
* @return the current timestamp
|
||||
*/
|
||||
private long currentTimestamp() {
|
||||
return LocalDateTime.now().toInstant(ZoneOffset.UTC).toEpochMilli();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (C) 2023 CodeCraftersCN.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0"
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package cn.org.codecrafters.guid.exceptions;
|
||||
|
||||
/**
|
||||
* The TimingException class represents an exception that is thrown when there is an error related
|
||||
* to time sequence.
|
||||
* <p>
|
||||
* This class extends the RuntimeException class, which means that instances of TimingException
|
||||
* do not need to be declared in a method or constructor's throws clause.
|
||||
* <p>
|
||||
* Instances of TimingException can be created with or without a message and a cause. The message
|
||||
* provides a description of the exception, while the cause represents the underlying cause of the
|
||||
* exception and provides additional information about the error.
|
||||
* <p>
|
||||
* TimingException is typically used to handle exceptions related to timing, such as timeouts or
|
||||
* synchronization issues. It is a subclass of RuntimeException, which means it is an unchecked
|
||||
* exception and does not need to be caught or declared.
|
||||
* <p>
|
||||
*
|
||||
* @author Zihlu Wang
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class TimingException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* A custom exception that is thrown when there is an issue with timing or scheduling.
|
||||
*/
|
||||
public TimingException() {
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom exception that is thrown when there is an issue with timing or scheduling with
|
||||
* customized error message.
|
||||
*
|
||||
* @param message customized message
|
||||
*/
|
||||
public TimingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom exception that is thrown when there is an issue with timing or scheduling with
|
||||
* customized error message.
|
||||
*
|
||||
* @param message customized message
|
||||
* @param cause the cause of this exception
|
||||
*/
|
||||
public TimingException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom exception that is thrown when there is an issue with timing or scheduling with
|
||||
* customized error message.
|
||||
*
|
||||
* @param cause the cause of this exception
|
||||
*/
|
||||
public TimingException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2023 CodeCraftersCN.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0"
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The package provides a set of tools for generating globally unique
|
||||
* identifiers (GUIDs).
|
||||
* <p>
|
||||
* The goal of this library is to provide an efficient, reliable way to
|
||||
* generate globally unique identifiers without requiring any specific
|
||||
* environment or configuration.
|
||||
* <p>
|
||||
* Key features include:
|
||||
* <ul>
|
||||
* <li>Efficient generation of globally unique identifiers</li>
|
||||
* <li>High performance and quick response</li>
|
||||
* <li>Easy to integrate</li>
|
||||
* </ul>
|
||||
*
|
||||
* @since 2023.2
|
||||
*/
|
||||
package cn.org.codecrafters.guid;
|
||||
Reference in New Issue
Block a user