如何设计一个订单id
设计一个订单ID系统需要考虑多个因素,包括唯一性、排序性(时间顺序)、可读性(可选)以及系统的扩展性和性能。结合这些因素,可以选择不同的方案来生成订单ID。以下是几种常见的订单ID设计方案:
1. 使用 UUID
UUID(Universally Unique Identifier)是一种标准的唯一标识符,确保了在分布式系统中的唯一性。UUID 有几种版本,其中最常用的是 UUID v4,它是基于随机数生成的。
优点:
- 全局唯一性
- 简单易用
缺点:
- 不能按时间排序
- 较长,不适合做主键(占用更多存储空间,影响索引性能)
示例:
import java.util.UUID;
public class OrderIdGenerator {
public static String generateOrderId() {
return UUID.randomUUID().toString();
}
public static void main(String[] args) {
System.out.println(generateOrderId());
}
}
2. 使用雪花算法(Snowflake)
雪花算法是 Twitter 开发的一种分布式 ID 生成算法,能够生成有序且唯一的 64 位整数 ID。雪花算法的 ID 结构如下:
- 1 位符号位(始终为 0)
- 41 位时间戳(自定义纪元后的毫秒数)
- 10 位机器标识(5 位数据中心ID和5位机器ID)
- 12 位序列号(同一毫秒内生成的不同ID)
优点:
- 唯一且有序
- 高性能,适合高并发场景
示例:
public class SnowflakeIdGenerator {
private final long epoch = 1609459200000L; // 自定义纪元 (2021-01-01)
private final long dataCenterIdBits = 5L;
private final long workerIdBits = 5L;
private final long sequenceBits = 12L;
private final long maxDataCenterId = (1L << dataCenterIdBits) - 1;
private final long maxWorkerId = (1L << workerIdBits) - 1;
private final long sequenceMask = (1L << sequenceBits) - 1;
private final long workerIdShift = sequenceBits;
private final long dataCenterIdShift = sequenceBits + workerIdBits;
private final long timestampShift = sequenceBits + workerIdBits + dataCenterIdBits;
private long dataCenterId;
private long workerId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long dataCenterId, long workerId) {
if (dataCenterId > maxDataCenterId || dataCenterId < 0) {
throw new IllegalArgumentException("dataCenterId must be between 0 and " + maxDataCenterId);
}
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException("workerId must be between 0 and " + maxWorkerId);
}
this.dataCenterId = dataCenterId;
this.workerId = workerId;
}
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds");
}
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - epoch) << timestampShift) |
(dataCenterId << dataCenterIdShift) |
(workerId << workerIdShift) |
sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
public static void main(String[] args) {
SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1, 1);
for (int i = 0; i < 10; i++) {
System.out.println(idGenerator.nextId());
}
}
}
3. 使用自增ID和时间戳组合
将自增ID和时间戳组合起来,可以确保订单ID的唯一性和时间顺序性。
优点:
- 简单易实现
- 有序且可读性好
缺点:
- 分布式环境中需解决自增ID的生成问题
示例:
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.atomic.AtomicLong;
public class OrderIdGenerator {
private static final AtomicLong counter = new AtomicLong(0);
public static String generateOrderId() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
String timestamp = sdf.format(new Date());
long increment = counter.incrementAndGet();
return timestamp + String.format("%04d", increment);
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println(generateOrderId());
}
}
}
4. 基于数据库的自增ID
使用数据库的自增列来生成订单ID,可以确保ID的唯一性和有序性。但需要注意在高并发和分布式环境中的性能和扩展性问题。
示例:
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id INT,
status VARCHAR(20),
total NUMERIC(10, 2),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
在插入新订单时,数据库会自动生成唯一的自增ID:
INSERT INTO orders (user_id, status, total) VALUES (1, 'completed', 100.00);
总结
设计订单ID时,可以选择不同的方案,具体选择取决于系统的需求和环境:
- UUID 适合需要全局唯一性的场景,但不能按时间排序。
- 雪花算法 适合分布式环境,能生成有序且唯一的ID。
- 自增ID和时间戳组合 简单易实现,适合单机或小规模分布式环境。
- 数据库自增ID 适合不需要高度分布式的系统,简单直接。
根据具体需求,选择合适的方案并进行实现。
雪花算法生成的ID的组成部分
雪花算法(Snowflake Algorithm)是一种分布式唯一 ID 生成算法,由 Twitter 开发。它生成的 ID 是 64 位的整型数字,通过组合多个不同部分来确保生成的 ID 唯一且有序。雪花算法生成的 ID 通常由以下几个部分组成:
-
符号位(1位):
- 固定为 0,因为生成的 ID 为正数。
-
时间戳(41位):
- 表示自定义纪元(通常是某个固定时间点,如 Unix 纪元)以来的毫秒数。
- 41 位可以表示的毫秒数约为 69 年(2^41 - 1 毫秒)。
-
数据中心ID(5位):
- 表示数据中心或机房的 ID。
- 5 位可以表示 32 个不同的数据中心。
-
机器ID(5位):
- 表示工作机器的 ID。
- 5 位可以表示 32 台不同的机器。
-
序列号(12位):
- 表示同一毫秒内生成的不同 ID。
- 12 位可以表示 4096 个不同的序列号。
雪花算法生成的 ID 由符号位、时间戳、数据中心ID、机器ID 和序列号组成。它通过时间戳保证全局有序性,通过数据中心ID和机器ID保证分布式环境中的唯一性,通过序列号保证同一毫秒内生成的多个 ID 的唯一性。
其他问题考虑
并发极高情况下,序列号溢出怎么办?
前面提到,序列号部分是12位,这意味着在同一毫秒内可以生成的ID数量为2^12 = 4096个。因此,在同一毫秒内,最多可以生成4096个不同的ID。如果在极高并发的情况下(超过4096个ID每毫秒,即每秒超过4096*1000个ID),标准的雪花算法会遇到序列号溢出的问题,进而导致生成重复的ID。在这种情况下,有几种策略可以解决这一问题:
1. 扩展序列号位数
一种简单的方法是扩展序列号部分的位数,但这会减少其他部分(如时间戳、数据中心ID、机器ID)的位数,从而减少它们的容量。
2. 时间回拨
另一种方法是在同一毫秒内生成的ID数量超过序列号限制时,等待到下一个毫秒。这种方法在极端高并发情况下可能会导致性能瓶颈。
3. 使用多实例
您可以部署多个生成器实例,每个实例有独立的机器ID和数据中心ID,通过负载均衡来分配请求,以减少单个实例的负载。
4. 分布式ID生成服务
使用分布式ID生成服务来分担负载,确保生成唯一ID的同时提升系统的扩展性和可靠性。
5. 混合策略
结合多种策略,如时间回拨和多实例,来应对极高并发的需求。
实现示例:扩展序列号位数
以下是一个扩展序列号位数的示例实现:
public class SnowflakeIdGeneratorExtended {
private final long epoch = 1609459200000L; // 自定义纪元 (2021-01-01)
private final long dataCenterIdBits = 5L;
private final long workerIdBits = 5L;
private final long sequenceBits = 14L; // 扩展序列号位数到14位
private final long maxDataCenterId = (1L << dataCenterIdBits) - 1;
private final long maxWorkerId = (1L << workerIdBits) - 1;
private final long sequenceMask = (1L << sequenceBits) - 1;
private final long workerIdShift = sequenceBits;
private final long dataCenterIdShift = sequenceBits + workerIdBits;
private final long timestampShift = sequenceBits + workerIdBits + dataCenterIdBits;
private long dataCenterId;
private long workerId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGeneratorExtended(long dataCenterId, long workerId) {
if (dataCenterId > maxDataCenterId || dataCenterId < 0) {
throw new IllegalArgumentException("dataCenterId must be between 0 and " + maxDataCenterId);
}
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException("workerId must be between 0 and " + maxWorkerId);
}
this.dataCenterId = dataCenterId;
this.workerId = workerId;
}
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds");
}
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - epoch) << timestampShift) |
(dataCenterId << dataCenterIdShift) |
(workerId << workerIdShift) |
sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
public static void main(String[] args) {
SnowflakeIdGeneratorExtended idGenerator = new SnowflakeIdGeneratorExtended(1, 1);
for (int i = 0; i < 10000; i++) {
System.out.println(idGenerator.nextId());
}
}
}
在这个示例中,序列号部分扩展到14位,这样每毫秒可以生成的ID数量增加到16384个(2^14)。这种扩展虽然增加了每毫秒可以生成的ID数量,但同时减少了其他部分的位数,可能不适用于所有场景。
使用多实例的方案
在分布式系统中,使用多实例来生成ID也是一个可行的方案。例如,将请求负载均衡到不同的实例,每个实例使用不同的数据中心ID和机器ID,这样可以大幅度减少单个实例的负载。
public class SnowflakeIdGeneratorMultiInstance {
private static final int MAX_INSTANCES = 32;
private static final SnowflakeIdGenerator[] GENERATORS = new SnowflakeIdGenerator[MAX_INSTANCES];
static {
for (int i = 0; i < MAX_INSTANCES; i++) {
GENERATORS[i] = new SnowflakeIdGenerator(1, i);
}
}
public static long generateId(int instanceId) {
return GENERATORS[instanceId % MAX_INSTANCES].nextId();
}
public static void main(String[] args) {
for (int i = 0; i < 100000; i++) {
System.out.println(generateId(i));
}
}
}
在这个示例中,使用32个实例,每个实例使用不同的机器ID,通过简单的负载均衡分配请求。
总结
如果系统需要在极高并发(每毫秒超过4096个ID)下生成唯一ID,可以考虑以下解决方案:
- 扩展序列号位数:增加序列号部分的位数,增加每毫秒可生成的ID数量。
- 时间回拨:在同一毫秒内生成的ID数量超过限制时,等待下一毫秒。
- 使用多实例:通过负载均衡将请求分配到多个ID生成实例,每个实例使用不同的机器ID和数据中心ID。
- 分布式ID生成服务:使用专业的分布式ID生成服务,如 Twitter的Snowflake,MongoDB的ObjectId,或Apache的Kudu等。
如何做到订单号又短又不重复?
1. 基于时间的订单号生成
使用时间戳和自增序列的组合,可以生成唯一且有序的订单号。为了确保订单号的长度限制,需要进行一些特殊处理。
方案一:短时间戳 + 自增序列
- 时间戳:使用自定义的短时间戳,如年月日时分秒。
- 自增序列:在高并发的情况下,使用自增序列或分布式唯一ID生成器来确保同一秒内的唯一性。
- 数据中心和机器ID:可以嵌入一些数据中心和机器ID信息。
示例代码:
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.atomic.AtomicInteger;
public class OrderIdGenerator {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyMMddHHmmss");
private static final AtomicInteger counter = new AtomicInteger(0);
private static final int MAX_COUNTER = 999;
public static synchronized String generateOrderId() {
// 获取当前时间的短时间戳
String timestamp = dateFormat.format(new Date());
// 自增序列,达到最大值后重置
int count = counter.getAndIncrement();
if (count > MAX_COUNTER) {
counter.set(0);
count = counter.getAndIncrement();
}
// 格式化自增序列为三位
return String.format("%s%03d", timestamp, count);
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println(generateOrderId());
}
}
}
在这个方案中,订单号的格式为 yyMMddHHmmssSSS
+ 自增序列的组合。生成的订单号长度为12位,格式如下:
- 时间戳:6位(如
210615
表示 2021年6月15日)。 - 自增序列:3位(如
001
,002
,最多999)。
2. 基于随机数和哈希的订单号生成
使用随机数生成唯一订单号,并通过哈希函数确保唯一性。
方案二:短随机数 + 哈希校验
- 随机数:生成一个较短的随机数。
- 哈希校验:使用哈希函数来校验和生成唯一的订单号。
示例代码:
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
public class OrderIdGeneratorHash {
private static final int ORDER_ID_LENGTH = 12;
private static final Random random = new Random();
private static final Set<String> existingOrderIds = new HashSet<>();
public static synchronized String generateOrderId() {
String orderId;
do {
orderId = generateRandomOrderId();
} while (existingOrderIds.contains(orderId));
existingOrderIds.add(orderId);
return orderId;
}
private static String generateRandomOrderId() {
StringBuilder orderId = new StringBuilder(ORDER_ID_LENGTH);
for (int i = 0; i < ORDER_ID_LENGTH; i++) {
orderId.append(random.nextInt(10)); // 生成0-9之间的随机数字
}
return orderId.toString();
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println(generateOrderId());
}
}
}
在这个方案中,订单号完全是随机生成的,并且使用一个Set来确保唯一性。实际生产环境中,需要有更高效的去重方法(如分布式缓存)。
3. 混合使用时间和随机数
结合时间戳和随机数,既保证有序性又保证唯一性。
方案三:时间戳 + 随机数
- 时间戳:使用短时间戳。
- 随机数:生成一定长度的随机数。
示例代码:
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
public class OrderIdGeneratorMixed {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyMMddHHmmss");
private static final Random random = new Random();
private static final int RANDOM_LENGTH = 4;
public static synchronized String generateOrderId() {
// 获取当前时间的短时间戳
String timestamp = dateFormat.format(new Date());
// 生成随机数
String randomNumber = String.format("%04d", random.nextInt(10000)); // 生成4位随机数
return timestamp + randomNumber;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println(generateOrderId());
}
}
}
在这个方案中,订单号的格式为 yyMMddHHmmss
+ 4位随机数,生成的订单号长度为16位。例如:2106151200001234
。
总结
- 方案一:基于时间戳和自增序列,适合有严格时间顺序要求的场景。
- 方案二:基于随机数和哈希校验,适合没有严格时间顺序要求但需要高并发生成订单号的场景。
- 方案三:结合时间戳和随机数,适合需要平衡有序性和唯一性的场景。