2023-12-25补充: 有反馈说并发高的项目使用时, 重启项目会导致重复id, 这是必然的, 原雪花算法支持每毫秒内4096个ID, 为了减少位数兼容int类型, 我这里阉割了数据中心和机器id, 并把并发压缩到每分钟128个ID, 虽然并发量大的时候可以提前支取未来的ID, 但这必然会导致重启项目后预支计数器重置, 如果不能接受请根据自己项目并发情况自行调整
起因
Twitter的雪花算法是出名的高效率,无网络和io开销的全局唯一ID生成算法,且相较于UUID、百度的UidGenerator相比,生成的id具有一定的时间顺序,不需要另起字段排序。
在开发过程中碰到对接硬件SDK时,唯一ID仅支持int类型,但无分布式需求且并发量低。所以动手改写了雪花算法的生成规则。
原雪花算法规则:
- 正数位(1bit):一个符号位,永远是0。
- 时间戳(41bit) :自从2012年以来的毫秒数,能撑139年。
- 自增序列(12bit,最大值4096):毫秒之内的自增,过了一毫秒会重新置0。
- DataCenter ID (5 bit, 最大值32):配置值,支持多机房。
- Worker ID ( 5 bit, 最大值32),配置值,一个机房里最多32个机器
- 生成结果为64bit的long类型数字,长度是18-19位。
改写后规则:
- 正数位(1bit):一个符号位,永远是0。
- 时间戳(24bit) :自从2019年以来的分钟数
- 自增序列(7bit,最大值128):分钟之内的自增,过了一分钟会重新置0。
- 生成结果为32bit的int类型数字,长度是11位以内。
话不多说,上源码
import com.google.common.collect.Sets;
import java.util.HashSet;
public class SnowFlowIntUtil {
/** 开始时间戳 (2020-01-01) */
private static final int twepoch = 26341440;// 1580486400401L/1000/60;
/** 序列在id中占的位数 */
private static final long sequenceBits = 6L;
/** 时间截向左移6位 */
private static final long timestampLeftShift = sequenceBits;
/** 生成序列的掩码,这里为63 */
private static final int sequenceMask = -1 ^ (-1 << sequenceBits);
/** 分钟内序列(0~63) */
private static int sequence = 0;
private static int laterSequence = 0;
/** 上次生成ID的时间戳 */
private static int lastTimestamp = -1;
private static final MinuteCounter counter = new MinuteCounter();
/** 预支时间标志位 */
static boolean isAdvance = false;
// ==============================Constructors=====================================
private SnowFlowIntUtil() {
}
// ==============================Methods==========================================
/**
* 获得下一个ID (该方法是线程安全的)
*
* @return SnowflakeId
*/
public static synchronized int nextId() {
int timestamp = timeGen();
// 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format(
"Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
if(timestamp > counter.get()) {
counter.set(timestamp);
isAdvance = false;
}
// 如果是同一时间生成的,则进行分钟内序列
if (lastTimestamp == timestamp || isAdvance) {
if(!isAdvance) {
sequence = (sequence + 1) & sequenceMask;
}
// 分钟内自增列溢出
if (sequence == 0) {
// 预支下一个分钟,获得新的时间戳
isAdvance = true;
int laterTimestamp = counter.get();
if (laterSequence == 0) {
laterTimestamp = counter.incrementAndGet();
}
int nextId = ((laterTimestamp - twepoch) << timestampLeftShift) //
| laterSequence;
laterSequence = (laterSequence + 1) & sequenceMask;
return nextId;
}
}
// 时间戳改变,分钟内序列重置
else {
sequence = 0;
laterSequence = 0;
}
// 上次生成ID的时间截
lastTimestamp = timestamp;
// 移位并通过或运算拼到一起组成32位的ID
return ((timestamp - twepoch) << timestampLeftShift) //
| sequence;
}
/**
* 返回以分钟为单位的当前时间
*
* @return 当前时间(分钟)
*/
protected static int timeGen() {
String timestamp = String.valueOf(System.currentTimeMillis() / 1000 / 60);
return Integer.valueOf(timestamp);
}
// ==============================Test=============================================
public static void main(String[] args) {
HashSet<Object> set = Sets.newHashSet();
for (int i = 0; i < 10000; i++) {
int id = SnowFlowIntUtil.nextId();
set.add(id);
String s = String.valueOf(id);
if (s.length()!=8){
System.out.println(i + ": " + id);
}
}
System.out.println(set.size());
}
}
public class MinuteCounter {
private static final int MASK = 0x7FFFFFFF;
private final AtomicInteger atom;
public MinuteCounter() {
atom = new AtomicInteger(0);
}
public final int incrementAndGet() {
return atom.incrementAndGet() & MASK;
}
public int get() {
return atom.get() & MASK;
}
public void set(int newValue) {
atom.set(newValue & MASK);
}
}
文末贴上Twitter原生的雪花算法及位运算解析
public class SnowFlakeUtil {
// private static final Logger logger = LoggerFactory.getLogger(SnowFlakeUtil.class);
//下面两个每个5位,加起来就是10位的工作机器id
// @Getter
// @Setter
// private static long workerId; //工作id
// @Getter
// @Setter
// private static long datacenterId; //数据id
//12位的序列号
private static long sequence = 158433506100L;
// static {
// if (datacenterId == 0L) {
// logger.error("请先配置机器id");
// throw new RuntimeException("业务id无法生成,缺少配置项目!");
// }
// }
// public SnowFlakeUtil(long workerId, long datacenterId, long sequence) {
// // sanity check for 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));
// }
// System.out.printf("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
// timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);
//
// this.workerId = workerId;
// this.datacenterId = datacenterId;
// this.sequence = sequence;
// }
//初始时间戳
private static long twepoch = 1288834974657L;
//长度为5位
private static long workerIdBits = 5L;
private static long datacenterIdBits = 5L;
//最大值
private static long maxWorkerId = -1L ^ (-1L << workerIdBits);
private static long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
//序列号id长度
private static long sequenceBits = 12L;
//序列号最大值
private static long sequenceMask = -1L ^ (-1L << sequenceBits);
//工作id需要左移的位数,12位
private static long workerIdShift = sequenceBits;
//数据id需要左移位数 12+5=17位
private static long datacenterIdShift = sequenceBits + workerIdBits;
//时间戳需要左移位数 12+5+5=22位
private static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
//上次时间戳,初始值为负数
private static long lastTimestamp = -1L;
//下一个ID生成算法
public static synchronized long nextId(long wId, long datacenterId) {
long timestamp = timeGen();
//获取当前时间戳如果小于上次时间戳,则表示时间戳获取出现异常
if (timestamp < lastTimestamp) {
System.err.printf("clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
lastTimestamp - timestamp));
}
//获取当前时间戳如果等于上次时间戳(同一毫秒内),则在序列号加一;否则序列号赋值为0,从0开始。
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
//将上次时间戳值刷新
lastTimestamp = timestamp;
/**
* 返回结果:
* (timestamp - twepoch) << timestampLeftShift) 表示将时间戳减去初始时间戳,再左移相应位数
* (datacenterId << datacenterIdShift) 表示将数据id左移相应位数
* (workerId << workerIdShift) 表示将工作id左移相应位数
* | 是按位或运算符,例如:x | y,只有当x,y都为0的时候结果才为0,其它情况结果都为1。
* 因为个部分只有相应位上的值有意义,其它位上都是0,所以将各部分的值进行 | 运算就能得到最终拼接好的id
*/
return ((timestamp - twepoch) << timestampLeftShift) |
(datacenterId << datacenterIdShift) |
(wId << workerIdShift) |
sequence;
}
//获取时间戳,并与上次时间戳比较
private static long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
//获取系统时间戳
private static long timeGen() {
return System.currentTimeMillis();
}
public long getTimestamp() {
return System.currentTimeMillis();
}
//---------------测试---------------
// public static void main(String[] args) {
// SnowFlakeUtil worker = new SnowFlakeUtil(1, 1, 1234567890);
// for (int i = 0; i < 30; i++) {
// System.out.println(worker.nextId());
// }
// }
}
其中return处的位运算做的事可以这样理解
// 将时间戳的42bit左移22位,给5bit的数据中心ID、5bit的机器ID和12bit的序列号ID腾出位置
((timestamp - twepoch) << timestampLeftShift) | // 与上数据中心ID+机器ID+序列号ID
// 将数据中心ID的5bit左移17位,给5bit的机器ID和12bit的序列号ID腾出位置
(datacenterId << datacenterIdShift) | // 与上机器ID+序列号ID
// 将机器ID的5bit左移12位,给12bit的序列号ID腾出位置
(wId << workerIdShift) | // 与上序列号ID
sequence;
最后结果就是:
时间戳.append数据中心ID.append机器ID.append序列号