分布式id生成器优化
已有实现
- 核心代码
由上述的代码可知,本质上生成一个分布式id主要包括3段:41bit时间戳 + 10bit机器标识(这里的实现是5bit的机器码+5bit数据中心码)+ 12bit的序列号public synchronized long nextId() { long timestamp = timeGen(); // 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常 if (timestamp < lastTimestamp) { throw new RuntimeException( String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); } // 如果是同一时间生成的,则进行毫秒内序列 if (lastTimestamp == timestamp) { sequence = (sequence + 1) & sequenceMask; // 毫秒内序列溢出 if (sequence == 0) { // 阻塞到下一个毫秒,获得新的时间戳 timestamp = tilNextMillis(lastTimestamp); } } // 时间戳改变,毫秒内序列重置 else { sequence = 0L; } // 上次生成ID的时间截 lastTimestamp = timestamp; // 移位并通过或运算拼到一起组成64位的ID return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence; }
- 已有实现的问题
- 没有使用10bit机器标识。目前的机器码和数据中心码都是写死为1,在所有场景下这10bit完全一样,如果在分布式环境下,遇到一些机器时间漂移的情况,虽然趋势shi递增,但不是绝对递增,那么很可能生成重复的id;
- 没有充分利用序列号。上述代码遇到不是在同一时间毫秒内,则直接写死序列号为0.同样地很大概率会导致最后的序列号都是0,如果在分布式环境,机器时间漂移,依然容易产生重复的id;
// 时间戳改变,毫秒内序列重置 else { sequence = 0L; }
- 对系统时钟回退的误差没有容忍度,而是直接抛出异常
测试
由上述可知,只要时间戳一样,那么生成的id就是一样的,经过测试确实也是如此。
/**
* 返回以毫秒为单位的当前时间
*
* @return 当前时间(毫秒)
*/
protected long timeGen() {
return twepoch + 1;
// return System.currentTimeMillis();
}
为了简单,直接修改timeGen函数,让其返回时间一样,则生成的id都一样。
优化
- 生成机器id。通过获取机器MAC地址来生成
/** * 获取机器ID,使用进程ID配合数据中心ID生成<br> * 机器依赖于本进程ID或进程名的Hash值。 * * <p> * 此算法来自于mybatis-plus#Sequence * </p> * * @param datacenterId 数据中心ID * @param maxWorkerId 最大的机器节点ID * @return ID */ public static long getWorkerId(long datacenterId, long maxWorkerId) { final StringBuilder mpid = new StringBuilder(); mpid.append(datacenterId); try { final String processName = ManagementFactory.getRuntimeMXBean().getName(); if (StringUtils.isBlank(processName)) { throw new Exception("Process name is blank!"); } final int atIndex = processName.indexOf('@'); int pid; if (atIndex > 0) { pid = Integer.parseInt(processName.substring(0, atIndex)); } else { pid = processName.hashCode(); } mpid.append(pid); } catch (Exception ex) { //ignore } /* * MAC + PID 的 hashcode 获取16个低位 */ return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1); }
- 生成数据中心id。通过获取进程id,并配合机器id来生成
/** * 获取数据中心ID<br> * 数据中心ID依赖于本地网卡MAC地址。 * <p> * 此算法来自于mybatis-plus#Sequence * </p> * * @param maxDatacenterId 最大的中心ID * @return 数据中心ID */ public static long getDataCenterId(long maxDatacenterId) { if(maxDatacenterId == Long.MAX_VALUE){ maxDatacenterId -= 1; } long id = 1L; SystemInfo systemInfo = new SystemInfo(); String str = getDeviceId(systemInfo); byte[] mac = str.getBytes(); if (null != mac) { id = ((0x000000FF & (long) mac[mac.length - 2]) | (0x0000FF00 & (((long) mac[mac.length - 1]) << 8))) >> 6; id = id % (maxDatacenterId + 1); } return id; }
- 充分利用序列号段,对于不在同一毫秒内的id,随机生成一个序列号
public static long randomLong(final long limitExclude) { return ThreadLocalRandom.current().nextLong(limitExclude); }