最近公司在搞什么领域负责人,其实就是每个人负责某几个模块,也就是owner的意思,公司的snowflake服务是我负责,借此机会研究下雪花算法
1.1 雪花算法结构
snowflake生成的结果是一个64bit大小的整数,使用long存储,它的结构如下图:
其中
[1]、1位,不用,二进制中的最高位是符号位,1表示负数,0表示正数,由于我们生成的雪花算法都是正整数,所以这里是0[2]、41位,这里的时间戳是表示的是从起始时间算起,到生成id时间所经历的时间戳,也就是(当前时间戳-起始时间戳(固定)) 这里一共是41位,范围就是(0~ 2^41-1),这么大的毫秒数转化成时间就是大约69年 [3]、10位,这里的10位代表工作机器id,一共可以部署在(2^10=1024)台机器上面,10位又可以分为前面五位是数据中心id(0~31),后面五位是机器id(0-31) [4]、共12位,序列位,一共可用(0 ~ 2^12-1)共4096个数字复制代码
1.2 雪花算法作用
雪花算法可以保证:
- 生成的所有的id都是随着时间递增
- 分布式系统内不会产生重复的id(因为有机器位做区分)
1.3 雪花算法代码实现
package snowflake;import java.util.HashSet;import java.util.Set;/** * @author Smith 2020/12/26 */public class SnowFlakeService {//起始时间戳( 2020-12-26 00:00:00 )private static final long START_STAMP = 1608912000000L;//序列号占用位数private static final long SEQUENCE_BIT = 12;//机器标识占用位数private static final long MACHINE_BIT = 5;//数据中心占用位数private static final long DATACENTER_BIT = 12;//序列号最大值private static final long MAX_SEQUENCE = -1L ^ (-1L << 12); // 4095/** * 偏移量 **/private static final long MACHINE_LEFT = SEQUENCE_BIT;private static final long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;private static final long TIMESTAMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;private static long dataCenterId; //0, 数据中心(0-31)private static long machineId; //0, 机器标识(0-31)private static long sequence; //序列号 range(0 ~ 4095)private static long lastStamp; //上一次时间戳public static synchronized long getNextId() {long currentStamp = System.currentTimeMillis();if (currentStamp < lastStamp) {throw new IllegalArgumentException("时间被回退,不能继续产生id"); }if (currentStamp == lastStamp) {//相同毫秒内,序列号自增sequence = (sequence + 1) & MAX_SEQUENCE;if (sequence == 0L) {//序列号已经到最大值System.out.println("序列号已经到达最大值");//使用下一个时间戳currentStamp = getNextStamp(); } } else {//不同毫秒,序列号重置sequence = 0L; } lastStamp = currentStamp;//当前时间戳存档,用于判断下次产生id时间戳是否相同return (currentStamp - START_STAMP) << TIMESTAMP_LEFT | dataCenterId << DATACENTER_LEFT | machineId << MACHINE_LEFT | sequence; }/** * 阻塞直至获得下一个时间戳 * * @return */public static long getNextStamp() {long newStamp = getCurrentStamp();while (newStamp <= lastStamp) { newStamp = getCurrentStamp(); }return newStamp; }/** * 获取当前时间戳 * * @return */public static long getCurrentStamp() {return System.currentTimeMillis(); }public static void main(String[] args) { Set<Long> set = new HashSet<Long>();long start = System.currentTimeMillis();for (int i = 0; i < 100000; i++) { Long id = SnowFlakeService.getNextId(); set.add(id); } System.out.println("总耗时 : " + (System.currentTimeMillis() - start)); System.out.println(set.size()); } }复制代码
运行结果:
序列号已经到达最大值 序列号已经到达最大值 序列号已经到达最大值 序列号已经到达最大值 序列号已经到达最大值 序列号已经到达最大值 序列号已经到达最大值 序列号已经到达最大值 序列号已经到达最大值 序列号已经到达最大值 序列号已经到达最大值 序列号已经到达最大值 序列号已经到达最大值 序列号已经到达最大值 总耗时 : 34100000复制代码
因为序列号是每秒最多可以生成4096个id,所以在序列号到达最大值的时候,程序会阻塞直到下一个毫秒时间戳,然后继续生成id,从运行结果来看,在34ms内生成了100000个不同的id,还是比较可观的。
1.4 雪花算法的弊端
- snowflake不依赖数据库也不依赖内存,随时可以生成id,这也是为什么它如此受欢迎,但是因为它在设计时通过时间戳来避免对内存和数据库的依赖,所以它依赖于服务器的时间。 试想,如果服务器的时间发生了错乱或者回拨,这就直接影响了生成的id,有很大可能生成重复的id,且一定会打破递增属性,这也是他的一个致命缺点(不支持服务器时间回拨)
- 每毫秒生成id的上限受到限制,由于时间戳位是41位的毫秒级时间戳,所以从当前起始到41bit 耗尽,只能坚持70年
- 程序获取操作系统时间会耗费较多时间
那么如何解决这些问题呢(后面在研究): 百度开源了UIDGenerator 算法 美团团队根据业务场景提出了基于号段思想的 Leaf-Segment 方案和基于 Snowflake 的 Leaf-Snowflake 方案