Snowflake简介
Snowflake生成的每一个ID都是64位的整型数,它的核心算法也比较简单高效,结构如下:
41位的时间序列,精确到毫秒级,41位长度可以使用69年。时间位还有一个很重要的作用是可以根据时间进行排序。
10位的机器标识,10位的长度最多支持部署1024个节点。
12位的计数序列号,序列号即一系列的自增id,可以支持同一节点同一毫秒生成多个ID序号,12位的计数序列号支持每个节点每毫秒产生4096个ID序号。
最高位是符号位,始终为0,不可用。
Snowflake原生算法java实现
package com.app.xm.utils;
/**
* 唯一序列ID,原生java算法
* 从网上查找的源码
* @author ouyangjun
*
*/
public class IdSequenceUtils {
private long workerId;
private long datacenterId;
private long sequence = 0L;
private long twepoch = 1514736000000L; //从该时间2018-01-01 00:00:00开始
private long workerIdBits = 5L; //节点ID长度
private long datacenterIdBits = 5L; //数据中心ID长度
private long maxWorkerId = -1L ^ (-1L << workerIdBits); //最大支持机器节点数0~31,一共32个
private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); //最大支持数据中心节点数0~31,一共32个
private long sequenceBits = 12L; //序列号12位
private long workerIdShift = sequenceBits; //机器节点左移12位
private long datacenterIdShift = sequenceBits + workerIdBits; //数据中心节点左移17位
private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; //时间毫秒数左移22位
private long sequenceMask = -1L ^ (-1L << sequenceBits); //4095
private long lastTimestamp = -1L;
public IdSequenceUtils() {
this(0L, 0L);
}
public IdSequenceUtils(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("业务ID不能大于%d或小于0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("数据中心ID不能大于%d或小于0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
/**
* 调用该方法,获取序列ID
* @return
*/
public synchronized long nextId() {
long timestamp = currentTime(); //获取当前时间毫秒数
//如果服务器时间有问题(时钟后退) 报错
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format("时钟向后移动。拒绝在%d毫秒内生成id", lastTimestamp - timestamp));
}
//如果上次生成时间和当前时间相同,在同一毫秒内
if (lastTimestamp == timestamp) {
//sequence自增,因为sequence只有12bit,所以和sequenceMask相与一下,去掉高位
sequence = (sequence + 1) & sequenceMask;
//判断是否溢出,也就是每毫秒内超过4095,当为4096时,与sequenceMask相与,sequence就等于0
if (sequence == 0) {
timestamp = nextMillis(lastTimestamp); //自旋等待到下一毫秒
}
} else {
sequence = 0L; //如果和上次生成时间不同,重置sequence,就是下一毫秒开始,sequence计数重新从0开始累加
}
lastTimestamp = timestamp;
// 最后按照规则拼出ID。
// 000000000000000000000000000000000000000000 00000 00000 000000000000
// time datacenterId workerId sequence
return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift)
| (workerId << workerIdShift) | sequence;
}
/**
* 比较当前时间戳和下一个时间戳,如果下一个时间戳等于或小于当前时间戳,则循环获取下个时间戳
* 该方法主要是避免同一时间获取同一时间戳
* @param lastTimestamp
* @return
*/
protected long nextMillis(long lastTimestamp) {
long timestamp = currentTime();
while (timestamp <= lastTimestamp) {
timestamp = currentTime();
}
return timestamp;
}
/**
* 获取系统当前时间戳
* @return
*/
protected long currentTime() {
return System.currentTimeMillis();
}
}
package com.app.xm.utils;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
/**
* 订购业务唯一订单号实现
* @author ouyangjun
*
*/
public class IdGeneratorUtils {
private long workerId; //用ip地址最后几个字节标示
private long datacenterId = 0L; //可配置在properties中,启动时加载,此处默认先写成0
private long sequence = 0L;
private long workerIdBits = 8L; //节点ID长度
//private long datacenterIdBits = 2L; //数据中心ID长度,可根据时间情况设定位数
private long sequenceBits = 12L; //序列号12位
private long workerIdShift = sequenceBits; //机器节点左移12位
private long datacenterIdShift = sequenceBits + workerIdBits; //数据中心节点左移17位
private long sequenceMask = -1L ^ (-1L << sequenceBits); //4095
private long lastTimestamp = -1L;
public IdGeneratorUtils() {
workerId = 0x000000FF & getLastIP();
}
/**
* 调用该方法,获取序列ID
* @return
*/
public synchronized String nextId() {
long timestamp = currentTime(); //获取当前毫秒数
//如果服务器时间有问题(时钟后退) 报错。
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format("时钟向后移动。拒绝在%d毫秒内生成id", lastTimestamp - timestamp));
}
//如果上次生成时间和当前时间相同,在同一毫秒内
if (lastTimestamp == timestamp) {
//sequence自增,因为sequence只有12bit,所以和sequenceMask相与一下,去掉高位
sequence = (sequence + 1) & sequenceMask;
//判断是否溢出,也就是每毫秒内超过4095,当为4096时,与sequenceMask相与,sequence就等于0
if (sequence == 0) {
timestamp = nextMillis(lastTimestamp); //自旋等待到下一毫秒
}
} else {
sequence = 0L; //如果和上次生成时间不同,重置sequence,就是下一毫秒开始,sequence计数重新从0开始累加
}
lastTimestamp = timestamp;
long suffix = (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence;
// 格式化日期
SimpleDateFormat timePe = new SimpleDateFormat("yyyyMMddHHMMssSSS");
String datePrefix = timePe.format(timestamp);
return datePrefix + suffix;
}
/**
* 比较当前时间戳和下一个时间戳,如果下一个时间戳等于或小于当前时间戳,则循环获取下个时间戳
* 该方法主要是避免同一时间获取同一时间戳
* @param lastTimestamp
* @return
*/
protected long nextMillis(long lastTimestamp) {
long timestamp = currentTime();
while (timestamp <= lastTimestamp) {
timestamp = currentTime();
}
return timestamp;
}
/**
* 获取系统当前时间戳
* @return
*/
protected long currentTime() {
return System.currentTimeMillis();
}
/**
* 获取当前本地IP
* @return
*/
private byte getLastIP(){
byte lastip = 0;
try{
InetAddress ip = InetAddress.getLocalHost();
byte[] ipByte = ip.getAddress();
lastip = ipByte[ipByte.length - 1];
} catch (UnknownHostException e) {
e.printStackTrace();
}
return lastip;
}
}
多线程测试获取唯一序列订单号:
package com.app.xm.test;
import com.app.xm.utils.IdGeneratorUtils;
public class TestSequence extends Thread {
IdGeneratorUtils idGenerator;
public TestSequence(IdGeneratorUtils idGenerator) {
this.idGenerator = idGenerator;
}
int ct = 0;
@Override
public void run() {
System.out.println();
while(ct<100) {
System.out.println(Thread.currentThread().getId() + " : " + idGenerator.nextId());
ct++;
}
}
public static void main(String[] args) {
IdGeneratorUtils utils = new IdGeneratorUtils();
TestSequence test1 = new TestSequence(utils);
TestSequence test2 = new TestSequence(utils);
test1.start();
test2.start();
}
}
其它方式实现序列唯一:
方式一:JAVA原生API提供的UUID,它能确保唯一,但是有个缺点,UUID是一个字符串,字母数字组合,对于分布式系统中数据库主键的生成就不行了,由于它是本地生成,没有远程调用,不存在网络时间,效率比较高,在主键非整数时可以使用
方式二:获取当前毫秒时间戳,效率比较高,属于整型数字,但是对并发量要求高的就不行了,无法保证唯一,1秒最多只能生成1000个,因为是毫秒时间戳,小规模系统可以用用的,简单高效