数据库主键的设计是数据库架构中的一个重要环节,不同的主键生成策略适用于不同的场景和需求
以下是几种常见的主键设计方法及其优缺点比较:
优点:
缺点:
优点:
缺点:
雪花算法(Snowflake Algorithm)是一种用于生成唯一ID的算法,最初由Twitter公司开发。它是为了解决分布式系统中生成全局唯一ID的需求而设计的。在分布式系统中,如果不同节点生成的ID可能会发生冲突,这就需要一种机制来保证生成的ID在整个系统中唯一。
雪花算法的设计考虑了以下几个因素:
Java中如何使用雪花算法来设计数据库主键呢?下面是一个简单的示例:
public class SnowflakeIdGenerator {
// 定义机器ID,可以通过配置文件或其他方式设置
private long machineId;
// 定义序列号
private long sequence = 0L;
// 定义初始时间戳
private long twepoch = 1622874000000L; // 2021-06-05 00:00:00
// 定义各部分占位数
private long machineIdBits = 5L;
private long maxMachineId = -1L ^ (-1L << machineIdBits);
private long sequenceBits = 12L;
private long sequenceMask = -1L ^ (-1L << sequenceBits);
// 定义机器ID左移位数
private long machineIdShift = sequenceBits;
// 定义时间戳左移位数
private long timestampLeftShift = sequenceBits + machineIdBits;
// 上次生成ID的时间戳
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long machineId) {
if (machineId > maxMachineId || machineId < 0) {
throw new IllegalArgumentException("Machine ID can't be greater than " + maxMachineId + " or less than 0");
}
this.machineId = machineId;
}
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 - twepoch) << timestampLeftShift) | (machineId << machineIdShift) | 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); // 传入机器ID
for (int i = 0; i < 10; i++) {
System.out.println(idGenerator.nextId());
}
}
}
在这个示例中,我们通过nextId()
方法来生成雪花算法生成的唯一ID。首先,我们需要设置一个机器ID,确保不同的机器有不同的ID。然后,调用nextId()
方法即可生成一个唯一的ID,这个ID包含了时间戳、机器ID和序列号三部分。最后,我们可以将生成的ID作为数据库表的主键。
值得注意的是,雪花算法生成的ID是趋势递增的,因此在数据库中使用时可能会带来一定的优势,比如辅助索引的性能优化。但也要注意在高并发情况下可能出现的一些问题,比如时钟回拨等。
优点:
缺点:
然而,雪花算法依赖于时间戳,因此时钟回拨(clock rollback)会对其造成问题。
当检测到时钟回拨时,直接等待直到时间回到正确的时间。这是最简单的处理方式,但会导致 ID 生成暂停一段时间。
public class SnowflakeIdGenerator {
private long lastTimestamp = -1L;
public synchronized long nextId() {
long timestamp = timeGen();
// 如果当前时间小于上一次生成ID的时间戳,说明系统时钟回拨
if (timestamp < lastTimestamp) {
// 等待直到时钟追上
while (timestamp < lastTimestamp) {
timestamp = timeGen();
}
}
lastTimestamp = timestamp;
return generateId(timestamp);
}
private long timeGen() {
return System.currentTimeMillis();
}
private long generateId(long timestamp) {
// 生成ID的逻辑
return timestamp;
}
}
允许一定范围内的时钟回拨,在这个范围内继续生成 ID,但如果超出这个范围则抛出异常或采取其他措施。
public class SnowflakeIdGenerator {
private long lastTimestamp = -1L;
private static final long MAX_BACKWARD_MS = 5L; // 允许的最大时钟回拨时间
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
if (offset <= MAX_BACKWARD_MS) {
// 等待,直到时钟追上
try {
Thread.sleep(offset + 1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
} else {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
}
lastTimestamp = timestamp;
return generateId(timestamp);
}
private long timeGen() {
return System.currentTimeMillis();
}
private long generateId(long timestamp) {
// 生成ID的逻辑
return timestamp;
}
}
在分布式系统中,每台机器有唯一的机器 ID。当检测到时钟回拨时,改变机器 ID 来避免冲突。这种方法需要协调机器 ID 的分配。
public class SnowflakeIdGenerator {
private long lastTimestamp = -1L;
private long machineId;
private static final long MAX_MACHINE_ID = 1023L;
public SnowflakeIdGenerator(long machineId) {
if (machineId < 0 || machineId > MAX_MACHINE_ID) {
throw new IllegalArgumentException("Machine ID out of range");
}
this.machineId = machineId;
}
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
machineId = (machineId + 1) & MAX_MACHINE_ID;
if (machineId == 0) {
// 如果机器ID回到0,说明时钟回拨过大,拒绝生成ID
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
timestamp = timeGen();
}
lastTimestamp = timestamp;
return generateId(timestamp, machineId);
}
private long timeGen() {
return System.currentTimeMillis();
}
private long generateId(long timestamp, long machineId) {
// 生成ID的逻辑,包含时间戳和机器ID
return (timestamp << 22) | (machineId << 12);
}
}
以下是一个经过优化的方案,涵盖时钟回拨问题、分布式系统中的唯一性问题和高可用性问题
public class SnowflakeIdGenerator {
private static final long EPOCH = 1609459200000L; // 自定义纪元时间(2021-01-01)
private static final long DATA_CENTER_ID_BITS = 5L;
private static final long MACHINE_ID_BITS = 5L;
private static final long SEQUENCE_BITS = 12L;
private static final long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS);
private static final long MAX_MACHINE_ID = ~(-1L << MACHINE_ID_BITS);
private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS);
private static final long MACHINE_ID_SHIFT = SEQUENCE_BITS;
private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS;
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS + DATA_CENTER_ID_BITS;
private final long dataCenterId;
private final long machineId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long dataCenterId, long machineId) {
if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {
throw new IllegalArgumentException(String.format("DataCenter ID can't be greater than %d or less than 0", MAX_DATA_CENTER_ID));
}
if (machineId > MAX_MACHINE_ID || machineId < 0) {
throw new IllegalArgumentException(String.format("Machine ID can't be greater than %d or less than 0", MAX_MACHINE_ID));
}
this.dataCenterId = dataCenterId;
this.machineId = machineId;
}
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 (timestamp == lastTimestamp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - EPOCH) << TIMESTAMP_SHIFT)
| (dataCenterId << DATA_CENTER_ID_SHIFT)
| (machineId << MACHINE_ID_SHIFT)
| sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
}
为了进一步提高高可用性和唯一性,可以结合 Redis 或数据库实现分布式唯一 ID 生成。这里是一个使用 Redis 的示例:
import redis.clients.jedis.Jedis;
public class DistributedIdGenerator {
private final SnowflakeIdGenerator snowflakeIdGenerator;
private final Jedis jedis;
public DistributedIdGenerator(long dataCenterId, long machineId, String redisHost, int redisPort) {
this.snowflakeIdGenerator = new SnowflakeIdGenerator(dataCenterId, machineId);
this.jedis = new Jedis(redisHost, redisPort);
}
public long nextId() {
long id = snowflakeIdGenerator.nextId();
String key = "snowflake:" + id;
while (jedis.exists(key)) {
id = snowflakeIdGenerator.nextId();
key = "snowflake:" + id;
}
jedis.setex(key, 3600, "1"); // 设置过期时间,避免长期存储
return id;
}
}
基本雪花算法:
EPOCH
:自定义的纪元时间。DATA_CENTER_ID_BITS
、MACHINE_ID_BITS
和 SEQUENCE_BITS
:数据中心 ID、机器 ID 和序列号的位数。nextId
方法:生成唯一 ID,并处理时钟回拨问题。分布式唯一 ID:
jedis.setex(key, 3600, "1")
:使用带过期时间的键来避免长期存储。时钟回拨处理:
tilNextMillis
方法等待直到时间前进。这种方案结合了雪花算法的高性能和 Redis 的分布式存储能力,解决了时钟回拨问题,并确保在分布式环境下生成唯一 ID。通过这些措施,可以实现一个稳定、高效的分布式唯一 ID 生成系统。
选择哪种主键生成策略取决于具体的应用场景: