关于雪花算法的设计与思考

2023-05-16


2017年的时候项目组在开发一款大区游戏,由于之前demo阶段的玩家id都是单服生成的,只能保证单进程中的唯一,而无法保证在分布式服务器端的唯一性。随着项目的开发进展,需要设计能保证在分布式的场景下,玩家id全局唯一的方案。由于当时游戏里面服务器都有一个唯一的serverId,所以自然而然想到用“serverId + 自增id”作为全局唯一的玩家id的设计方案。后来才知道,类似的这种分布式全局唯一id的方法叫“雪花算法”。这次借着重新造轮子的机会,重新思考了一下雪花算法的设计过程和实现方式。

一个long型id的二进制总共有64位,除去符号位剩余63位,全局唯一id就是将这63位的二进制划分为几个部分,其中一部分代表这个id在全局中的分段,一部分表示局部的差异。

通常雪花算法生成的id由三个部分组成:机器编码,时间戳,序列号。其中机器编码表示当前进程的唯一id;时间戳区分不同时间点生成的id;序列号用来区分相同时间戳下生成的不同的id。

机器编码就是代表这个id在全局中的分段,相当于是给当前的进程分配了一个id分段。当前进程只要在这个分段中分配id,那么就不可能和其他进程分配的id发生重复。这也有点类似于ip的分段,假如某个国家分配到了192.0.0.0这个分段的ip,那么只要在192这个分段内任意再分配ip,比如192.0.0.2,它们都不会和其他国家的ip地址冲突。因此,当整个系统中,每个服务器的机器编码确定且唯一时,各个系统可以独自以自增的形式生成id,而不必使用全局锁控制全局id的唯一性。

时间戳的作用是为了区分不同时间点生成的id。如果单单使用自增id,当第一次从0开始计数,生成N次id以后。如果服务器重启,那么自增id又将从头开始从0计数,那么id就会出现重复的情况。这时,要么依赖存储将自增id记录下来,要么在全局id中加入时间戳的部分,让id持续往自增,不再出现重启之后id回退的情况。

假如在同一个时间点上,进程需要生成多个id,那么序列号就标出这个id在同一时间点上,是第几个id了。


雪花算法的二进制位数分配
  

雪花算法的id中的每个部分的二进制长度都是根据自己的业务需求来进行分配的。目前我的代码中分配的二进制如下。

groupId(5) + serverId(15) + sequence(11) + timestamp(32)

其中groupId和serverId表示服务器的机器编码。由于是在我们游戏中生成全局唯一的id,游戏可能存在分区可能,而且预计每个区可能有几千个服,因此对机器编码留了一定的余量,总共分配了20位。

sequence大体上都是分配10~14位左右,这里取11位。剩下的32位空间就是时间戳,而32位一般可以表示时间戳的秒数。那么这里两个字段也就预示了在1秒的时间范围内最多只能生成2048个id。

贴上实现的代码:

public class SnowFlake {

    private static final int GroupId_Bit_Num = 5;
    private static final int ServerId_Bit_Num = 15;
    private static final int Sequence_Bit_Num = 11;
    private static final int Timestamp_Bit_Num = 63 - GroupId_Bit_Num - ServerId_Bit_Num - Sequence_Bit_Num;

    private static final int GroupId_Bit_Shift = ServerId_Bit_Num + Sequence_Bit_Num + Timestamp_Bit_Num;
    private static final int ServerId_Bit_Shift = Sequence_Bit_Num + Timestamp_Bit_Num;
    private static final int Sequence_Bit_Shift = Timestamp_Bit_Num;

    private static final long Sequence_Mark_Flag = (1L << Sequence_Bit_Num) - 1;
    private static final long Timestamp_Mark_Flag = (1L << Timestamp_Bit_Num) - 1;

    /** 设置一个时间起始点 */
    private static long Epoch = 0;
    private static long Default_Epoch_Date = 1671073633L; // 2022-12-15 10:38:50
    private static long Time_Offset = 5;

