最近写了个ID生成器: FireWork。项目地址: firework-id-generator
- 16 byte顺序字符串[8byte时间戳 1byte回拨位 2byte ServiceId 5byte序列号]序列号不在下一秒重置
- 总体趋势递增
- 支持时间到 8888年左右
- 支持3844台相同微服务之间id唯一
- 支持时钟回拨无数次,61次后时间还小于上次回拨时间时通过消费未来时间确保ID不重复
- 支持监听时钟回拨告警或者其他业务处理
- 性能在多线程的时候表现良好(12000/ms 多余snowflake 4000/ms),理论上1s能生成1200wID
- 通过实现存取接口来确保下次启动加载回拨位和回拨最大时间(可选)
业界关于ID生成器有比较多的解决方案
- 数据库自增
- UUID
- Snowflake
- Leaf
- MongDB ObjectId
- Uid-generator
- …
但是无论哪个ID生成器,从设计上会考虑的问题可以归纳成几类:
- 无序/有序/趋势递增
- 有服务端/无服务端
- 是否依赖钟/是否依赖存储
- ID长度规划
- 无序/有序/趋势递增的抉择。
一般ID生成直接会影响数据库使用:
对于B+树的存储引擎,拿Innodb举例,每次插入都是更改Page页的数据,因为写入乱序,InnoDB不得不做频繁的页分列操作,为新的数据腾出空间。其次需要加载一下页到内存里就为了插入数据。
对于LSM Tree的存储引擎(比如Ocean Base),虽然不影响插入时的性能,但是在做层级合并的时候,如果数据是随机的,会加载更多的文件,使写入放大。
综合起来我们会考虑趋势递增,因为有序递增在多线程上容易发生资源争抢。
- 有服务端/无服务端
1.有服务端的话,需要考虑每次网络请求的开销。基于这个基础上,我们一般会设计一个桶,每次拉取的时候拉取一个号段。这样就能减小开销。同时需要考虑服务端高可用,客户端需要缓存,在服务端无法使用的时候能继续消耗。
- 依赖时钟/依赖存储
- 不管是依赖时钟还是依赖存储,都是为了解决下次服务启动的时候,不会生成重复的ID。对于依赖存储的服务,有强依赖的比如依赖数据库生成号段的。只依赖时钟只能解决运行时的时钟回拨(可以用来消费未来时间),但是无法保障服务重启以后,再出现时钟回拨。
-
长度规划
- 类似Snowflake 8 byte的话支持不超过100年
- 但是如果不用8 byte的话,就需要考虑String了
总体考虑来说:
选了趋势递增,无服务端,依赖时钟,弱依赖存储(可选),长度16byte。
权衡带来的好处也有:
- 插入性能比较好。(按照字符串ascii码趋势递增)
- 使用简单,不用搭建服务端。
- 解决时钟回拨的问题
- 通过回拨位每次时钟回拨修改回拨位的值,并且记录上次回拨时间。
- 如果修改的回拨位已经有回拨记录,并且当前时间少于它,就算出一个差值,来消费未来时间。
- 解决snowflake时钟回拨检测加锁。在多线程下性能是snowflake3倍以上。
- 支持时间到 8888年左右
使用也比较简单
简单使用
添加MAVEN 依赖
<dependency>
<groupId>io.gitee.binaryfox</groupId>
<artifactId>firework-id-generator</artifactId>
<version>1.0</version>
</dependency>
使用
FireWorkGenerator.init(0, null);
System.out.println(FireWorkGenerator.nextId());
高阶使用
/**
* FireWorkStepBackHandler
* 不实现的话请自己去实现业务告警,能保障运行时ID不重复(因为它自己会维护一个回拨列表在内存
* 只要内存还存在它再次回拨发现时间小于上次时间 可以自动消费未来时间),
* 但是不能保障服务重启的时候要去计算下看看会不会生成重复id
* 解法有几种(改成未使用的ServiceId 但是无法保障大步幅回拨时id发生碰撞)
*
* 实现的话要实现服务加载的时候
* 加载一个回拨列表和回拨下标。
* 更新回拨下标和回拨列表。
* 如果实现了存取,其实业务告警不告警都可以
*
*
*/
FireWorkGenerator.init(0, new FireWorkStepBackHandler() {
@Override
public void notifyStepBack(long[] before) {
/**
* before 是一个length 63的数组
* index表示的回拨flag
* before[index]表示的是上次回拨回拨前的时间戳
* before[index]=0表示未发生回拨
*/
//比如回拨到63的一半水位线就告警
int count = 0;
for (long l : before) {
if (l != 0) {
count++;
}
if (count > before.length / 2) {
//输出error日志 并且监控报警
}
}
}
@Override
public long[] getStepBackTimeRecordArray(int serviceId) {
/**
* 从存储系统里面加载记载回拨时间的列表
* 这个方法只有在系统启动的时候会被调用
* 实现这个可以无限次回拨
*
*/
//比如 application_name+serviceId当作key 取一个list
return null;
}
@Override
public void setStepBackTimeRecordArray(int serviceId, int index, long timeBeforeStepBack) {
/**
* 更新存储系统里面加载记载回拨时间的列表
* 这个方法在每次时钟回拨时会被调用
* 实现无限次回拨需要实现这个方法
*/
//比如 application_name+serviceId当作key 存一个list
}
@Override
public void setStep(int serviceId, long step) {
/**
* 更新存储系统里面加载记载回拨时间的下标(回拨位)
* 这个方法在每次时钟回拨时会被调用
* 实现无限次回拨需要实现这个方法
*/
//比如 application_name+serviceId当作key 存一个long
}
@Override
public long getStep(int serviceId) {
/**
* 从存储系统里面加载记载回拨时间的回拨位
* 这个方法只有在系统启动的时候会被调用
* 实现无限次回拨需要实现这个方法
*
*/
//比如 application_name+serviceId当作key 存一个long
return 0;
}
});
//使用
String s = FireWorkGenerator.nextId();