在分布式系统中,经常需要使用
全局唯一ID
查找对应的数据。产生这种ID需要保证系统全局唯一,而且要高性能以及占用相对较少的空间。
全局唯一ID在数据库中一般会被设成
主键,这样为了保证数据插入时索引的快速建立,还需要保持一个有序的趋势。
这样全局唯一ID就需要保证这两个需求:
全局ID产生的几种方式
数据库自增
当服务使用的数据库只有单库单表时,可以利用数据库的
auto_increment
来生成全局唯一递增ID.
优势:
- 简单,无需程序任何附加操作
- 保持定长的增量
- 在单表中能保持唯一性
劣势:
- 高并发下性能不佳,主键产生的性能上限是数据库服务器单机的上限。
- 水平扩展困难,在分布式数据库环境下,无法保证唯一性。
UUID
一般的语言中会自带UUID的实现,比如Java中UUID方式
UUID.randomUUID().toString()
,可以通过服务程序本地产生,ID的生成不依赖数据库的实现。
优势:
- 本地生成ID,不需要进行远程调用。
- 全局唯一不重复。
- 水平扩展能力非常好。
劣势:
- ID有128 bits,占用的空间较大,需要存成字符串类型,索引效率极低。
- 生成的ID中没有带Timestamp,无法保证趋势递增
snowflake
是twitter开源的分布式ID生成算法,其核心思想是:产生一个long型的ID,使用其中41bit作为毫秒数,10bit作为机器编号,12bit作为毫秒内序列号。这个算法单机每秒内理论上最多可以生成
1000*(2^12)
个,也就是大约
400W
的ID,完全能满足业务的需求。
根据
snowflake
算法的思想,我们可以根据自己的业务场景,产生自己的全局唯一ID。因为Java中
long
类型的长度是64bits,所以我们设计的ID需要控制在64bits。
比如我们设计的ID包含以下信息:
| 41 bits: Timestamp | 3 bits: 区域 | 10 bits: 机器编号 | 10 bits: 序列号 |
产生唯一ID的
Java
代码:
import java.security.SecureRandom;
public class CustomUUID {
private long twepoch = 1288834974657L;
private final static long regionIdBits = 3L;
private final static long workerIdBits = 10L;
private final static long sequenceBits = 10L;
private final static long maxRegionId = -1L ^ (-1L << regionIdBits);
private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);
private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
private final static long workerIdShift = sequenceBits;
private final static long regionIdShift = sequenceBits + workerIdBits;
private final static long timestampLeftShift = sequenceBits + workerIdBits + regionIdBits;
private static long lastTimestamp = -1L;
private long sequence = 0L;
private final long workerId;
private final long regionId;
public CustomUUID(long workerId, long regionId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException("worker Id can't be greater than %d or less than 0");
}
if (regionId > maxRegionId || regionId < 0) {
throw new IllegalArgumentException("datacenter Id can't be greater than %d or less than 0");
}
this.workerId = workerId;
this.regionId = regionId;
}
public CustomUUID(long workerId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException("worker Id can't be greater than %d or less than 0");
}
this.workerId = workerId;
this.regionId = 0;
}
public long generate() {
return this.nextId(false, 0);
}
private synchronized long nextId(boolean isPadding, long busId) {
long timestamp = timeGen();
long paddingnum = regionId;
if (isPadding) {
paddingnum = busId;
}
if (timestamp < lastTimestamp) {
try {
throw new Exception("Clock moved backwards. Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds");
} catch (Exception e) {
e.printStackTrace();
}
}
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tailNextMillis(lastTimestamp);
}
} else {
sequence = new SecureRandom().nextInt(10);
}
lastTimestamp = timestamp;
return ((timestamp - twepoch) << timestampLeftShift) | (paddingnum << regionIdShift) | (workerId << workerIdShift) | sequence;
}
private long tailNextMillis(final long lastTimestamp) {
long timestamp = this.timeGen();
while (timestamp <= lastTimestamp) {
timestamp = this.timeGen();
}
return timestamp;
}
protected long timeGen() {
return System.currentTimeMillis();
}
}
使用自定义的这种方法需要注意的几点:
- 为了保持增长的趋势,要避免有些服务器的时间早,有些服务器的时间晚,需要控制好所有服务器的时间,而且要避免
NTP时间服务器
回拨服务器的时间。
- 在跨毫秒时,序列号总是归0,会使得序列号为0的ID比较多,导致生成的ID取模后不均匀,所以序列号不是每次都归0,而是归一个0到9的随机数。
- 使用这个
CustomUUID
类,最好在一个系统中能保持单例模式运行。