分布式环境下生成全剧唯一的ID的方式:
一、UUID
生成比较简单,能保证全局唯一性;但是生成的是无序的ID序列,作为主键ID性能较差(无法使用索引)
二、数据库主键自增
可以根据数据库的自增来实现,分布式系统可以为多个数据库节点设置自增步长保证全局唯一;但是这样需要依赖数据库,不但性能较差而且数据库挂掉会导致服务不可用
id feild //数据库字段
35 a //用于获取自增ID的记录
//通过下面的语句可以获取自增长的ID
begin;
REPLACE INTO table ( feild ) VALUES ( 'a' );
SELECT LAST_INSERT_ID();
commit;
三、使用SnowFlake算法
存储结构:
总共分四部分:
1、第一位:占用1bit,没实际作用
2、时间戳:占41bit,精确到毫秒,总共可以容纳约69年时间
3、工作机器id:占10bit,高5位是数据中心id(datacenterId),低5位是工作节点id(workId),最多可以容纳1024个节点
4、序列号:占用12bit,这个值在同一毫秒同一节点上从0开始不断累加,最多可以累加到4095
SnowFlake算法在一毫秒内最多可以生成1024*4096=4194304个全局唯一ID
代码实现:
//初始时间截 (2017-01-01)
private static final long INITIAL_TIME_STAMP = 1483200000000L;
//机器id所占的位数
private static final long WORKER_ID_BITS = 5L;
//数据标识id所占的位数
private static final long DATACENTER_ID_BITS = 5L;
//支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
//支持的最大数据标识id,结果是31
private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS);
//序列在id中占的位数
private final long SEQUENCE_BITS = 12L;
//机器ID的偏移量(12)
private final long WORKERID_OFFSET = SEQUENCE_BITS;
//数据中心ID的偏移量(12+5)
private final long DATACENTERID_OFFSET = SEQUENCE_BITS + SEQUENCE_BITS;
//时间截的偏移量(5+5+12)
private final long TIMESTAMP_OFFSET = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;
//生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
private final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
//工作节点ID(0~31)
private long workerId;
//数据中心ID(0~31)
private long datacenterId;
//毫秒内序列(0~4095)
private long sequence = 0L;
//上次生成ID的时间截
private long lastTimestamp = -1L;
/**
* 构造函数
*
* @param workerId 工作ID (0~31)
* @param datacenterId 数据中心ID (0~31)
*/
public SnowFlakeIdGenerator(long workerId, long datacenterId) {
if (workerId > MAX_WORKER_ID || workerId < 0) {
throw new IllegalArgumentException(String.format("WorkerID 不能大于 %d 或小于 0", MAX_WORKER_ID));
}
if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
throw new IllegalArgumentException(String.format("DataCenterID 不能大于 %d 或小于 0", MAX_DATACENTER_ID));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
/**
* 获得下一个ID (用同步锁保证线程安全)
*
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException("当前时间小于上一次记录的时间戳!");
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
//sequence等于0说明毫秒内序列已经增长到最大值
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - INITIAL_TIME_STAMP) << TIMESTAMP_OFFSET) | (datacenterId << DATACENTERID_OFFSET) | (workerId << WORKERID_OFFSET) | sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
*
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
public static void main(String[] args) {
final SnowFlakeIdGenerator idGenerator = new SnowFlakeIdGenerator(1, 1);
//线程池并行执行10000次ID生成
ExecutorService executorService = Executors.newCachedThreadPool();;
for (int i = 0; i < 10000; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
long id = idGenerator.nextId();
System.out.println(id);
}
});
}
executorService.shutdown();
}
有几点注意:
1.获得单一机器的下一个序列号,使用Synchronized控制并发,而非CAS的方式,是因为CAS不适合并发量非常高的场景。
2.如果当前毫秒在一台机器的序列号已经增长到最大值4095,则使用while循环等待直到下一毫秒。
3.如果当前时间小于记录的上一个毫秒值,则说明这台机器的时间回拨了,抛出异常。但如果这台机器的系统时间在启动之前回拨过,那么有可能出现ID重复的危险
SnowFlake算法的优点:
1.生成ID时不依赖于DB,完全在内存生成,高性能高可用。
2.ID呈趋势递增,后续插入索引树的时候性能较好。
SnowFlake算法的缺点:
依赖于系统时钟的一致性。如果某台机器的系统时钟回拨,有可能造成ID冲突,或者ID乱序。