    private static long groupId;
    private static long serverId;
    private static long sequence;
    private static long lastTime = System.currentTimeMillis() / 1000;

    /**
     * 初始化算法的字段
     * @param groupId
     * @param serverId
     * @param epochDate 初始化时间起点(null表示默认起始日期),后期修改会导致id重复,如果要修改,时间只能往前改,不能往后改
     */
    public static void init(int groupId, int serverId, Date epochDate) {
        if (groupId <= 0 || groupId >= (1 << GroupId_Bit_Num)) {
            throw new IllegalArgumentException("groupId is out of range, 0 ~ " + (1 >> GroupId_Bit_Num));
        }
        if (serverId <= 0 || serverId >= (1 << ServerId_Bit_Num)) {
            throw new IllegalArgumentException("serverId is out of range, 0 ~ " + (1 >> ServerId_Bit_Num));
        }
        if (epochDate != null) {
            SnowFlake.Epoch = epochDate.getTime() / 1000;
        } else {
            SnowFlake.Epoch = Default_Epoch_Date;
        }

        SnowFlake.groupId = groupId;
        SnowFlake.serverId = serverId;
    }

    public static synchronized long nextId() {
        long currentTime = System.currentTimeMillis();
        long timestamp = currentTime / 1000;
        if (timestamp < lastTime) {
            // 防止时钟回拨
            if(lastTime - timestamp < 2) {
                timestamp = lastTime;
            } else {
                throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for {} s", lastTime - timestamp));
            }
        }
        if (timestamp == lastTime) {
            long newSequence = (sequence + 1) & Sequence_Mark_Flag;
            if (newSequence == 0) {
                timestamp = tilNextSecond(lastTime);
            }
            sequence = newSequence;
        } else {
            sequence = 0;
        }
        lastTime = timestamp;

        return (groupId << GroupId_Bit_Shift)
                | (serverId << ServerId_Bit_Shift)
                | (sequence << Sequence_Bit_Shift)
                | ((timestamp - Epoch) & Timestamp_Mark_Flag);
    }

    private static long tilNextSecond(long lastSecond) {
        long timestampNow = System.currentTimeMillis() / 1000;
        while (timestampNow == lastSecond) {
            timestampNow = System.currentTimeMillis() / 1000;
        }
        if (timestampNow < lastSecond) {
            throw new RuntimeException("Clock moved backwards");
        }

        return timestampNow;
    }

}

到现在为止,有几个问题:
1.为什么将时间戳放在id的二进制的最后一部分,而不是sequence放在最后面,有什么区别?
2.代码中设置时间起始点epoch有什么用处?
3.上面的代码,如果发生了1秒的时钟回拨的情况,会发生什么?
4.在面对流量高峰的时候,频繁生成id会发生什么状况?

上面这段代码是按照一个开源的java工具包的实现来写的,来说一下我对上面这段代码的理解:
1.与原来的实现方式相比,将时间戳(而不是sequence)放在最后一部分,是因为每次进入下一秒的时候,sequence字段都会被重置为0,在流量并发没那么高的情况下,大多数生成的id的sequence部分都是1。当我们使用id去做散列的时候(比如id % 1024),这种情况下得到的结果大部分都是1,散列结果高度集中,通常容易导致一些意想不到的问题。因此把时间戳放在最末尾,生成的id往往具有更好的散列效果。

2.设置时间起始点epoch,是因为时间戳的字段的位数有限制,使得算法有效期为130年左右,从1974年开始计数的时间戳,到2106将超出32位的表示范围。在超出该时间范围之后,生成的id将有可能重复。而起始时间点的设置,可以将该算法的使用时间窗口范围从1974~2106改为2022~2158。
另外使用该算法的过程中,修改起始时间点将会导致生成的id重复。

3.时钟回拨了1s钟的时候,是代码允许的时钟回拨范围,但是tilNextSecond()方法依然会报错。

