首页 > 编程知识 正文

分布式统一id生成,分布式自增id方案

时间:2023-05-03 19:30:05 阅读:146108 作者:3103

我的个人网站: http://riun.xyz

本文是廖雪峰老师对分布式唯一ID生成器的解读。 我第一次读这篇文章的时候看不懂,然后和很多其他文章结合,渐渐明白了所谓的“用bit保存什么信息”、“保存在什么位置”以及这些词的意思。 所以我想尽量用简单易懂的语言说明这些东西。 和在耳边指导一样。 我建议在阅读正文之前通读一下廖雪峰老师的文章。

零,预备知识1字节=8位

1字节=8bit

在java中,int类型为4字节,即32位

00000000 00000000 00000000 00000000

正数二进制文件的最高有效位必须为0,因此要表示正数,32个二进制文件只能使用以下31个:

0000000 00000000 00000000 00000000

因此,能够表现的最大的正数的低位31位都是1 :

1111111 11111111 11111111 11111111

结果是230 229 … 22 21 2^0=2^31-1。 因此,在不考虑编码比特的情况下,能够由31比特表现的最大正整数是2^31-1。

在java中,长类型为8字节,即64位

一年约365*24*3600=31536000 3154万秒(常年。 闰年还要一天)

一、大致想法1、我们一定要使用长类型保存唯一的id。 int类型只能使用31位,所以太少了。

使用长整型时,如果使用后53位,则所有高位都为0,因此最后显示的数字必须为正数。

接下来,我会考虑这53个人如何使用。

分布式唯一Id的配置必须具有时间戳,当前时间戳为当前时间距离1970.1.1的毫秒数。 为了节省空间,使用秒级别的时间戳。 也就是说,取当前时间到1970.1.1之间的秒数。 如果我们想让我们的id生成器在100年里一直可用,就必须至少能显示100*31536000=3153600000秒,也就是3153600000(31亿)这样大的数字。

如果不包含符号位,则二进制1111 1111表示28-1=255(202122…2^7=28-1 )。 也就是说,八个二进制数可以表示的最大数量为28-1。

如果是:

2 ^ 31=2147483648,31 bit表示的最大数量为2^31-1=2147483647

2 ^ 32=4294967296,32 bit表示的最大数量为2^32-1=4294967295

因此,至少需要32个二进制位才能存储3153600000。

32位显示的秒数具体是多少? 4294967295/31536000=136.19251950152。 可以表示约136年。

1970 136=2106。 因此,如果使用32位存储时间戳(秒),这一数字现在至少在2106年之前是足够的。

请在这里也考虑一下时间戳是放在上面好还是放在下面好。

如果想整理生成的id,使其随着时间的推移而增加,可以将其置于上位。 因为如果放在上位的话,随着时间的经过,后面的时间的数值会变大,上位的数值越大最终得到的十进制数字也会变大,所以应该放在上位。

2、如果有秒级别的时间戳,不同秒生成的id一定不同,所以必须只考虑同一秒生成的id。

同一秒生成的不同id可以使用自增加序列来区分。 是把1、2、3、4、5…这个数字增加了的数字。 这叫做序列号。

2^15=32768

2^16=65536

16个二进制位用于区分同一秒内的不同请求,至少可以表示65536个。 也就是说,如果以16位存储序列号,则每秒至少可以生成6.5万个不同的id。

这对很多业务来说都足够了,也可以根据情况进行调整。

3、现在1秒内可以生成6.5万个不同的id了。 还有别的吗? 服务器。 为了提高服务的处理能力,我们的服务部署在多台机器上。 在同一秒内,多台机器生成的id可能相同。 秒数相同,正好可以得到相同的序列号。 如何确保在多台机器上生成的id不同?

这需要进一步引入机械标识。 使用5bit存储机器标识时,即使不同机器的标识位不同,他们也不会得到相同的序列号。 在同一台机器上,即使序列号在同一秒内从1万增加到6.5万,也不会相同。

五个二进制位最多可以表示2^5=32台不同的设备。 也就是说,32台机器可以同时生成id。 这个数值也可以根据业务状况进行调整。

