分布式应用场景中,ID生成处理是必不可少的环节。
防止ID冲突最简单的方式就是UUID,但UUID也它的缺点:
1 相对于来long来说,较长
2 无序,不便于识别
3 由于是字符串,在db中存储、排序、索引会有影响
当然在分布式发展迅猛的阶段,ID的处理方案已经多样化。如:基于redis集群,zookeeper集群等。都各有优劣。
(没有最好的方案,只有相对业务诉求而合适的方案)。
有时我们生成ID,不仅仅只要一个唯一标识,还想获得更多的信息,比如:生成的服务器,生成的时间信息等
所以ID的职责在满足原始需求的情况下又需要容纳更多其他的辅助信息。
twitter的snowflake算法刚刚解决了这类需求。
snowflake算法其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID,理想情况),最后还有一个符号位,永远是0。
从算法中可以看出每毫秒生成的ID数量,已经完全满足一个节点的业务需求。如果一个生成器对应一个表,那更不用说了。
一般的常用服务器,TPS能破千就已非常厉害。所以单节点生成的数量远远超过使用的数量。
最后可用的工具类
package com.arivn.common.utils;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* 分布式ID生成工具类
*/
public class IdMakerUtils {
private static int workerId=0;
private static int dataCenterId=0;
//存放不同表或实体对应的id生成器,(生成器生成id是线程同步安全的,如果多表共用一个生成器可能会降低性能)
private static Map<String,SnowflakeIdMaker> idWorkerMap=new HashMap<String,SnowflakeIdMaker>();
//从配置中读取数据中心和机器号信息进行初始化
//本类是由spring启动创建实例,进行初始化实例,简介设置static dataCenterId和workerId
@Resource
public void setConfigProperties(Properties configProperties) {
if(configProperties.containsKey("workerId")){
workerId=NumUtil.toInt(configProperties.getProperty("workerId"));
}
if(configProperties.containsKey("dataCenterId")){
dataCenterId=NumUtil.toInt(configProperties.getProperty("dataCenterId"));
}
}
/**
* 全局公用的id,如果支付订单,在微信和支付宝支付的时候,需要保证订单号唯一。
*
* @return
*/
public static long getOrderId( ){
return getId("ARIVN_ORDER");
}
/**
* 根据实体名或表明,生成id
*
* @param type
* @return
*/
public static long getId(String type) {
SnowflakeIdMaker idWorker = null;
if (!idWorkerMap.containsKey(type)) {
//安全创建id生成器
synchronized (idWorkerMap) {
if (!idWorkerMap.containsKey(type)) {
idWorker = new SnowflakeIdMaker(dataCenterId, workerId);
idWorkerMap.put(type, idWorker);
} else {
idWorker = idWorkerMap.get(type);
}
}
} else {
idWorker = idWorkerMap.get(type);
}
//生成id
return idWorker.nextId();
}
/**
* 重载
*
* @param classType
* @return
*/
public static long getId(Class classType){
String type=classType.getSimpleName();
return getId(type);
}
}
生成器
基于原始的Twitter_Snowflake,存储结构进行过小调整:
原始存储顺序:0+时间戳(41位)+数据中心(5位)+机器号(5位)+序号(12位)=64位
调整存储顺序:0+时间戳(41位)+序号(12位)+数据中心(5位)+机器号(5位)=64位
调整目的:主要是为了排序有序的逻辑,类似sql中的order by 时间戳,序号,数据中心,机器号的先后顺序。
优先根据时间戳和序号进行排序。
这样调整后,生成的ID用途会跟广,如:
1 数据修改后,可存放id而不存放时间(前提是修改时间不显示)
2 在时间维度查询时,根据id来排序会更快
3 如在APP中下拉刷新时,如果 where A
package com.arivn.common.utils;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
/**
* Twitter_Snowflake<br>
* SnowFlake的结构如下(每部分用-分开):<br>
* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 000000000000- 00000 - 00000
* 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0
* 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
* 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69
* 12位序号,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号
* 10位的数据机器位,可以部署在1024个节点,包括5位dataCenterId和5位workerId
* 加起来刚好64位,为一个Long型。
*/
public class SnowflakeIdMaker {
//开始时间截 (2015-01-01)
private final long anchor = 1420041600000L;
//机器id所占的位数
private final long workerIdBits = 5L;
//支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
//数据中心id所占的位数
private final long dataCenterIdBits = 5L;
//支持的最大数据中心id,结果是31
private final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits);
//时间戳中序号在id中占的位数
private final long sequenceBits = 12L;
/********** 位移时的偏移量计算*********/
//机器ID向左移0位
private final long workerIdShift = 0;
//数据标识id向左移5位(5)
private final long dataCenterIdShift = workerIdBits;
//时间戳中序号向左移17位(5+5)
private final long sequenceShift = dataCenterIdBits + workerIdBits;
//时间截向左移22位(5+5+12)
private final long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits;
//生成序号的掩码,这里为4095 (0b-111111111111=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的时间截(相对于anchor来计算)
private long lastTimestamp = -1L;
/**
* 构造函数
*
* @param dataCenterId 数据中心ID (0~31)
* @param workerId 工作ID (0~31)
*/
public SnowflakeIdMaker(long dataCenterId, long workerId) {
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 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 - anchor) << timestampLeftShift) //加入时间戳
| (sequence << sequenceShift) //加入时间戳中的序号
| (dataCenterId << dataCenterIdShift) //加入数据中心ID
| (workerId << workerIdShift) //加入机器ID
;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
*
* @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();
}
/********** 信息提取方法*********/
//信息提取偏移量,时间(偏移22)+序号(偏移12)+中心(偏移5)+机器号(0)
public static int getWorkId(long id) {
return (int) (id & 31);
}
public static int getDataCenterId(long id) {
return (int) (id >> 5 & 31);
}
public static int getSequence(long id) {
return (int) (id >> 10 & 4095);
}
public static long getTimestamp(long id) {
return id >> 22 & Long.MAX_VALUE;
}
public static String getBinaryString(long id) {
byte[] data = ByteUtils.longToByte64(id);
StringBuffer buffer = new StringBuffer();
for (int j = 0; j < data.length; j++) {
buffer.append(data[j]);
if (j == 0)
buffer.append(" ");
else if (j == 41)
buffer.append(" ");
else if (j == 41 + 12)
buffer.append(" ");
else if (j == 41 + 12 + 5)
buffer.append(" ");
}
return buffer.toString();
}
public IdInfo getIdInfo(long id) {
return new IdInfo(id, anchor);
}
public void spiltId(long id) {
long time = getTimestamp(id);//时间
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(time + this.anchor);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
System.out.println("时间:" + sdf.format(calendar.getTime()) + ",序号:" + getSequence(id) + ",数据中心:" + getDataCenterId(id) + ",机器号:" + getWorkId(id));
}
/**
* 测试
*/
public static void main(String[] args) {
SnowflakeIdMaker idWorker = new SnowflakeIdMaker(2, 9);
System.out.println(idWorker.getIdInfo(375334799105064960L));
System.out.println(idWorker.getIdInfo(375334799033761792L));
long begin = System.currentTimeMillis();
for (int i = 0; i < 0 * 10000; i++) {
long id = idWorker.nextId();
System.out.println(id + " " + getBinaryString(id));
}
long end = System.currentTimeMillis();
System.out.println(begin);
System.out.println(end);
System.out.println(end - begin);
}
class IdInfo {
private long anchor = 0;
private long timestamp = 0;
private long sequence = 0;
private int dataCenterId = 0;
private int wordId = 0;
public IdInfo() {
}
public IdInfo(long id, long anchor) {
this.anchor = anchor;
this.timestamp = SnowflakeIdMaker.getTimestamp(id);
this.sequence = SnowflakeIdMaker.getSequence(id);
this.dataCenterId = SnowflakeIdMaker.getDataCenterId(id);
this.wordId = SnowflakeIdMaker.getWorkId(id);
}
public long getAnchor() {
return anchor;
}
public void setAnchor(long anchor) {
this.anchor = anchor;
}
public Date getTime() {
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(timestamp + this.anchor);
return calendar.getTime();
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
public long getSequence() {
return sequence;
}
public void setSequence(long sequence) {
this.sequence = sequence;
}
public int getDataCenterId() {
return dataCenterId;
}
public void setDataCenterId(int dataCenterId) {
this.dataCenterId = dataCenterId;
}
public int getWordId() {
return wordId;
}
public void setWordId(int wordId) {
this.wordId = wordId;
}
@Override
public String toString() {
return "IdInfo{ time=" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(getTime()) +
", timestamp=" + timestamp +
", sequence=" + sequence +
", dataCenterId=" + dataCenterId +
", wordId=" + wordId +
", anchor=" + anchor +
'}';
}
}
}