外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=%E5%88%86%E5%B8%83%E5%BC%8FID-%E9%9B%AA%E8%8A%B1%E7%AE%97%E6%B3%95%E7%9A%84%E9%97%AE%E9%A2%98%E4%B8%8E%E6%96%B9%E6%A1%88%EF%BC%88CosId%EF%BC%89_image.&pos_id=img-SZPkqRew-1724123351152)
Snowflake算法的原理相对直观,它有不同的部分组成,每个部分单独来看可能会导致重复,但是组合在一起做到全局唯一。
它负责生成一个64位(long型)的全局唯一ID,这个ID的构成包括:1位无用的符号位, 41位的时间戳, 10位的机器ID. 以及12位的序列号,除了固定的1位符号位之外,其余的三个部分都可以根据实际需求进行调整:
EPOCH
+69年,一般我们需要自定义EPOCH
为产品开发时间,另外还可以通过压缩其他区域的分配位数,来增加时间戳位数来延长可用时间。服务器时钟回拨是由于在某些情况下,服务器的系统时钟会发生不可避免或人为的变化,在高并发场景下, 获得的高精度时间戳,有时候会往前跳,有时候又会往回拨。一旦时钟往回拨,就有可能产生重复的ID,这 就是时钟回拨问题。
解决的方法有很多,雪花算法对此并没有标准解决方案,不同框架有自己的解决方法,但是基本思路都是用上一次生成主键的时间戳,然后拿当前时间和上一次的时间进行比较,只是发现有问题后的解决方式会有不同:
@SneakyThrows(InterruptedException.class)
private boolean waitTolerateTimeDifferenceIfNeed(final long currentMillis) {
if (lastMillis.get() <= currentMillis) {
return false;
}
long timeDifferenceMillis = lastMillis.get() - currentMillis;
ShardingSpherePreconditions.checkState(timeDifferenceMillis < maxTolerateTimeDifferenceMillis,
() -> new AlgorithmExecuteException(this, "Clock is moving backwards, last time is %d milliseconds, current time is %d milliseconds.", lastMillis.get(), currentMillis));
Thread.sleep(timeDifferenceMillis);
return true;
}
AbstractSnowflakeId
long currentTimestamp = getCurrentTime();
if (currentTimestamp < lastTimestamp) {
throw new ClockBackwardsException(lastTimestamp, currentTimestamp);
}
在SnowflakeId中根据业务设计的位分配方案确定了基本上就不再有变更了,也很少需要维护。但是工作进程ID
总是需要配置的,而且集群中是不能重复的,还要考虑服务重启后分配ID保持稳定性,否则分区原则就会被破坏而导致ID唯一性原则破坏,当集群规模较大时工作进程ID
的维护工作是非常繁琐,低效的。
COSID提供的方案如下:
MachineIdDistributor
是 SnowflakeId
的机器号分配器,它负责分配机器号,同时还会存储MachineId
的上一次时间戳,用于启动时时钟回拨的检查。
目前 CosId 提供了以下六种 MachineId
分配器。
ManualMachineIdDistributor
: 手动配置machineId
,一般只有在集群规模非常小的时候才有可能使用,不推荐。StatefulSetMachineIdDistributor
: 使用Kubernetes
的StatefulSet
提供的稳定的标识ID(HOSTNAME=service-01)作为机器号。RedisMachineIdDistributor
: 使用Redis作为机器号的分发存储,同时还会存储MachineId
的上一次时间戳,用于启动时时钟回拨的检查。JdbcMachineIdDistributor
: 使用关系型数据库作为机器号的分发存储,同时还会存储MachineId
的上一次时间戳,用于启动时时钟回拨的检查。ZookeeperMachineIdDistributor
: 使用ZooKeeper作为机器号的分发存储,同时还会存储MachineId
的上一次时间戳,用于启动时时钟回拨的检查。MongoMachineIdDistributor
: 使用MongoDB作为机器号的分发存储,同时还会存储MachineId
的上一次时间戳,用于启动时时钟回拨的检查。对于实例应用分成两类,一类是stable应用,就是稳定的应用,一类是不稳定的应用。以JdbcMachineIdDistributor
分发器为例:
.cosid-machine-state
目录会保存当前应用的机器号和时间戳,下次启动时还是会找到同一条记录。在雪花算法中,排在最后的12位自增序列号部分,默认的生成逻辑是当时间戳部分相等时,自增序列号部分才会+1,否则,将从0重新开始。我们想想这样的话会有什么问题,因为时间戳相同的情况很少,所以我们生成出来的id末尾大部分会导致取模的时候分布并不均匀,比如分库分表时,数据大部分就会落到一个地方,不适用于需要做取模运算的场景。
我们先复现一下问题,使用hutool的雪花算法工具类生成唯一id,然后做一个简单的取模运算:
@Test
public void hutoolSnowflakeMod() throws InterruptedException {
for (int i = 0; i < 100; i++) {
long id = IdUtil.getSnowflake(1).nextId();
Thread.sleep(1);
log.info("id: {}, after mod 4: {}", id, id % 4);
}
}
截取的结果可以看到,基本上就是0,几乎没有其它数字,取模的结果很不均匀。
[2024-08-19 15:46:45.486] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.hutoolSnowflakeMod(41)] - id: 1825439244152344576, after mod 4: 0
[2024-08-19 15:46:45.487] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.hutoolSnowflakeMod(41)] - id: 1825439244160733184, after mod 4: 0
[2024-08-19 15:46:45.490] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.hutoolSnowflakeMod(41)] - id: 1825439244164927488, after mod 4: 0
[2024-08-19 15:46:45.492] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.hutoolSnowflakeMod(41)] - id: 1825439244177510400, after mod 4: 0
[2024-08-19 15:46:45.493] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.hutoolSnowflakeMod(41)] - id: 1825439244185899008, after mod 4: 0
[2024-08-19 15:46:45.496] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.hutoolSnowflakeMod(41)] - id: 1825439244190093312, after mod 4: 0
[2024-08-19 15:46:45.498] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.hutoolSnowflakeMod(41)] - id: 1825439244202676224, after mod 4: 0
[2024-08-19 15:46:45.501] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.hutoolSnowflakeMod(41)] - id: 1825439244211064832, after mod 4: 0
[2024-08-19 15:46:45.503] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.hutoolSnowflakeMod(41)] - id: 1825439244223647744, after mod 4: 0
[2024-08-19 15:46:45.505] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.hutoolSnowflakeMod(41)] - id: 1825439244232036352, after mod 4: 0
[2024-08-19 15:46:45.507] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.hutoolSnowflakeMod(41)] - id: 1825439244240424960, after mod 4: 0
在CosId框架中,解决方案也很简单 – 轻易不要重置这个自增序列位即可,通过引入 sequenceResetThreshold
属性,巧妙地解决了取模分片不均匀的问题,这一设计在无需牺牲性能的同时,为用户提供了更加出色的使用体验。
sequenceResetThreshold
在不同的情况下可能会取不同的值,但是作用都是一样的,通过限制自增序列不要轻易重置来达到目的。
AbstractSnowflakeId
//region Reset sequence based on sequence reset threshold,Optimize the problem of uneven sharding.
if (currentTimestamp > lastTimestamp
&& sequence >= sequenceResetThreshold) {
sequence = 0L;
}
我们跑一遍CosId的取模情况:
@Test
public void cosIdSnowflakeMod() throws InterruptedException {
for (int i = 0; i < 100; i++) {
long id = snowflakeId.generate();
Thread.sleep(1);
log.info("id: {}, after mod 4: {}", id, id % 4);
}
}
可以看出已经不存在取模分配不均匀的问题
[2024-08-19 15:50:35.949] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.cosIdSnowflakeMod(50)] - id: 615936209755045889, after mod 4: 1
[2024-08-19 15:50:35.951] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.cosIdSnowflakeMod(50)] - id: 615936209763434498, after mod 4: 2
[2024-08-19 15:50:35.953] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.cosIdSnowflakeMod(50)] - id: 615936209771823107, after mod 4: 3
[2024-08-19 15:50:35.955] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.cosIdSnowflakeMod(50)] - id: 615936209780211716, after mod 4: 0
[2024-08-19 15:50:35.957] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.cosIdSnowflakeMod(50)] - id: 615936209788600325, after mod 4: 1
[2024-08-19 15:50:35.959] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.cosIdSnowflakeMod(50)] - id: 615936209796988934, after mod 4: 2
[2024-08-19 15:50:35.961] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.cosIdSnowflakeMod(50)] - id: 615936209805377543, after mod 4: 3
[2024-08-19 15:50:35.963] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.cosIdSnowflakeMod(50)] - id: 615936209813766152, after mod 4: 0
JavaScript
的Number.MAX_SAFE_INTEGER
只有53-bit,如果直接将63位的SnowflakeId
返回给前端,那么会产生值溢出的情况(所以这里我们应该知道后端传给前端的long
值溢出问题,迟早会出现,只不过SnowflakeId出现得更快而已)。 很显然溢出是不能被接受的,一般可以使用以下俩种处理方案:
SnowflakeId
转换为String
类型。
long
转换成String
。SnowflakeFriendlyId
将SnowflakeId
转换成比较友好的字符串表示:{timestamp}-{machineId}-{sequence} -> 20210623131730192-1-0
SnowflakeId
位分配来缩短SnowflakeId
的位数(53-bit)使 ID
提供给前端时不溢出
SafeJavaScriptSnowflakeId
(JavaScript
安全的 SnowflakeId
)