数据库中的主键是保证数据表中每行记录唯一性的关键。一个良好的主键生成策略对于数据库的性能、可扩展性和可靠性至关重要。本文将深入探讨主键生成策略的多种方法,分析其优缺点,并提供实际应用中的案例分析。

一、主键生成策略概述

主键生成策略主要分为两大类:自增主键和业务主键。

1. 自增主键

自增主键是数据库中最常见的主键生成方式,通过在表结构中设置一个自增字段,每次插入新记录时,该字段会自动增加一个值。

优点:

  • 简单易用,无需手动干预。
  • 性能高,数据库优化器可以针对自增主键进行优化。

缺点:

  • 可扩展性差,当数据量非常大时,自增主键可能会出现冲突。
  • 不便于分布式系统中的数据同步。

2. 业务主键

业务主键是根据业务需求生成的主键,通常由多个字段组合而成。

优点:

  • 可扩展性好,适用于分布式系统。
  • 可以根据业务需求灵活调整。

缺点:

  • 复杂性高,需要设计合理的组合规则。
  • 性能相对较低,因为需要计算组合字段的值。

二、常见的主键生成策略

1. UUID

UUID(通用唯一识别码)是一种基于128位数的随机数生成的主键策略。

优点:

  • 唯一性极高,几乎不可能出现重复。
  • 不依赖于数据库的任何操作。

缺点:

  • 存储空间占用大。
  • 不便于排序和索引。

2. Snowflake 算法

Snowflake 算法是一种基于时间戳和自增序列生成主键的策略。

public class SnowflakeIdWorker {
    private long workerId;
    private long datacenterId;
    private long sequence = 0L;

    private long twepoch = 1288834974657L;

    private long workerIdBits = 5L;
    private long datacenterIdBits = 5L;
    private long maxWorkerId = -1L ^ (-1L << workerIdBits);
    private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
    private long sequenceBits = 12L;

    private long workerIdShift = sequenceBits;
    private long datacenterIdShift = sequenceBits + workerIdBits;
    private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
    private long sequenceMask = -1L ^ (-1L << sequenceBits);

    private long lastTimestamp = -1L;

    public SnowflakeIdWorker(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    public synchronized long nextId() {
        long timestamp = timeGen();

        if (timestamp < lastTimestamp) {
            throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }

        lastTimestamp = timestamp;

        return ((timestamp - twepoch) << timestampLeftShift) | (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();
    }
}

优点:

  • 高效,性能好。
  • 唯一性高,适用于分布式系统。

缺点:

  • 不便于排序和索引。

3. 数据库序列

数据库序列是数据库内部提供的主键生成方式,通过调用数据库的序列生成函数来获取主键值。

优点:

  • 简单易用,无需手动干预。
  • 性能高,数据库优化器可以针对序列进行优化。

缺点:

  • 可扩展性差,不便于分布式系统中的数据同步。

三、案例分析

以下是一个使用 Snowflake 算法生成主键的案例分析:

public class SnowflakeIdWorkerTest {
    public static void main(String[] args) {
        SnowflakeIdWorker idWorker = new SnowflakeIdWorker(1, 1);
        for (int i = 0; i < 10; i++) {
            long id = idWorker.nextId();
            System.out.println(id);
        }
    }
}

输出结果如下:

1638388337985224
1638388337985225
1638388337985226
1638388337985227
1638388337985228
1638388337985229
1638388337985230
1638388337985231
1638388337985232
1638388337985233

四、总结

选择合适的主键生成策略对于数据库的性能和可靠性至关重要。本文介绍了多种主键生成策略,并分析了它们的优缺点。在实际应用中,应根据业务需求和系统架构选择合适的主键生成策略。