订单id的设计问题探讨

如何设计一个订单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. 符号位(1位)

    • 固定为 0,因为生成的 ID 为正数。
  2. 时间戳(41位)

    • 表示自定义纪元(通常是某个固定时间点,如 Unix 纪元)以来的毫秒数。
    • 41 位可以表示的毫秒数约为 69 年(2^41 - 1 毫秒)。
  3. 数据中心ID(5位)

    • 表示数据中心或机房的 ID。
    • 5 位可以表示 32 个不同的数据中心。
  4. 机器ID(5位)

    • 表示工作机器的 ID。
    • 5 位可以表示 32 台不同的机器。
  5. 序列号(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,可以考虑以下解决方案:

  1. 扩展序列号位数:增加序列号部分的位数,增加每毫秒可生成的ID数量。
  2. 时间回拨:在同一毫秒内生成的ID数量超过限制时,等待下一毫秒。
  3. 使用多实例:通过负载均衡将请求分配到多个ID生成实例,每个实例使用不同的机器ID和数据中心ID。
  4. 分布式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位(如 001002,最多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

总结

  • 方案一:基于时间戳和自增序列,适合有严格时间顺序要求的场景。
  • 方案二:基于随机数和哈希校验,适合没有严格时间顺序要求但需要高并发生成订单号的场景。
  • 方案三:结合时间戳和随机数,适合需要平衡有序性和唯一性的场景。

  • 27
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值