雪花 id 是一个 64bit 的 long 型 id。
snowflake 是 Twitter 开源的分布式 ID 生成算法,结果是 64bit 的 Long 类型的 ID,有着全局唯一和有序递增的特点。
最高位是符号位,因为生成的 ID 总是正数,始终为 0,不可用。
41 位的时间序列,精确到毫秒级,41 位的长度可以使用 69 年。时间位还有一个很重要的作用是可以根据时间进行排序。
10 位的机器标识,10 位的长度最多支持部署 1024 个节点。
12 位的计数序列号,序列号即一系列的自增 ID,可以支持同一节点同一毫秒生成多个 ID 序号,12 位的计数序列号支持每个节点每毫秒产生 4096 个 ID 序号。 今天我们来探究下 2 种常用 jar 包里面的 snowflake 的实现差异。 除了标准实现有很多种实现方式,不同方式的实现有不同的差异,主要体现在时间、数据中心、workId、sequence 的 4 段长度差异上。
生成雪花 id 号段分配 Mybatis-plus mybatis-plus 自带了 IdentifierGenerator 的接口,提供了 ImadcnIdentifierGenerator 和 DefaultIdentifierGenerator 两种实现。
其中 DefaultIdentifierGenerator 是大多数情况的默认实现。
mybatis-plus 的实现与原版实现相比加入了 datacenterId 的概念,而且也引入了基准时间 1288834974657L;
标志位
时间
datacenterId
workId
sequence
1
41
5
5
12
Sharding-jdbc sharding 的实现类叫做:SnowflakeShardingKeyGenerator
sharding 的实现与原版实现相比没有特别差异,而且基准时间也有差异
标志位
时间
workId
sequence
1
41
10
12
生成逻辑差异 Mybatis-plus 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public synchronized long nextId () { long timestamp = timeGen(); if (timestamp < lastTimestamp) { long offset = lastTimestamp - timestamp; if (offset <= 5 ) { try { wait(offset << 1 ); timestamp = timeGen(); if (timestamp < lastTimestamp) { throw new RuntimeException (String.format("Clock moved backwards. Refusing to generate id for %d milliseconds" , offset)); } } catch (Exception e) { throw new RuntimeException (e); } } else { throw new RuntimeException (String.format("Clock moved backwards. Refusing to generate id for %d milliseconds" , offset)); } } if (lastTimestamp == timestamp) { sequence = (sequence + 1 ) & sequenceMask; if (sequence == 0 ) { timestamp = tilNextMillis(lastTimestamp); } } else { sequence = ThreadLocalRandom.current().nextLong(1 , 3 ); } lastTimestamp = timestamp; return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence; }
最有一部分要记住,反解析时需使用。
生成几个 id 看看:参数 = datacenterId=22&workId=11&count=10
1 2 3 4 5 6 7 8 9 10 1722086321019465730 ,1722086321019465731 ,1722086321019465732 ,1722086321019465733 ,1722086321019465734 ,1722086321019465735 ,1722086321019465736 ,1722086321019465737 ,1722086321019465738 ,1722086321019465739
Sharding-jdbc 直接看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Override public synchronized Comparable<?> generateKey() { long currentMilliseconds = timeService.getCurrentMillis(); if (waitTolerateTimeDifferenceIfNeed(currentMilliseconds)) { currentMilliseconds = timeService.getCurrentMillis(); } if (lastMilliseconds == currentMilliseconds) { if (0L == (sequence = (sequence + 1 ) & SEQUENCE_MASK)) { currentMilliseconds = waitUntilNextTime(currentMilliseconds); } } else { vibrateSequenceOffset(); sequence = sequenceOffset; } lastMilliseconds = currentMilliseconds; return ((currentMilliseconds - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) | (getWorkerId() << WORKER_ID_LEFT_SHIFT_BITS) | sequence; }
核心在于最后一行,((currentMilliseconds - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) | (getWorkerId() << WORKER_ID_LEFT_SHIFT_BITS) | sequence; 这个要记住,反解析时要用到。
生成几个 id 看看:workId=555&type=sharding&count=10
1 2 3 4 5 6 7 8 9 10 928955622318321665 ,928955622318321666 ,928955622318321667 ,928955622318321668 ,928955622318321669 ,928955622318321670 ,928955622318321671 ,928955622318321672 ,928955622318321673 ,928955622318321674
反解析 Mybatis-plus 反解析就是把生成的逻辑反过来,按照号段分配逻辑,按位解析,注意:时间因为有基准时间,所以必须反向加减。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 private void buildMybatis (Map<String, Object> res, String sonwFlakeId) { final long twepoch = 1288834974657L ; final long workerIdBits = 5L ; final long datacenterIdBits = 5L ; final long sequenceBits = 12L ; final long workerIdShift = sequenceBits; final long datacenterIdShift = sequenceBits + workerIdBits; final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; Map<String, Object> mybatisMap = new HashMap <>(); res.put("mybatis-plus" , mybatisMap); int len = sonwFlakeId.length(); int sequenceStart = (int ) (len < workerIdShift ? 0 : len - workerIdShift); int workerStart = (int ) (len < datacenterIdShift ? 0 : len - datacenterIdShift); int timeStart = (int ) (len < timestampLeftShift ? 0 : len - timestampLeftShift); String sequence = sonwFlakeId.substring(sequenceStart, len); String workerId = sequenceStart == 0 ? "0" : sonwFlakeId.substring(workerStart, sequenceStart); String dataCenterId = workerStart == 0 ? "0" : sonwFlakeId.substring(timeStart, workerStart); String time = timeStart == 0 ? "0" : sonwFlakeId.substring(0 , timeStart); int sequenceInt = Integer.valueOf(sequence, 2 ); mybatisMap.put("sequence" , sequenceInt); int workerIdInt = Integer.valueOf(workerId, 2 ); mybatisMap.put("workerId" , workerIdInt); int dataCenterIdInt = Integer.valueOf(dataCenterId, 2 ); mybatisMap.put("dataCenter" , dataCenterIdInt); long diffTime = Long.parseLong(time, 2 ); long timeLong = diffTime + twepoch; String date = DateFormatUtils.format(timeLong, "yyyy-MM-dd HH:mm:ss" ); mybatisMap.put("date" , date); }
运行结果:1722086321019465730 的解析结果:
1 2 3 4 "date" : "2023-11-08 10:59:08" ,"sequence" : 2 ,"workerId" : 11 ,"dataCenter" : 22
1722086321019465731 的解析结果:
1 2 3 4 "date" : "2023-11-08 10:59:08" ,"sequence" : 3 ,"workerId" : 11 ,"dataCenter" : 22
Sharding-jdbc 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 private void buildShardingJdbc (Map<String, Object> res, String sonwFlakeId) { final long SEQUENCE_BITS = 12L ; final long WORKER_ID_BITS = 10L ; Calendar calendar = Calendar.getInstance(); calendar.set(2016 , Calendar.NOVEMBER, 1 ); calendar.set(Calendar.HOUR_OF_DAY, 0 ); calendar.set(Calendar.MINUTE, 0 ); calendar.set(Calendar.SECOND, 0 ); calendar.set(Calendar.MILLISECOND, 0 ); Long EPOCH = calendar.getTimeInMillis(); final long WORKER_ID_LEFT_SHIFT_BITS = SEQUENCE_BITS; final long TIMESTAMP_LEFT_SHIFT_BITS = WORKER_ID_LEFT_SHIFT_BITS + WORKER_ID_BITS; Map<String, Object> dataMap = new HashMap <>(); res.put("sharding-jdbc" , dataMap); int len = sonwFlakeId.length(); int sequenceStart = (int ) (len < SEQUENCE_BITS ? 0 : len - SEQUENCE_BITS); int workerStart = (int ) (len < TIMESTAMP_LEFT_SHIFT_BITS ? 0 : len - TIMESTAMP_LEFT_SHIFT_BITS); String sequence = sonwFlakeId.substring(sequenceStart, len); String workerId = sequenceStart == 0 ? "0" : sonwFlakeId.substring(workerStart, sequenceStart); String time = workerStart == 0 ? "0" : sonwFlakeId.substring(0 , workerStart); int sequenceInt = Integer.valueOf(sequence, 2 ); dataMap.put("sequence" , sequenceInt); int workerIdInt = Integer.valueOf(workerId, 2 ); dataMap.put("workerId" , workerIdInt); long diffTime = Long.parseLong(time, 2 ); long timeLong = diffTime + EPOCH; String date = DateFormatUtils.format(timeLong, "yyyy-MM-dd HH:mm:ss" ); dataMap.put("date" , date); }
解析结果:928955622318321665
1 2 3 "date" : "2023-11-08 10:17:59" ,"sequence" : 1 ,"workerId" : 555
928955622318321666
1 2 3 "date" : "2023-11-08 10:17:59" ,"sequence" : 2 ,"workerId" : 555
总结 经过上面的反解析可以发现 1 个隐藏规律:
mybatis-plus 生成的 id 是以 1 开头的,而 sharding-jdbc 生成的是以 9 开头的,原因在于 2 个的时间基础不同。