分布式环境下自增ID的解决方案中,Snowflake是无疑是非常优秀的选择了。代码同样值得我们学习,把位运算运用得淋漓尽致。
更详细的内容大家看代码吧。
package cn.tx.idwoker;
/*
ID的构成,位长64,首位是符号为,不能使用,剩下63位长。41位的时间戳 + 5位机房编号 + 5位机器编号 + 12位的顺序数字。
对于某台机器来说,一个号码内产生4096个序列号(0~4095),就是2的12次方个,应该是够用了
* */
public class IdWorker {
private long workerId;
private long datacenterId;
private long sequence;
/*twepoch : (twitter epoch 推特纪元)时间戳计数基准点,我们用41位字节存放的是实时获取的时间戳减去这个基准点的差。
通过用new Date(twepock)转化为日期,发现结果是2010-11-04 09:42:54,应该是Twitter公司写函数前的一个日期
我们做项目时可以修改成当前System.currentTimeMillis()对应的long值。
如果不设置这个基准时间,那生产的ID最多能用到1970 + 69 = 2039,没几年了。(1970年的某个时间的时间戳是0,是计数起点)
如果使用Twitter这个时间,那么可以用到2010 + 63 = 2079年。
所以改成项目启动时间之前的某个时间戳,就可以用到从目前开始计算的第69年。这辈子够用了,至于百年老店,到时候早就是128位操作系统了。
解释:
容量为啥是69年,这里用41位长记录时间戳,41位最大数字是2199023255551L,大约是69年的时间。
拓展:
程序语言受电脑系统影响,而现代电脑系统都受到Unix系统的广泛影响,而1970年1月1日这个时间正是Unix系统的起始时间(epoch·time)。
所以每个时间戳都以自从1970年1月1日午夜(历元)经过了多长时间来表示
*/
private long twepoch = 1288834974657L;
private long workerIdBits = 5L;
private long datacenterIdBits = 5L;
/*机器编号能使用的最大数字。
二进制运算,-1的二进制是1111...1111,即64位都是1。把它左移5位就是59个1和5个0
然后再和另一个-1做异或预算,就是使其翻转,变成59个0和5个1,也就十进制的31了。
其实就是获得5个长度的二进制能表达的最大数字
*/
private long maxWorkerId = -1L ^ (-1L << workerIdBits); //31
//机房编号能使用的最大数字,原理同前
private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); //31
//顺序数字位长。
private long sequenceBits = 12L;
//顺序数字的最大值,原理同前
private long sequenceMask = -1L ^ (-1L << sequenceBits);
private long lastTimestamp = -1L;
//位移量定义,生产四种编码数字后,要位移到指定位置,然后进行与操作,得到最后的ID
private long workerIdShift = sequenceBits;
private long datacenterIdShift = sequenceBits + workerIdBits;
private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
public synchronized long nextId() {
// 这儿就是获取当前时间戳
long timestamp = timeGen();
/*
机器时间不能回调,回调就生成重复的ID了,这是这个算法的掣肘所在。
* */
if (timestamp < lastTimestamp) {
System.err.printf("clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
throw new RuntimeException(String.format(
"Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
//时间戳发生变化是,顺序号清零。时间戳不发生变化时
if (lastTimestamp == timestamp) {
// 取下个顺序号,并判读顺序号是否超过4095,超过则等到下一毫秒的到来。不超过则使用该顺序号。
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
// 这儿记录一下最近一次生成id的时间戳,单位是毫秒
lastTimestamp = timestamp;
// 这儿就是将时间戳左移,放到 41 bit那儿;
// 将机房 id左移放到 5 bit那儿;
// 将机器id左移放到5 bit那儿;将序号放最后12 bit;
// 最后拼接起来成一个 64 bit的二进制数字,转换成 10 进制就是个 long 型
return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift)
| (workerId << workerIdShift) | sequence;
}
public IdWorker(long workerId, long datacenterId, long sequence) {
// sanity check for workerId
// 检查了传递进来的机房编码和机器编码不能超过31,不小于0
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(
String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(
String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
System.out.printf(
"worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d\n",
timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);
this.workerId = workerId;
this.datacenterId = datacenterId;
this.sequence = sequence;
}
public long getWorkerId() {
return workerId;
}
public long getDatacenterId() {
return datacenterId;
}
public long getTimestamp() {
return System.currentTimeMillis();
}
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) {
IdWorker worker = new IdWorker(1, 1, 1);
for (int i = 0; i < 30; i++) {
System.out.println(worker.nextId());
}
}
}
//.... by T.G.