分布式全局唯一ID--SnowFlake算法
说到全局唯一ID,之前做的一个项目,有遇到类似的需求,会有多并发,但是,又需要类似于id的这么个存在。当时是直接采用的UUID(这个方案实施起来效率最高),当时为了赶进度,就匆匆忙忙的上线了。现在正好来总结一下。
一般情况,实现全局唯一ID,有三种方案,分别是通过中间件方式、UUID、雪花算法。
方案一,通过中间件方式,可以是把数据库或者redis缓存作为媒介,从中间件获取ID。这种呢,优点是可以体现全局的递增趋势(优点只能想到这个),缺点呢,倒是一大堆,比如,依赖中间件,假如中间件挂了,就不能提供服务了;依赖中间件的写入和事务,会影响效率;数据量大了的话,你还得考虑部署集群,考虑走代理。这样的话,感觉问题复杂化了
方案二,通过UUID的方式,java.util.UUID就提供了获取UUID的方法,使用UUID来实现全局唯一ID,优点是操作简单,也能实现全局唯一的效果,缺点呢,就是不能体现全局视野的递增趋势;太长了,UUID是32位,有点浪费;最重要的,是插入的效率低,因为呢,我们使用mysql的话,一般都是B+tree的结构来存储索引,假如是数据库自带的那种主键自增,节点满了,会裂变出新的节点,新节点满了,再去裂变新的节点,这样利用率和效率都很高。而UUID是无序的,会造成中间节点的分裂,也会造成不饱和的节点,插入的效率自然就比较低下了。
方案三,雪花算法SnowFlake,是推特公司使用的一款通过划分命名空间并行生成的算法,来解决全局唯一ID的需求,类似的还有MongoDB的object_id。雪花算法,是64位二进制,转换十进制,不超过20位。第一位是符号位,一般是不变的0,第二阶梯的是41位的毫秒,第三阶梯是10位的机器ID,第四阶梯是12位的序列号,雪花算法能保证一毫秒内,支持1024*4096个并发,400多W了,对付绝大多数场景,都适用了。优点就是方案一和方案二的不足的反例---不依赖中间件、可以体现趋势递增,且通过第三阶梯的机器ID可以知道是哪一台机器生成的ID,缺点呢,就是因为雪花算法是依赖毫秒,而毫秒又是通过本机来获取的,假如本机的时钟回拨了,那就乱套了,可能会造成ID冲突或者ID乱序。
最后说下java的一个SnowFlake算法的实现
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SnowFlakeIdGenerator {
// 初始时间截 (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 + WORKER_ID_BITS ;
// 时间截的偏移量(5+5+12)
private final long TIMESTAMP_OFFSET = WORKER_ID_BITS + DATACENTER_ID_BITS + SEQUENCE_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;
if (sequence == 0) {// 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();
}
}
待讨论的问题:时钟回拨了,怎么办呢?只能while循环等待?
雪花算法使用起来有哪些坑?