雪花算法-Snowflake
Snowflake,雪花算法是由Twitter开源的分布式ID生成算法,以划分命名空间的方式将 64-bit位分割成多个部分,每个部分代表不同的含义。而 Java中64bit的整数是Long类型,所以在 Java 中 SnowFlake 算法生成的 ID 就是 long 来存储的。
- 第1位占用1bit,其值始终是0,可看做是符号位不使用。
- 第2位开始的41位是时间戳,41-bit位可表示2^41个数,每个数代表毫秒,那么雪花算法可用的时间年限是(1L<<41)/(1000L360024*365)=69 年的时间。
- 中间的10-bit位可表示机器数,即2^10 = 1024台机器,但是一般情况下我们不会部署这么台机器。如果我们对IDC(互联网数据中心)有需求,还可以将 10-bit 分 5-bit 给 IDC,分5-bit给工作机器。这样就可以表示32个IDC,每个IDC下可以有32台机器,具体的划分可以根据自身需求定义。
- 最后12-bit位是自增序列,可表示2^12 = 4096个数。
这样的划分之后相当于在一毫秒一个数据中心的一台机器上可产生4096个有序的不重复的ID。但是我们 IDC 和机器数肯定不止一个,所以毫秒内能生成的有序ID数是翻倍的。
Snowflake 的Twitter官方原版是用Scala写的,对Scala语言有研究的同学可以去阅读下,以下是 Java 版本的写法。
package com.xxx.util;
/**
* Twitter_Snowflake
* SnowFlake的结构如下(每部分用-分开):
* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
* 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位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号
* 加起来刚好64位,为一个Long型。
* SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
*
* @author wsh
* @version 1.0
* @since JDK1.8
* @date 2019/7/31
*/
public class SnowflakeDistributeId {
// ==============================Fields===========================================
/**
* 开始时间截 (2015-01-01)
*/
private final long twepoch = 1420041600000L;
/**
* 机器id所占的位数
*/
private final long workerIdBits = 5L;
/**
* 数据标识id所占的位数
*/
private final long datacenterIdBits = 5L;
/**
* 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
*/
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
/**
* 支持的最大数据标识id,结果是31
*/
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
/**
* 序列在id中占的位数
*/
private final long sequenceBits = 12L;
/**
* 机器ID向左移12位
*/
private final long workerIdShift = sequenceBits;
/**
* 数据标识id向左移17位(12+5)
*/
private final long datacenterIdShift = sequenceBits + workerIdBits;
/**
* 时间截向左移22位(5+5+12)
*/
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
/**
* 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
*/
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/**
* 工作机器ID(0~31)
*/
private long workerId;
/**
* 数据中心ID(0~31)
*/
private long datacenterId;
/**
* 毫秒内序列(0~4095)
*/
private long sequence = 0L;
/**
* 上次生成ID的时间截
*/
private long lastTimestamp = -1L;
//==============================Constructors=====================================
/**
* 构造函数
*
* @param workerId 工作ID (0~31)
* @param datacenterId 数据中心ID (0~31)
*/
public SnowflakeDistributeId(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;
}
// ==============================Methods==========================================
/**
* 获得下一个ID (该方法是线程安全的)
*
* @return SnowflakeId
*/
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;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
*
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
*
* @return 当前时间(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
}
}
测试的代码如下
public static void main(String[] args) {
SnowflakeDistributeId idWorker = new SnowflakeDistributeId(0, 0);
for (int i = 0; i < 1000; i++) {
long id = idWorker.nextId();
// System.out.println(Long.toBinaryString(id));
System.out.println(id);
}
}
雪花算法提供了一个很好的设计思想,雪花算法生成的ID是趋势递增,不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的,而且可以根据自身业务特性分配bit位,非常灵活。
但是雪花算法强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。如果恰巧回退前生成过一些ID,而时间回退后,生成的ID就有可能重复。官方对于此并没有给出解决方案,而是简单的抛错处理,这样会造成在时间被追回之前的这段时间服务不可用。
雪花算法(Snowflake) - 改进版
- 时间戳:高位取从2018年1月1日到现在的毫秒数,假设系统至少运行10年,那至少需要10年365天24小时3600秒1000毫秒=320*10^9,差不多预留39bit给毫秒数
- 业务线:8bit
- 机器:自动生成,预留10bit
- 毫秒内序号:每秒的单机高峰并发量小于10W,即平均每毫秒的单机高峰并发量小于100,差不多预留7bit给每毫秒内序列号。
时间戳 | 业务线 | 机器 | 毫秒内序号 |
---|---|---|---|
timestamp | service | worker | sequence |
39 | 8 | 10 | 7 |
代码如下:
SnowflakeIdGenerator.java
package com.wsh.common.util;
import com.wsh.common.exception.IdsException;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.List;
import java.util.Random;
/**
* Snowflake算法改进版
*
* @author wsh
* @version 1.0
* @date 2019/7/31
* @since JDK1.8
*/
public class SnowflakeIdGenerator {
/**
* 业务线标识id所占的位数
**/
private final long serviceIdBits = 8L;
/**
* 业务线标识支持的最大数据标识id(这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
*/
private final long maxServiceId = -1L ^ (-1L << serviceIdBits);
private final long serviceId;
/**
* 机器id所占的位数
**/
private final long workerIdBits = 10L;
/**
* 支持的最大机器id
*/
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
private final long workerId;
/**
* 序列在id中占的位数
**/
private final long sequenceBits = 7L;
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/**
* 开始时间戳(2018年1月1日)
**/
private final long twepoch = 1514736000000L;
/**
* 最后一次的时间戳
**/
private volatile long lastTimestamp = -1L;
/**
* 毫秒内序列
**/
private volatile long sequence = 0L;
/**
* 随机生成器
**/
private static volatile Random random = new Random();
/**
* 机器id左移位数
**/
private final long workerIdShift = sequenceBits;
/**
* 业务线id左移位数
**/
private final long serviceIdShift = workerIdBits + sequenceBits;
/**
* 时间戳左移位数
**/
private final long timestampLeftShift = serviceIdBits + workerIdBits + sequenceBits;
public SnowflakeIdGenerator(long serviceId) {
if ((serviceId > maxServiceId) || (serviceId < 0)) {
throw new IllegalArgumentException(String.format("service Id can't be greater than %d or less than 0", maxServiceId));
}
workerId = getWorkerId();
if ((workerId > maxWorkerId) || (workerId < 0)) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
this.serviceId = serviceId;
}
public synchronized long nextId() throws IdsException {
long timestamp = System.currentTimeMillis();
if (timestamp < lastTimestamp) {
throw new IdsException("Clock moved backwards. Refusing to generate id for " + (
lastTimestamp - timestamp) + " milliseconds.");
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
//跨毫秒时,序列号总是归0,会导致序列号为0的ID比较多,导致生成的ID取模后不均匀,所以采用10以内的随机数
sequence = random.nextInt(10) & sequenceMask;
}
//上次生成ID的时间截(设置最后时间戳)
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift) //时间戳
| (serviceId << serviceIdShift) //业务线
| (workerId << workerIdShift) //机器
| sequence; //序号
}
/**
* 等待下一个毫秒的到来, 保证返回的毫秒数在参数lastTimestamp之后
* 不停获得时间,直到大于最后时间
*/
private long tilNextMillis(final long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
/**
* 根据机器的MAC地址获取工作进程Id,也可以使用机器IP获取工作进程Id,取最后两个段,一共10个bit
* 极端情况下,MAC地址后两个段一样,产品的工作进程Id会一样;再极端情况下,并发不大时,刚好跨毫秒,又刚好随机出来的sequence一样的话,产品的Id会重复
*
* @return
* @throws IdsException
*/
protected long getWorkerId() throws IdsException {
try {
java.util.Enumeration en = NetworkInterface.getNetworkInterfaces();
while (en.hasMoreElements()) {
NetworkInterface iface = en.nextElement();
List addrs = iface.getInterfaceAddresses();
for (InterfaceAddress addr : addrs) {
InetAddress ip = addr.getAddress();
NetworkInterface network = NetworkInterface.getByInetAddress(ip);
if (network == null) {
continue;
}
byte[] mac = network.getHardwareAddress();
if (mac == null) {
continue;
}
long id = ((0x000000FF & (long) mac[mac.length - 1]) | (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 11;
if (id > maxWorkerId) {
return new Random(maxWorkerId).nextInt();
}
return id;
}
}
return new Random(maxWorkerId).nextInt();
} catch (SocketException e) {
throw new IdsException(e);
}
}
/**
* 获取序号
*
* @param id
* @return
*/
public static Long getSequence(Long id) {
String str = Long.toBinaryString(id);
int size = str.length();
String sequenceBinary = str.substring(size - 7, size);
return Long.parseLong(sequenceBinary, 2);
}
/**
* 获取机器
*
* @param id
* @return
*/
public static Long getWorker(Long id) {
String str = Long.toBinaryString(id);
int size = str.length();
String sequenceBinary = str.substring(size - 7 - 10, size - 7);
return Long.parseLong(sequenceBinary, 2);
}
/**
* 获取业务线
*
* @param id
* @return
*/
public static Long getService(Long id) {
String str = Long.toBinaryString(id);
int size = str.length();
String sequenceBinary = str.substring(size - 7 - 10 - 8, size - 7 - 10);
return Long.parseLong(sequenceBinary, 2);
}
}
IdsGen.java
package com.wsh.common.util;
/**
* ID生成器
*
* @author wsh
* @version 1.0
* @date 2019/7/31
* @since JDK1.8
*/
public enum IdsGen {
/**
* 基础公共
*/
BASIC(0),
/**
* 业务服务
*/
BUSSINESS(1),
/**
* 其它
*/
OTHER(255);
private SnowflakeIdGenerator snowflakeIdGenerator;
IdsGen(final int service) {
snowflakeIdGenerator = new SnowflakeIdGenerator(service);
}
public long getIdGen() {
return snowflakeIdGenerator.nextId();
}
public String getIdGenStr() {
return String.valueOf(snowflakeIdGenerator.nextId());
}
}
Test.java
package com.wsh.common.util;
/**
* @author wsh
* @version 1.0
* @date 2019/7/31
* @since JDK1.8
*/
public class Test {
public static void main(String[] args) {
long t = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
System.out.println(IdsGen.BASIC.getIdGenStr());
}
long t1 = System.currentTimeMillis();
System.out.println("耗时-->" + (t1 - t));
}
}
缺点:
- 极端情况下,获取的workerId可能会重复,请看getWorkerId的注释,后续可以改造为读取配置文件,如果配置文件读取不到再自动生成
- 无法避免时间回拨,比如润秒
- 无法保证每个ID都不浪费