二、如何实现1。 java的System.currentTimeMillis ) )获取当前时间戳。 但是,得到的是毫秒单位。 要获取以秒为单位的时间戳,可以是/1000。

longcurrentepochsecond=system.current time millis ()/1000

2、获取秒级时间戳后,使用静态变量作为自增位。 使用变量记录上次生成id的秒数,如果不等于当前秒数,则将自增量重置为0。 如果相等,则表示是

同一秒内的请求,则自增位加1。

static long offset = 0;if (lastEpoch != currentEpochSecond) { lastEpoch = currentEpochSecond; //重置自增序列号 reset();}//自增offset++;private static void reset() { offset = 0;}

3、获取当前机器码,根据规则映射成[0,31]之间的数字。

private static final long SHARD_ID = getServerIdAsLong();

4、将以上三部分按照二进制拼接起来:

假设第一部分是秒级时间戳currentEpochSecond,第二部分是自增序列号next,第三部分是机器码shardId。

根据前面的分析,第二三部分共占16+5=21位,而时间戳要放在最高位,所以将时间戳向左移动21位;自增序列号次之,所以将自增序列号向左移动5位;机器码原位不动。

return (currentEpochSecond << 21) | (next << 5) | shardId;

位移运算是位运算,位运算之后为保留三部分信息,所以将其进行或运算|。或运算|规则是:当前位置上有一个为1则结果为1,均不是1则为0。使用或运算能够完整保留三部分中每个位置上的数字信息。

三、源码

为了加快生成时间,我把无用的log日志打印全部去掉了。为了尽可能独立简洁,我把不必要的方法去掉了,保证只要这一个类就能够使用。

廖雪峰老师的源码不止有上述写的那些东西。还有当1秒内生成的id超过6.5万时向后借下一秒的id来用的处理;还有最后并没有使用当前秒级时间戳,而是当前秒级时间戳减去2000.1.1到1970.1.1的秒数,以此来减小不必要的二进制位的使用,进一步减少内存占用(系统使用生成id的服务时,需要在2000.1.1之后上线);还有时针回拨问题,原文的首评说的很正确:保障了程序运行时间回拨,并没有解决停机时间回拨问题。停机时服务已经停止,内存中记录的lastEpoch 也就丢失,所以停机时针回拨后仍有可能出现重复id(回拨时间>重启服务时间时)。由于不是主线,我就没有在上文中介绍,可以看下面源码结合注释思考一下。

【全部】廖雪峰老师的源码:IdUtil.java

【删减】我更改和加注释之后的源码:

package com.example.demo.utils;//import org.slf4j.Logger;//import org.slf4j.LoggerFactory;import java.net.InetAddress;import java.net.UnknownHostException;import java.time.LocalDate;import java.time.ZoneId;import java.util.regex.Matcher;import java.util.regex.Pattern;/** * 53 bits unique id: * * |--------|--------|--------|--------|--------|--------|--------|--------| * |00000000|00011111|11111111|11111111|11111111|11111111|11111111|11111111| * |--------|---xxxxx|xxxxxxxx|xxxxxxxx|xxxxxxxx|xxx-----|--------|--------| * |--------|--------|--------|--------|--------|---xxxxx|xxxxxxxx|xxx-----| * |--------|--------|--------|--------|--------|--------|--------|---xxxxx| * * Maximum ID = 11111_11111111_11111111_11111111_11111111_11111111_11111111 * * Maximum TS = 11111_11111111_11111111_11111111_111 * * Maximum NT = ----- -------- -------- -------- ---11111_11111111_111 = 65535 * * Maximum SH = ----- -------- -------- -------- -------- -------- ---11111 = 31 * * It can generate 64k unique id per IP and up to 2106-02-07T06:28:15Z. */public final class IdUtil {//private static final Logger logger = LoggerFactory.getLogger(IdUtil.class);private static final Pattern PATTERN_HOSTNAME = Pattern.compile("^.*\D+([0-9]+)$");/** * 1970.1.1 到 2000.1.1 的秒数 */private static final long OFFSET = LocalDate.of(2000, 1, 1).atStartOfDay(ZoneId.of("Z")).toEpochSecond();/** * 2的16次方-1 65535 */private static final long MAX_NEXT = 0b11111_11111111_111L;/** * 当前机器码,只允许返回0~31,共能标识32个数,所以占用5bit(2^5=32) */private static final long SHARD_ID = getServerIdAsLong();/** * 自增位 */private static long offset = 0;/** * 上次生成时的秒级时间戳 */private static long lastEpoch = 0;public static long nextId() {return nextId(System.currentTimeMillis() / 1000);}private static synchronized long nextId(long epochSecond) {if (epochSecond < lastEpoch) {// warning: clock is turn back://logger.warn("clock is back: " + epochSecond + " from previous:" + lastEpoch);epochSecond = lastEpoch;}//不是同一秒的请求if (lastEpoch != epochSecond) {//记录lastEpochlastEpoch = epochSecond;//重置自增位offsetreset();}//自增位自增offset++;//确保自增序列号在[1,65535]之间long next = offset & MAX_NEXT;if (next == 0) {//当在同一秒内,offset自增到65536,则 65536 & 65535 = 0,就不再在当前秒内自增,使用下一秒的自增序列号//(使用下一秒调用nextId(),那么秒级时间戳就是下一秒,自增序列就从下一秒的1开始) 我理解这是向后借下一个秒级时间戳的6.5万个序列号//logger.warn("maximum id reached in 1 second in epoch: " + epochSecond);return nextId(epochSecond + 1);}//每部分各自位移到自己的位置,然后做或运算return generateId(epochSecond, next, SHARD_ID);}private static void reset() {offset = 0;}private static long generateId(long epochSecond, long next, long shardId) {return ((epochSecond - OFFSET) << 21) | (next << 5) | shardId;}private static long getServerIdAsLong() {try {String hostname = InetAddress.getLocalHost().getHostName();Matcher matcher = PATTERN_HOSTNAME.matcher(hostname);if (matcher.matches()) {long n = Long.parseLong(matcher.group(1));if (n >= 0 && n < 32) {//logger.info("detect server id from host name {}: {}.", hostname, n);return n;}}} catch (UnknownHostException e) {//logger.warn("unable to get host name. set server id = 0.");}return 0;}}

测试:

public static void main(String[] args) { //生成一百万个唯一id大概需要25ms System.out.println(IdUtil.nextId()); //1452736581206048 //Set<Long> set = new HashSet<>(1500000); long startTime = System.currentTimeMillis(); for (int i = 0; i < 1000000; i++) { long l = IdUtil.nextId(); //set.add(l); } long endTime = System.currentTimeMillis(); System.out.println(endTime - startTime); //System.out.println(set.size());} 四、总结

总述:唯一id,纯数字,long型,可安全传给前端展示

组成:32bit秒级时间戳+16bit自增数字+5bit机器码

速度: 生成1000000(一百万)个平均需要25毫秒。

优点:

1、虽然加了锁,但是生成效率极高,速度极快(因为大多数情况下都只做了几步简单的数字运算,没有循环)。

2、生成的id占用空间少,利于储存和传输(且53位的long刚好够前端完全精度展示)。

3、由于是趋势递增的数字,更便于做索引。

缺点:

1、由于没有全局时钟,若每台服务器的时间不是完全一致,则生成的 id 不是绝对递增的,而是“趋势递增”(同一时刻,有的服务器时间早,有的时间晚。那么这一时刻不同服务器生成的时间戳就不一样,那么他们生成的id就会有的大有的小,但是由于时间是递增的,所以呈现趋势递增的特征。有小下降的上升折线图)

2、每次跨秒时,自增序列号offset都会归零,这样序列号为0的id比较多。如果我们使用这些id做分库分表时的取模依据,就会导致取模后分布不均匀。(这个没太理解,为什么序列号为0的多?“跨秒”的时刻,只是一刻,不是应该和其他“时刻”一样吗?对请求来说遇到这些时刻的机会应该是一样多的吧)

五、参考

分布式唯一ID生成器

细聊分布式ID生成方法

篇外:

1111 1111

(不含符号位时)8个二进制位能表达的最大数值是28-1=255,但能表达28=256个不同数字。

相差的1,从数学角度来说,是因为有0的存在;又因为0也是一个数,所以才能表达比其最大数值本身多1的个数。

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。