SnowFlake 雪花算法 生成分布式唯一ID
背景
公司近期需要重构已有项目,打算重新从架构开始底层搭建,所采用的是市面上最流行的分布式架构搭建,但是业务中有非常之多的数据需要在整个系统中都保持唯一标识,那么问题就来了,分布式项目中id的生成是采用数据库Mysql的唯一自增主键呢?又或者是采用UUID的方式呢? 这两种方法弊端都很明显:
- 【Mysql自增主键生成唯一id】生成的方式是调用了数据库,网络I/O开销和数据库开销非常大。
- 【UUID生成唯一id】性能倒是非常之高,但是所有的数据都得入库呀,但UUID的生成方式又是无序的,就拿Mysql来说,新增性能非常差。
有没有更好的可以生成分布式唯一ID的方式呢? 对的,相信很多人都知道,twitter家的雪花算法生成唯一id(现在百度的UidGenerator..都是基于雪花算法的),虽然听过,但是很多时候很多业务,真的就是不接触就不了解,又或者是很多时候你在用的都快用烂的一些玩意儿,就是没有深入的看过原理。那就好好的了解一下这个到底是怎么样生成的吧。
JAVA 源码
/**
* 雪花算法生成分布式id
*/
public class SnowUtil {
private long sequence = 0L;// 序列号
private long machineId = 1L;// 自定义 节点
private static long lastTimestamp = -1L;// 上次时间
private final static long START_TIME_STAMP = 1698372440174L;// 开始时间戳 2023-10-27
/**
* 每一部分占用的位数
*/
private final static long SEQUENCE_BIT = 11L;
private final static long MACHINE_BIT = 10L;
/**
* 每一部分的最大值
*/
// 1024/节点
private static long MACHINE_MAX = -1L ^ (-1L << MACHINE_BIT);
// 2048/序号
private final static long SEQUENCE_MAX = -1L ^ (-1L << SEQUENCE_BIT);
/**
* 每一部分向左的位移
*/
// 11位
private final static long MACHINE_ID_LEFT = SEQUENCE_BIT;
// 22位
private final static long TIME_STAMP_LEFT = SEQUENCE_BIT + MACHINE_BIT;
private static SnowUtil snowUtil = null;
static {
snowUtil = new SnowUtil();
}
/**
* 生成唯一id
* @return
*/
public static synchronized long nextId() {
return snowUtil.getNextId();
}
private SnowUtil() {
this.machineId = machineId & MACHINE_MAX;
}
public synchronized long getNextId() {
long timestamp = getCurrentTimeStamp();
if (timestamp < lastTimestamp) {
try {
throw new Exception("");
} catch (Exception e) {
e.printStackTrace();
}
} else if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & SEQUENCE_MAX;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
lastTimestamp = timestamp;
long nextId = ((timestamp - START_TIME_STAMP) << TIME_STAMP_LEFT) | (machineId << MACHINE_ID_LEFT) | sequence;
return nextId;
}
/**
* 再次获取时间戳直到获取的时间戳与现有的不同
*
* @param lastTimestamp
* @return 下一个时间戳
*/
private long tilNextMillis(final long lastTimestamp) {
long timestamp = this.getCurrentTimeStamp();
while (timestamp <= lastTimestamp) {
timestamp = this.getCurrentTimeStamp();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
*/
protected long getCurrentTimeStamp() {
return System.currentTimeMillis();
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
for(long i = 0 ; i < 500000L ; i ++){
long nextId = SnowUtil.nextId();
}
long end = System.currentTimeMillis();
Long cost = end-start;
System.out.println("生成50wid总共花费了" + cost.toString() + "毫秒");
}
}
性能测试
可能是因为在本地的缘故,同事用C#同一套思想(同步方法不同,也不是很清楚是不是这个问题),但是C#是运行在Windows平台直接用PowerShell运行速度非常快。
C#的性能测试
java本地环境IDEA运行的测试数据
虽然如此,但是生成id的速度还是让人瞠目结舌。
代码中注意的几个点
1、建议采用单例模式
- 防止重复创建对象,影响性能
private static SnowUtil snowUtil = null;
static {
snowUtil = new SnowUtil();
}
2、同步代码块
- 防止序列号自增产生的并发问题
public synchronized long getNextId() {}
3、需要修改开始的时间戳
因为时间的位数只有1-42 (41位),总共能生成的id只有 (0,2^42-1) (139年)。
- 为了这个生成id的方法可以多用几年,一定要修改开始的时间戳。
private final static long START_TIME_STAMP = 1698372440174L;// 开始时间戳 2023-10-27
原理
Id的数据类型采用Long数据类型,8byte=64bit。
地址(高到低) | 位数 | 用途 | 存储数值范围 |
---|---|---|---|
0-1 | 1 | 符号位 | 0 |
1-42 | 42 | 时间差 | (0,2^42-1) (139年) |
42-52 | 10 | 节点编号 | (0,2^10-1) (1024节点) |
52-64 | 11 | 序列数 | (0,2^11-1)(2048/ms) |
也可以根据自己的实际需求自定义适合自己实际状况的id生成器。
觉得作者写的不错的,值得你们借鉴的话,就请点一个免费的赞吧!这个对我来说真的很重要。૮(˶ᵔ ᵕ ᵔ˶)ა
转载自:https://juejin.cn/post/7294189695285690402