1. 背景
工作中当前项目采用的是雪花算法作为主键生成策略;
业务中碰到过一种情况:
个体表user,和团队表team,它们都有一个雪花算法生成的id,如何做到根据id就知道这个id是属于个体还是团体呢???
常规的做法我知道的有两种:
1.另加一个字段,字段type一直跟着这个id;
2.另加一个表,表中一个字段为id,一个字段为类型,麻烦的是要维护这个表的id和类型映射关系;
我们能不能控制雪花的生成算法去更简单有效地控制这点呢???
2. 简述雪花算法原理
SnowFlake算法生成id的结果是一个64bit大小的整数,它的结构如下图:
1) 1位,不用。二进制中最高位为1的都是负数,但是我们生成的id一般都使用整数,所以这个最高位固定是0
2) 41位,用来记录时间戳(毫秒)。
3) 41位可以表示2^41−1个数字,如果只用来表示正整数(计算机中正数包含0),可以表示的数值范围是:0 至 2^41−1,减1是因为可表示的数值范围是从0开始算的,而不是1。
也就是说41位可以表示2^41−1个毫秒的值,转化成单位年则是(2^41−1)/(1000∗60∗60∗24∗365)=69年
4) 10位,用来记录工作机器id。可以部署在2^10=1024个节点,包括5位datacenterId和5位workerId
5位(bit)可以表示的最大正整数是2^5−1=31,即可以用0、1、2、3、....31这32个数字,来表示不同的datecenterId或workerId
6) 12位,序列号,用来记录同毫秒内产生的不同id。
12位(bit)可以表示的最大正整数是2^12−1=4095,即可以用0、1、2、3、....4094这4095个数字,来表示同一机器同一时间截(毫秒)内产生的4095个ID序号
由于在Java中64bit的整数是long类型,所以在Java中SnowFlake算法生成的id就是long来存储的。
看到雪花算法的组成部分,我第一感觉就是可以在这【10bit-工作机器id】上做文章;
原因:
- 1bit符号位是不能动的
- 41bit时间戳我也不太敢动
- 最后的12bit序列号讲道理是可以动的,因为我们系统没可能达到单位时间需要生成2^12-1个id
- 最重要的是我能确定我们系统不需要5bit来区别机房,用5bit来区别机器
我考虑先拿两个bit来作为自定义标志位,即可以标记2^2=4种情况;
话不多说,我先贴出原本的雪花算法生成的代码:
@Slf4j
public class IdWorker {
// 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)
private final static long twepoch = 1288834975687L;
// 机器标识位数
private final static long workerIdBits = 5L;
// 数据中心标识位数
private final static long datacenterIdBits = 5L;
// 机器ID最大值
private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 数据中心ID最大值
private final static long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
// 毫秒内自增位
private final static long sequenceBits = 12L;
// 机器ID偏左移12位
private final static long workerIdShift = sequenceBits;
// 数据中心ID左移17位
private final static long datacenterIdShift = sequenceBits + workerIdBits;
// 时间毫秒左移22位
private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
/* 上次生产id时间戳 */
private static long lastTimestamp = -1L;
// 0,并发控制
private long sequence = 0L;
private final long workerId;
// 数据标识id部分
private final long datacenterId;
public IdWorker(){
this.datacenterId = getDatacenterId(maxDatacenterId);
this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);
}
/**
* @param workerId
* 工作机器ID
* @param datacenterId
* 序列号
*/
public IdWorker(long workerId, long datacenterId) {
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));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
/**
* 获取下一个ID
* @return
*/
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 (lastTimestamp == timestamp) {
// 当前毫秒内,则+1
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 当前毫秒内计数满了,则等待下一秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
// ID偏移组合生成最终的ID,并返回ID
long nextId = ((timestamp - twepoch) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift) | sequence;
return nextId;
}
private long tilNextMillis(final long lastTimestamp) {
long timestamp = this.timeGen();
while (timestamp <= lastTimestamp) {
timestamp = this.timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
/**
* <p>
* 获取 maxWorkerId
* </p>
*/
protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) {
StringBuffer mpid = new StringBuffer();
mpid.append(datacenterId);
String name = ManagementFactory.getRuntimeMXBean().getName();
if (!name.isEmpty()) {
/*
* GET jvmPid
*/
mpid.append(name.split("@")[0]);
}
/*
* MAC + PID 的 hashcode 获取16个低位
*/
return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
}
/**
* <p>
* 数据标识id部分
* </p>
*/
protected static long getDatacenterId(long maxDatacenterId) {
long id = 0L;
try {
InetAddress ip = InetAddress.getLocalHost();
NetworkInterface network = NetworkInterface.getByInetAddress(ip);
if (network == null) {
id = 1L;
} else {
byte[] mac = network.getHardwareAddress();
id = ((0x000000FF & (long) mac[mac.length - 1])
| (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
id = id % (maxDatacenterId + 1);
}
} catch (Exception e) {
log.info(" getDatacenterId: " + e.getMessage());
}
return id;
}
}
上面的雪花算法的java实现相信大家在网上一搜一大把;
那么重点来了,我们如何在雪花算法中做点手脚,起到标记的作用呢???
我的计划是这样的,在下图的这两个bit上做标记
重点解析nextId()方法
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 (lastTimestamp == timestamp) {
// 当前毫秒内,则+1
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 当前毫秒内计数满了,则等待下一秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
// ID偏移组合生成最终的ID,并返回ID
long nextId = ((timestamp - twepoch) << timestampLeftShift) <<<---------------
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift) | sequence;
return nextId;
}
重点关注一下返回前组合nextId的那个位移操作
// (41bit)时间毫秒左移22位
long nextId = ((timestamp - twepoch) << timestampLeftShift)
// (5bit-->4bit)数据中心ID左移(17位-->18位)
| (datacenterId << datacenterIdShift)
// (5bit-->4bit)机器ID偏左移(12位--14位>),空出2位
| (workerId << workerIdShift)
//(2bit标志位)偏左移12位
| (USER << sequenceBits) //user的标志位
| sequence; //最后12位序列号无需移动
整个nextId的组成使用了向左位移 << ** 和位或 | **两种位操作
而改造的方法就是腾出两个位,并放入我们自己的位标志
/**
* 预留了2个位,即可作为 00 | 01 | 10 | 11 四种状态
* USER : 用户类型的标志位
* TEAM : 团队类型的标志位
**/
private static final long USER = 0 ; //----------------> 00
private static final long TEAM = 1 ; //----------------> 01
行文至此,我们做手脚算是做完了
但,做完手脚,我们自己怎么去解析标志位呢???
解析标志位
/**
* 通过自定义标志位解析出ID的类型
* @return 标志位的数值
*/
public long parseTypeFromId(Long id){
if (null == id || id <0)
throw new BusinessException(StatusCode.PARAM_HAS_ERROR,"snowflake id is invalid!");
long status = (id & (0x00003000)) >>sequenceBits;
return status;
}
这里先将id和0x00003000这个十六进制的数进行位与运算,在进行一个右位移运算,前面的逆过程;
示例如下:
00010001111111101110010010010111 01110100 00000100 00010000 00000101 <---id
00000000000000000000000000000000 00000000 00000000 00110000 00000000 <---0x00003000
假设id中第13位是1,第十四位是0,那么经过位与和位右移最后得到的是
00000000000000000000000000000000 00000000 00000000 00000000 00000001 <---结果
和我们定义的TEAM=1相同,那么我们就能判断这个id属于team表的id;
3. 总结
经过改造雪花算法,我们在其中做下手脚即加入自定义的标志位,使用时通过进过改造的nextId方法生成id,解析的时候通过位运算这种极其快速的操作读取出标志位,巧妙得玩出了新花样!!!