1 概述
分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。
为了满足Twitter每秒上万条消息的请求,每条消息都必须分配一条唯一的id,这些id还需要一些大致的顺序(方便客户端排序),并且在分布式系统中不同机器产生的id必须不同。
2 结构
snowflake生成64的id,刚好使用long来保存,结构如下:
1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0
41位时间截,注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69
10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId
12位序列,同一时间截,同一机器,可以生成4096个id
snowflake生成的ID整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞
3 源码及注释
public class IdWorker {
//开始该类生成ID的时间截,1288834974657 (Thu, 04 Nov 2010 01:42:54 GMT) 这一时刻到当前时间所经过的毫秒数,占 41 位(还有一位是符号位,永远为 0)。
private final long startTime = 1463834116272L;
//机器id所占的位数
private long workerIdBits = 5L;
//数据标识id所占的位数
private long datacenterIdBits = 5L;
//支持的最大机器id,结果是31,这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数(不信的话可以自己算一下,记住,计算机中存储一个数都是存储的补码,结果是负数要从补码得到原码)
private long maxWorkerId = -1L ^ (-1L << workerIdBits);
//支持的最大数据标识id
private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
//序列在id中占的位数
private long sequenceBits = 12L;
//机器id向左移12位
private long workerIdLeftShift = sequenceBits;
//数据标识id向左移17位
private long datacenterIdLeftShift = workerIdBits + workerIdLeftShift;
//时间截向左移5+5+12=22位
private long timestampLeftShift = datacenterIdBits + datacenterIdLeftShift;
//生成序列的掩码,这里为1111 1111 1111
private long sequenceMask = -1 ^ (-1 << sequenceBits);
private long workerId;
private long datacenterId;
//同一个时间截内生成的序列数,初始值是0,从0开始
private long sequence = 0L;
//上次生成id的时间截
private long lastTimestamp = -1L;
public IdWorker(long workerId, long datacenterId){
if(workerId < 0 || workerId > maxWorkerId){
throw new IllegalArgumentException(
String.format("workerId[%d] is less than 0 or greater than maxWorkerId[%d].", workerId, maxWorkerId));
}
if(datacenterId < 0 || datacenterId > maxDatacenterId){
throw new IllegalArgumentException(
String.format("datacenterId[%d] is less than 0 or greater than maxDatacenterId[%d].", datacenterId, maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
//生成id
public synchronized long nextId(){
long timestamp = timeGen();
if(timestamp < lastTimestamp){
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
//如果是同一时间生成的,则自增
if(timestamp == lastTimestamp){
sequence = (sequence + 1) & sequenceMask;
if(sequence == 0){
//生成下一个毫秒级的序列
timestamp = tilNextMillis();
//序列从0开始
sequence = 0L;
}
}else{
//如果发现是下一个时间单位,则自增序列回0,重新自增
sequence = 0L;
}
lastTimestamp = timestamp;
//看本文第二部分的结构图,移位并通过或运算拼到一起组成64位的ID
return ((timestamp - startTime) << timestampLeftShift)
| (datacenterId << datacenterIdLeftShift)
| (workerId << workerIdLeftShift)
| sequence;
}
protected long tilNextMillis(){
long timestamp = timeGen();
if(timestamp <= lastTimestamp){
timestamp = timeGen();
}
return timestamp;
}
protected long timeGen(){
return System.currentTimeMillis();
}
public static void main(String[] args) {
class IdServiceThread implements Runnable {
private Set<Long> set;
@Autowired
private IdService idService;
public IdServiceThread(Set<Long> set, IdService idService) {
this.set = set;
this.idService = idService;
}
@Override
public void run() {
while (true) {
long id = idService.nextId();
System.out.println("duplicate:" + id);
if (!set.add(id)) {
System.out.println("duplicate:" + id);
}
}
}
}
Set<Long> set = new HashSet<Long>();
for(int i=0;i<100;i++){
Thread t1 = new Thread(new IdServiceThread(set, idService));
t1.setDaemon(true);
t1.start();
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
着重说一下-1L ^ (-1L << workerIdBits)
可以用System.out.println检测一下,结果是31,那么具体是怎么操作的?
首先要明白,计算机中储存数据都是补码。这样做的好处在于,采用补码运算时,由于符号位与数值一样参与运算,所以不必像原码运算那样对两数的大小、符号作比较,从而使运算更简单。
-1L补码: 1111 1111
左移五位:1110 0000
异或操作:0001 1111
也就是最终的结果31