4.由于sequence长度限制的原因,在1秒的时间范围内最多只能生成2048个id。如果在1秒的时间内,快速消耗完了,那么接下来调用nextId()的线程将会进入轮询1秒的轮询中,而由于这个方法使用synchronized同步,其他调用nextId()的线程将进入阻塞状态。也就是说,在大流量、高并发的情况下,这个nextId()的方法有可能经常会导致线程集体阻塞。


针对sequence长度耗尽导致的阻塞问题,一个自然而然的解决办法就是增加sequence字段的长度。但是增加sequence长度,对应的就是需要减少时间戳的长度,而且在流量空闲的情况又会是一种浪费。

因此到这里就想设计了一种新的生成id的思路,代码如下:
  
    private static long lastTime = System.currentTimeMillis() / 1000;
    
    public static synchronized long nextId() {
        long newSequence = (sequence + 1) & Sequence_Mark_Flag;
        if (newSequence == 0) {
            lastTime ++;
        }
        sequence = newSequence;

        return (groupId << GroupId_Bit_Shift)
                | (serverId << ServerId_Bit_Shift)
                | (sequence << Sequence_Bit_Shift)
                | ((lastTime - Epoch) & Timestamp_Mark_Flag);
    }

原理很简单,使用进程启动时的时间作为时间戳,每次每个时间戳的序列耗尽的时候,才会使用下一个时间戳。这样,不需要考虑时钟回拨的问题,也不需要担心每秒钟生成的id超过限制时会阻塞线程。

