数据库中生成主键的方式及其优缺点
一、自动增长(AUTO_INCREMENT)
使用方法:设置auto_increment 实现数据表自增;
优点:
- 简单易用:自增主键是一种简单的方式,只需在数据库表中设置自增属性即可,无需在代码中进行繁琐的处理。
- 唯一性:自增主键保证了每个新记录都有唯一的主键值,避免了主键冲突的可能性。
缺点:
- 可预测性:自增主键通常是按照顺序递增的,这意味着主键值的分布是有序的。有些情况下,这可能会暴露敏感信息,例如可以推测出系统的数据规模或活跃程度。
- 不适用于分布式环境:在分布式环境下,不同节点生成的自增主键可能会冲突,导致主键冲突问题。此时需要额外的机制来保证主键的唯一性。
- 不可回收:一旦使用了自增主键,删除的记录之后产生的主键值将不会被再次使用,可能导致主键的浪费。
- 分表实现比较麻烦:在分库分表中,保证每张表实现自增同时,不同表之间还得保证连续。所以分表实现比较麻烦;假设有三张表,主键分别为下图所示的id,当我们操作第二张表的时候需要根据上一张表的最后那个数据的id值来进行划分。
适用场景:
- 单节点系统:在单节点的系统中,自增主键是一种方便且高效的方式,不需要额外的复杂处理逻辑。
- 数据规模较小且无需保密性的系统:如果数据规模较小且无需保密性,使用自增主键可以简化开发过程,减少复杂性。
- 需要确保主键的唯一性的系统:自增主键可以确保每个记录都有唯一的主键值,避免了主键冲突的可能性。
二、使用JDK生成UUID作为主键
使用方法:
在Java中,可以使用JDK提供的UUID类来生成UUID。以下是生成UUID的示例代码:
import java.util.UUID;public class Main {public static void main(String[] args) {// 生成新的UUIDUUID uuid = UUID.randomUUID();System.out.println(uuid.toString());// 将UUID转换为字符串形式,去掉中间的"-"符号String uuidString = uuid.toString().replace("-", "");System.out.println(uuidString);}
}
上述代码首先使用UUID.randomUUID()
方法生成一个新的UUID对象,然后可以使用toString()
方法将UUID转换为字符串形式。如果想去掉中间的"-"符号,可以使用replace("-", "")
方法进行替换。最后,可以通过调用System.out.println()
方法将UUID打印出来。
优点:
- 唯一性:UUID是基于时间戳、网卡MAC地址和随机数生成的,几乎可以保证全球范围内的唯一性。
- 生成简单:JDK中提供了UUID类,可以很方便地生成UUID。
- 可以在分布式环境中使用:UUID的唯一性使其在分布式系统中可以作为全局唯一标识符。
缺点:
- 长度较长:UUID是由16个字节组成的128位数字,相比于自增长的整数主键,长度较长。
- 不易读:UUID通常以字符串的形式表示,由一串数字和字母组成,不易于人类阅读。
适用场景:
- 数据库表主键:在数据库表中,使用UUID作为主键可以保证数据的唯一性。
- 分布式系统中的唯一标识符:在分布式系统中,使用UUID可以生成全局唯一的标识符,用于唯一标识不同节点或对象。
三、使用雪花算法(Snowflake)生成的ID作为主键
使用方法:
以下是一个简单的示例代码,演示如何使用Java实现雪花算法生成唯一ID:
public class SnowflakeIdGenerator {// 起始的时间戳,2022-01-01 00:00:00private final long twepoch = 1641004800000L;// 每个部分占用的位数private final long workerIdBits = 5L; // 机器标识位数private final long dataCenterIdBits = 5L; // 数据中心标识位数private final long sequenceBits = 12L; // 序列号位数// 每个部分的最大值private final long maxWorkerId = -1L ^ (-1L << workerIdBits);private final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits);private final long sequenceMask = -1L ^ (-1L << sequenceBits);// 每个部分向左的位移private final long workerIdShift = sequenceBits;private final long dataCenterIdShift = sequenceBits + workerIdBits;private final long timestampShift = sequenceBits + workerIdBits + dataCenterIdBits;private long workerId; // 机器IDprivate long dataCenterId; // 数据中心IDprivate long sequence = 0L; // 序列号private long lastTimestamp = -1L; // 上次生成ID的时间戳public SnowflakeIdGenerator(long workerId, long dataCenterId) {if (workerId > maxWorkerId || workerId < 0) {throw new IllegalArgumentException("Worker ID超出范围");}if (dataCenterId > maxDataCenterId || dataCenterId < 0) {throw new IllegalArgumentException("Data Center ID超出范围");}this.workerId = workerId;this.dataCenterId = dataCenterId;}public synchronized long generateId() {long timestamp = System.currentTimeMillis();// 如果当前时间小于上次生成ID的时间戳,说明系统时钟回退过,抛出异常if (timestamp < lastTimestamp) {throw new RuntimeException("系统时钟回退,无法生成ID");}// 如果是同一毫秒内生成的ID,自增序列号if (timestamp == lastTimestamp) {sequence = (sequence + 1) & sequenceMask;// 序列号溢出,等待下一毫秒if (sequence == 0) {timestamp = tilNextMillis(lastTimestamp);}} else {sequence = 0L;}lastTimestamp = timestamp;return ((timestamp - twepoch) << timestampShift)| (dataCenterId << dataCenterIdShift)| (workerId << workerIdShift)| sequence;}private long tilNextMillis(long lastTimestamp) {long timestamp = System.currentTimeMillis();while (timestamp <= lastTimestamp) {timestamp = System.currentTimeMillis();}return timestamp;}public static void main(String[] args) {SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1, 1);long id = idGenerator.generateId();System.out.println("生成的ID:" + id);}
}
以上代码演示了一个简单的雪花算法ID生成器。在main方法中,创建了一个SnowflakeIdGenerator对象,然后调用generateId方法生成一个唯一的ID,并将其打印出来。
在实际应用中,需要根据需要调整各个部分的位数,以及机器标识和数据中心标识的取值范围。此外,为了确保在多线程环境下的线程安全性,生成ID的方法被声明为synchronized。
优点:
- 全局唯一性:雪花算法生成的ID在全局范围内是唯一的,可以用于分布式系统中避免主键冲突的问题。
- 有序性:由于雪花算法生成的ID是基于时间戳的,所以生成的ID是按照时间先后顺序排序的,可以方便地根据ID查询按时间排序的数据。
缺点:
- 依赖机器时钟:雪花算法生成的ID中的时间戳依赖于机器的时钟,如果机器的时钟回拨或者不同机器的时钟不同步,可能会导致生成的ID不唯一或者不符合预期的顺序。
- 可读性较差:雪花算法生成的ID是一个64位的整数,对于人类来说可读性较差,不如自增ID或者UUID直观。
适用场景:
- 数据库分库分表:在分库分表的系统中,需要生成唯一的主键来保证数据的一致性和查询的效率。
- 分布式任务调度:在分布式任务调度系统中,每个任务需要有一个唯一的标识作为主键,用于任务的调度和追踪。
- 分布式日志记录:在分布式系统中,每条日志需要有一个唯一的ID,方便日志的聚合和查询。
四、使用Redis的原子命令INCR或INCRBY生成主键
使用方法:
在Java中使用Redis生成主键,可以使用Redis的原子命令INCR或INCRBY。
示例代码如下:
import redis.clients.jedis.Jedis;public class RedisPrimaryKeyGenerator {private static final String REDIS_HOST = "localhost";private static final int REDIS_PORT = 6379;private static final String KEY_NAME = "primary_key";public static void main(String[] args) {// 创建Jedis对象Jedis jedis = new Jedis(REDIS_HOST, REDIS_PORT);// 生成主键long primaryKey = generatePrimaryKey(jedis);System.out.println("Generated Primary Key: " + primaryKey);// 关闭连接jedis.close();}private static long generatePrimaryKey(Jedis jedis) {// 使用INCR命令递增主键return jedis.incr(KEY_NAME);}
}
上述代码通过创建Jedis对象连接到Redis服务器,然后使用jedis.incr(KEY_NAME)
命令递增名为"primary_key"的键,从而生成唯一的主键。
优点:
- 高性能:Redis是一个内存数据库,具有非常高的读写性能,可以快速生成主键。
- 高并发:Redis支持多线程并发访问,可以同时处理多个生成主键的请求。
- 可扩展性:Redis支持分布式部署,可以通过搭建Redis集群来提高生成主键的容量和吞吐量。
- 高可靠性:Redis具有数据持久化的功能,可以在发生故障时快速恢复数据。
缺点:
- 单点故障:如果Redis实例发生故障,可能会导致生成主键的过程停止。
- 数据一致性:由于Redis是内存数据库,数据可能会发生丢失或不一致的情况,造成生成的主键不可用。
适用场景:
- 分布式系统:当多个节点需要生成唯一的主键时,可以使用Redis作为共享的主键生成器。
- 快速生成唯一ID:如果需要在高并发场景下快速生成唯一的ID,使用Redis可以提高性能和并发能力。
- 分布式锁:可以通过Redis生成的主键来实现分布式锁机制,保证系统的并发安全性。