但是很快就会发现问题,假如进程每秒生成的id远远超过2048个,那么lastTime字段会迅速增加而超过当前时间,如果这个时候服务器重启了,lastTime会被重新设置为当前时间,从而出现id重复的情况。
那么我们是不是可以在lastTime超过当前时间时,阻塞当前线程呢?代码如下:
  
    private static long Time_Offset = 5;    

    public static synchronized long nextId() {
        long newSequence = (sequence + 1) & Sequence_Mark_Flag;
        if (newSequence == 0) {
            lastTime ++;

            long timestampSecond = System.currentTimeMillis() / 1000;
            if (lastTime - timestampSecond > Time_Offset) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        sequence = newSequence;

        return (groupId << GroupId_Bit_Shift)
                | (serverId << ServerId_Bit_Shift)
                | (sequence << Sequence_Bit_Shift)
                | ((lastTime - Epoch) & Timestamp_Mark_Flag);
    }

每次sequence用完时,当检测到lastTime超过当前时间5秒的时候都会阻塞1秒。这样依然会存在sequence长度耗尽导致的阻塞的问题,但是有5 * 2048个id可以备用当作缓冲(5秒是服务器的重启时间最小值,从服务器关机到下一次服务重新启动成功之间的时间,这些时间戳的id也是可以利用的),而且在服务器流量低谷的时候,没有用到的时间戳的id,在流量高峰的时候可以尽数使用,相当于削峰填谷了。

当然,以上的设计只在频繁调用nextId()生成全局唯一id,从而很容易导致sequence用完的场景。不同的项目的实际应用场景不同,如果sequence字段长度足够长,且调用频率不是那么高,使用初版的生成方式就足够了。

总结

1.雪花算法生成id的每个字段的长度需要根据不同的业务去决定。
2.合理的设置起始时间点有助于判断有效时间是否全部耗尽。
3.使用该算法的过程中,修改起始时间点将会导致生成的id重复。
4.每秒生成的id数量与sequence字段有关, 超出时将阻塞线程直到进入下一秒
5.使用进程启动时的时间作为时间戳,每次每个时间戳的序列耗尽的时候,才使用下一个时间戳。并且每次sequence用完时,检测到lastTime超过当前时间5秒的时候都会阻塞一定时间的线程,可以一定程度上缓解阻塞频率。

  
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

关于雪花算法的设计与思考 的相关文章

  • 云台控制协议VISCA、PELCO-D、PELCO-P

    原 云台控制协议VISCA PELCO D PELCO P 2013年12月02日 18 42 21 autowanglei 阅读数 xff1a 10146 更多 lt div class 61 34 tags box space 34 g
  • 目录和文件权限与 umask 关系

    一 权限 文件权限 xff1a r xff1a 读取文件内容的权限 w xff1a 新增 修改和删除文件内容的权限 x xff1a 执行文件的权限 例如 xff1a 一个文件a sh xff0c 它的权限是rw xff0c 使用 a sh
  • open 函数的 flag 参数和错误代码

    一 flag 参数 定义头文件 xff1a lt bits fcntl linux h gt 必选参数说明 xff1a define O ACCMODE 0003 xff1a 读写文件操作时取出 flag 的低两位 define O RDO
  • 系统全局变量 errno 是如何获得 errno.h 中的值的呢?

    很多时候我们在使用 errno 的时候都知道它代表的是 errno h 中的错误值 xff0c 可是为什么它就是代表那些值的呢 xff1f 系统在哪里给它赋值了呢 xff1f 故事就要从源头开始 1 errno 全局变量是在哪里定义的 xf
  • const pointer

    int a b const int p 61 a 与int const p 61 a 是一样的 表示p可以指向a xff0c 也可以改变指向b xff0c 但是不能通过指针p来修改a的值 p 61 b p 61 4 int const q
  • I_O —基础概念(参照 Ubuntu 16.04 版本)

    一 文件的概念 定义 xff1a 所谓文件是指一组相关数据的有序集合 xff0c 这个数据集有一个名称 xff0c 叫文件名 如源程序文件 xff0c 目标文件 xff0c 可执行文件 xff0c 头文件等文件通常是在驻留在外部介质上的 x
  • 网络通信2—UDP 模型程序编写步骤(参照 Ubuntu 16.04 版本)

    UDP 模型程序编写步骤 一 UDP基础模型 服务器流程 step 1 xff1a 创建 socket 套接字接口并判断 sockfd 61 socket AF INET SOCK DGRAM 0 if sockfd 61 61 1 per
  • switch 中 break 和 continue 的区别

    1 break 用来退出 switch xff0c continue 本身是不能用在 switch 里的 xff0c 他必须结合循环来用 xff0c 表示跳过本次循环 2 switch 的 case 语句最后如果没有加 break cont
  • 立即数

    一 概念 xff1a 通常把在 立即寻址方式 指令中给出的数称为立即数 二 判断步骤 xff1a 把数据转换成二进制 xff0c 从低到高写成 4 个一组 xff0c 最高位不够一组的补 0 xff1b 数 1 的个数 xff0c 如果大于
  • 位、字节、char、int(32位系统) 之间的关系

    一 概念 xff1a 位 xff08 bit xff09 xff1a 计算机中最小的数据单位 每一位的状态只能是0或1 字节 xff08 byte xff09 xff1a 存储空间的基本计量单位 xff0c 8 个二进制位构成1个字节 1
  • C语言中的那些宏

    DATE 进行预处理的日期 xff08 Mmm dd yyyy 形式的字符串文字 xff09 FILE 代表当前源代码文件名的字符串文字 LINE 代表当前源代码中的行号的整数常量 TIME 源文件编译时间 xff0c 格式微 hh xff
  • 任务栈简单入门

    最近又把两本进阶书看了一遍 xff0c 但总感觉好记性不如烂笔头 xff0c 所以还是决定通过博客记录一下 xff0c 我们将分两篇来全面深入地记录Activity 启动模式与任务栈的内容 android任务栈简单了解 1 android任
  • VS2010里函数枚举

    一 cout函数 说明 xff1a 调用该函数必须申明头文件 include lt iostream gt 同时声明后面必须使用 using namespace std 正确书写为 xff1a include lt iostream gt
  • I_O—标准 I_O 实验

    一 测试标准 I O 一次可以同时打开多少个文件 1 实验思路 xff1a 利用循环同时打开文件 xff0c 直到不能打开 2 代码如下 xff1a 二 fgetc 和 fputc 实现拷贝文件并输出文件行数 1 实验思路 xff1a 打开
  • Source Insight 配色方案

    Source Insight 对于程序员来说应该不陌生 xff0c 当然一个个性化的编程界面也会让自己赏析悦目 xff0c 下面就将个人的界面设置分享一下 xff1a 一 背景色设置 1 选择 Options Preferences 2 选
  • Linux 网络——交换机不能用两根网线相连

    同一个局域网所有的交换机之间可以用网线串联起来 xff0c 但绝对不能使任意 gt 61 2个交换机形成环路 xff0c 否则局域网内将形成广播风暴 xff0c 所用局域网内的用户都将不能上网 例如局域网内的交换机可以使用如下相连 xff1
  • GDB 知识点——基础操作

    Linux C 中的 GDB 调试使用 xff1a 1 GDB 的主要功能 xff1a 1 启动被调试程序 2 让被调试的程序在指定的位置停住 3 当程序被停住时 xff0c 可以检查程序状态 xff08 如变量的值 xff09 2 检查
  • 员工管理系统(C 语言)——项目说明

    项目名称 xff1a 员工管理系统 项目目的 xff1a 1 实现简单的公司对员工信息的管理 2 通过项目锻炼实现逻辑转换为代码的能力 3 利用函数封装实现项目过程中的逻辑过程以及需求功能的实现 4 学会数据库的操作以及网络通信 5 强化代
  • 员工管理系统(C 语言)——客户端解析

    源码下载地址 xff1a https download csdn net download wenfei11471 10477504 客户端功能 xff1a 1 运行时先测试是否能连通服务器 xff08 不畅通如下图所示 xff09 xff
  • 员工管理系统(C 语言)——服务器解析

    源码下载地址 xff1a https download csdn net download wenfei11471 10477504 服务器功能 xff1a 1 运行时主界面 xff08 服务器启动后 xff0c 只有管理员下线 xff0c

随机推荐

  • 排序——选择排序、冒泡排序和快速排序比较

    一 选择排序思路 xff1a 1 以 int 类型为例 2 拿第一个数与后面数相比较 xff0c 如果比后面的数大则交换 3 拿第二个数与后面的数比较 xff0c 如果比后面的数大则交换 4 直到比较到倒数第二个数 xff0c 最后一个数不
  • C 语言中 const 与指针的结合使用

    请区分一下几种指针的区别 1 const int p 2 int const p 3 int const p 4 const int const p 5 const int const p 解析 xff1a 1 const int p 中
  • Ubuntu16.04上安装百度网盘后打不开

    现在百度网盘推出了Linux版本 xff0c 也有Ubuntu下安装的deb文件 xff0c 但是我在Ubuntu上安装后却打不开 xff0c 报错 baidunetdisk crashed with SIGABRT in gnu cxx
  • C/C++的“文件包含”处理时头文件被重复包含的问题探究及解决方法(用最简单的例子进行说明)

    这篇博文是博文https blog csdn net wenhao ir article details 125668051的配套博文 头文件被重复包含是下面这样的现象 xff1a A文件里包含了C文件 xff0c B文件里也包含了C文件
  • BIN,BCD,ASCII码分别对应的Hex(16进制)数

    BIN BCD ASCII码分别对应的Hex xff08 16进制 xff09 数 以十进制的 56 为例 BIN 码 对应二进制数为 0011 1000对应Hex数据为 0x38BIN码就是二进制数 xff1b 压缩BCD 码 对应二进制
  • .LDS 文件详解

    最近在研究uboot xff0c 红色部分为我加上的注解 转载地址 xff1a http blog chinaunix net space php uid 61 23373524 amp do 61 blog amp cuid 61 232
  • 13 select的优化一

    1 上个例子中 xff0c select通过for循环轮询client套接字 xff0c 轮询的范围比较大 xff0c 有优化的地方 2 优化代码 xff1a 通过数组存储client的套接字 xff0c 达到少轮询的效果 xff0c 可以
  • 二.手写迷你版Tomcat-minicat2.0

    minicat 1 0我们实现了返回固定的字符串 34 Hello minicat 34 minicat 2 0需求 xff1a 封装Request和Response对象 xff0c 返回html静态资源文件 封装Request对象 想要封
  • 三.手写迷你版Tomcat-minicat3.0

    minicat 1 0我们实现了返回固定的字符串 34 Hello minicat 34 minicat 2 0封装Request和Response对象 xff0c 返回html静态资源文件 minicat 3 0需求 xff1a 请求se
  • python爬取全国五级行政区

    以前爬过国家统计局的四级行政区 xff08 http www stats gov cn tjsj tjbz tjyqhdmhcxhfdm 2017 xff09 xff0c 但是对于五级数据效果不是很好 偶然间发现这个网站 xff1a htt
  • ElasticSearch使用elasticsearchTemplate聚合查询

    这两天正好做个需求 xff0c 需要用到聚合查询 前几篇文章只是简单的提到过 xff0c 并没有真正的运用到实际产出中 xff0c 本篇结合实际代码 xff0c 专项学习ES的聚合查询 1 业务背景 有一张地址索引表 xff1a hisAd
  • Java字节码

    Java最黑科技的玩法就是字节码编程 xff0c 也就是动态修改或是动态生成 Java 字节码 使用字节码可以玩出很多高级的玩法 xff0c 最高级的还是在 Java 程序运行时进行字节码修改和代码注入 听起来是不是一些很黑客 xff0c
  • TCP/IP (一) accept建立连接

    七层网络协议 三次握手 四次分手 xff0c 这些大家都比较熟知 xff0c 这里主要是带着一些问题来思考整个TCP IP流程 1 三次握手的具体流程是怎么样的 xff1f 2 socket编程中int listen int fd int
  • http 的认证模式

    周海汉 2006 7 11 ablozhou 64 gmail com SIP类似Http协议 其认证模式也一样 Http协议 xff08 RFC 2616 xff09 规定可以采用Base模式和摘要模式 xff08 Digest sche
  • Java Agent

    在 Java 字节码 一文中有提到 xff0c 使用 Java Agent 操控字节码 xff0c 本文将讨论 Java Agent xff0c 这是普通 Java 开发人员的真正的黑魔法 Java Agent 能够通过执行字节码的直接修改
  • 通过gitlab远程统计git代码量

    git的代码量大多数都是根据命令行统计 xff0c 或者根据第三方插件统计 但是都不满足我的需求 xff0c 因为我们代码都由gitlab管理 xff0c 于是想到了通过gitlab暴露出来的接口获取数据 第一步 xff0c 生成私钥 登录
  • Qt第二十二章:将控件放到另一个控件的后面或前面

    话不多说 xff1a 看图
  • 缓存行填充与@sun.misc.Contended注解

    1 缓存模型 CPU和主内存之间有好几层缓存 xff0c 因为与cpu的速度相比 xff0c 访问主内存的速度是非常慢的 如果频繁对同一个数据做运算 xff0c 每次都从内存中加载 xff0c 运算完之后再写回到主内存中 xff0c 将会严
  • ThreadLocal那点事

    目录 1 ThreadLocal原理 2 ThreadLocal内存泄漏 3 ThreadLocal最佳实践 4 FastThreadLocal原理 5 FastThreadLocal最佳实践 6 ThreadLocal与FastThrea
  • 关于雪花算法的设计与思考

    2017年的时候项目组在开发一款大区游戏 xff0c 由于之前demo阶段的玩家id都是单服生成的 xff0c 只能保证单进程中的唯一 xff0c 而无法保证在分布式服务器端的唯一性 随着项目的开发进展 xff0c 需要设计能保证在分布式的