雪花算法:分布式唯一ID生成利器

码界启航者
• 阅读 5489

前言

无论是在分布式系统中的ID生成,还是在业务系统中请求流水号这一类唯一编号的生成,都是软件开发人员经常会面临的一场景。而雪花算法便是这些场景的一个解决方案。

以分布式ID为例,它的生成往往会在唯一性、递增性、高可用性、高性能等方面都有所要求。并且在业务处理时,还要防止爬虫根据ID的自增进行数据爬取。而雪花算法,在这些方面表现得都不错。

常见分布式ID生成

市面上比较常见的分布式ID生成算法及类库:

UUID:Java自带API,生成一串唯一随机36位字符串(32个字符串+4个“-”)。可以保证唯一性,但可读性差,无法有序递增。

SnowFlake:雪花算法,Twitter开源的由64位整数组成分布式ID,性能较高,并且在单机上递增。GitHub上官方地址:https://github.com/twitter-ar...

UidGenerator:百度开源的分布式ID生成器,基于雪花算法。GitHub参考链接:https://github.com/baidu/uid-... 。该项目的说明文档及测试案例都值得深入学习一下。

Leaf:美团开源的分布式ID生成器,能保证全局唯一,趋势递增,但需要依赖关系数据库、Zookeeper等中间件。相关实现可参考该文:https://tech.meituan.com/2017...

雪花算法

雪花(snowflake),美丽、独特又变幻莫测。在大自然中几乎找不到两片完全一样的雪花。雪花的这些特性正好在雪花算法上有所展示。

SnowFlake算法是Twitter开源的分布式ID生成算法。核心思想就是:使用一个64 bit的 long 型的数字作为全局唯一ID。算法中还引入了时间戳,基本上保证了自增特性。

最初的版本的雪花算法是基于scala写的,当然,不同的编程语言都可以根据其算法逻辑进行实现。

### 雪花算法原理

SnowFlake算法生成ID的结果是一个64bit大小的整数,结构如下图:

雪花算法:分布式唯一ID生成利器

算法解析

  • 第一个部分:1个bit,无意义,固定为0。二进制中最高位是符号位,1表示负数,0表示正数。ID都是正整数,所以固定为0。
  • 第二个部分:41个bit,表示时间戳,精确到毫秒,可以使用69年。时间戳带有自增属性。
  • 第三个部分:10个bit,表示10位的机器标识,最多支持1024个节点。此部分也可拆分成5位datacenterId和5位workerId,datacenterId表示机房ID,workerId表示机器ID。
  • 第四部分:12个bit,表示序列化,即一些列的自增ID,可以支持同一节点同一毫秒生成最多4095个ID序号。

由于在Java中64bit的整数是long类型,所以在Java中SnowFlake算法生成的id就是long来存储的。

雪花算法Java实现

雪花算法Java工具类实现:

public class SnowFlake {

    /**
     * 起始的时间戳(可设置当前时间之前的邻近时间)
     */
    private final static long START_STAMP = 1480166465631L;

    /**
     * 序列号占用的位数
     */
    private final static long SEQUENCE_BIT = 12;
    /**
     * 机器标识占用的位数
     */
    private final static long MACHINE_BIT = 5;
    /**
     * 数据中心占用的位数
     */
    private final static long DATA_CENTER_BIT = 5;

    /**
     * 每一部分的最大值
     */
    private final static long MAX_DATA_CENTER_NUM = ~(-1L << DATA_CENTER_BIT);
    private final static long MAX_MACHINE_NUM = ~(-1L << MACHINE_BIT);
    private final static long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT);

    /**
     * 每一部分向左的位移
     */
    private final static long MACHINE_LEFT = SEQUENCE_BIT;
    private final static long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    private final static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;

    /**
     * 数据中心ID(0~31)
     */
    private final long dataCenterId;
    /**
     * 工作机器ID(0~31)
     */
    private final long machineId;
    /**
     * 毫秒内序列(0~4095)
     */
    private long sequence = 0L;
    /**
     * 上次生成ID的时间截
     */
    private long lastStamp = -1L;

    public SnowFlake(long dataCenterId, long machineId) {
        if (dataCenterId > MAX_DATA_CENTER_NUM || dataCenterId < 0) {
            throw new IllegalArgumentException("dataCenterId can't be greater than MAX_DATA_CENTER_NUM or less than " +
                    "0");
        }
        if (machineId > MAX_MACHINE_NUM || machineId < 0) {
            throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
        }
        this.dataCenterId = dataCenterId;
        this.machineId = machineId;
    }

    /**
     * 产生下一个ID
     */
    public synchronized long nextId() {
        long currStamp = getNewStamp();
        if (currStamp < lastStamp) {
            throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
        }

        if (currStamp == lastStamp) {
            //相同毫秒内,序列号自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            //同一毫秒的序列数已经达到最大
            if (sequence == 0L) {
                //阻塞到下一个毫秒,获得新的时间戳
                currStamp = getNextMill();
            }
        } else {
            //不同毫秒内,序列号置为0
            sequence = 0L;
        }

        lastStamp = currStamp;

        // 移位并通过或运算拼到一起组成64位的ID
        return (currStamp - START_STAMP) << TIMESTAMP_LEFT //时间戳部分
                | dataCenterId << DATA_CENTER_LEFT       //数据中心部分
                | machineId << MACHINE_LEFT             //机器标识部分
                | sequence;                             //序列号部分
    }

    private long getNextMill() {
        long mill = getNewStamp();
        while (mill <= lastStamp) {
            mill = getNewStamp();
        }
        return mill;
    }

    private long getNewStamp() {
        return System.currentTimeMillis();
    }

    public static void main(String[] args) {
        SnowFlake snowFlake = new SnowFlake(11, 11);

        long start = System.currentTimeMillis();
        for (int i = 0; i < 10; i++) {
            System.out.println(snowFlake.nextId());
        }

        System.out.println(System.currentTimeMillis() - start);
    }
}

上述代码中,在算法的核心方法上,通过加synchronized锁来保证线程安全。这样,同一服务器线程是安全的,生成的ID不会出现重复,而不同服务器由于机器码不同,就算同一时刻两台服务器都产生了雪花ID,结果也是不一样的。

其他问题

41位时间戳最长只能有69年

下面来用程序推算一下,41位时间戳为什么只能支持69年。

41的二进制,最大值也就41位都是1,也就是说41位可以表示2^{41}-1个毫秒的值,转化成单位年则是(2^{41}-1) / (1000 60 60 24 365) = 69年。

通过代码验证一下:

public static void main(String[] args) {
   //41位二进制最小值
   String minTimeStampStr = "00000000000000000000000000000000000000000";
   //41位二进制最大值
   String maxTimeStampStr = "11111111111111111111111111111111111111111";
   //转10进制
   long minTimeStamp = new BigInteger(minTimeStampStr, 2).longValue();
   long maxTimeStamp = new BigInteger(maxTimeStampStr, 2).longValue();
   //一年总共多少毫秒
   long oneYearMills = 1L * 1000 * 60 * 60 * 24 * 365;
   //算出最大可以多少年
   System.out.println((maxTimeStamp - minTimeStamp) / oneYearMills);
}

所以,雪花算法生成的ID只能保证69年内不会重复,如果超过69年的话,那就考虑换个服务器(服务器ID)部署,并且要保证该服务器的ID和之前都没有重复过。

前后端数值类型

在使用雪花算法时,由于生成的ID是64位,在传递给前端时,需要考虑以字符串的类型进行传递,否则可能会导致前端类型溢出,再回传到服务器时已经变成另外一个值。

这是因为Number类型的ID在JS中最大只支持53位,直接将雪花算法的生成的ID传递给JS,会导致溢出。

小结

生成唯一性ID(其他数据)是几乎在每个系统中都会有的场景,对其生成算法不仅要保证全局唯一性、趋势递增性,还要保证信息安全(比如被爬取数据),同时还要保证算法的高可用性(QPS、可行5个9、平均延时、TP999等指标)。这就对ID生成的算法有一定的要求,而雪花算法算是一个不错的选择。

但它也是有一定的缺点的,比如强依赖机器时钟,如果机器上的时钟回拨,会导致重复或服务不可用的问题,这也是我们在使用时需要注意的事项。

博主简介:《SpringBoot技术内幕》技术图书作者,酷爱钻研技术,写技术干货文章。

公众号:「程序新视界」,博主的公众号,欢迎关注~

技术交流:请联系博主微信号:zhuan2quan

点赞
收藏
评论区
推荐文章
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
3年前
java 商城系统源码分享
目的snowflake是常见的id(编号)生成算法,由时间戳业务id机器id序列号组合而成,在电商系统(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fwww.javamall.com.cn%2F)中,用于订单号的生成、支付单号的生成等等。本发号器主要解决在容器化的部署情
捉虫大师 捉虫大师
4年前
如何设计一款“高可用高性能”的发号器
本文已收录https://github.com/lkxiaolou/lkxiaolou欢迎star。背景在分布式场景中,很多地方需要生成全局唯一的id,如数据库分库分表后需要用唯一id代替单机版本的自增id。发号器的基本要求是全局唯一,无论如何都不能重复某些场景下还要求单调递增,如排序需求等。网上有很多介绍发号器的文章,比如美团的《Leaf——美团点
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Stella981 Stella981
3年前
IM消息ID技术专题(六):深度解密滴滴的高性能ID生成器(Tinyid)
1、引言在中大型IM系统中,聊天消息的唯一ID生成策略是个很重要的技术点。不夸张的说,聊天消息ID贯穿了整个聊天生命周期的几乎每一个算法、逻辑和过程,ID生成策略的好坏有可能直接决定系统在某些技术点上的设计难易度。有中小型IM场景下,消息ID可以简单处理,反正只要唯一就行,而中大型场景下,因为要考虑到分布式的性能、一致性等,所以要考虑的问题
Stella981 Stella981
3年前
SnowFlake分布式唯一ID生成器
写在前面架构是权衡的结果,架构也是一层层的组件拼接起来的,如果没有好用的组件,架构势必会做阉割,架构的理想态是建立在一堆友好、易用、标准化的组件之上的。在我过去的经验中,有两类组件经常会出现在我的架构方案中,一个是唯一ID生成器,一个是推拉结合配置中心,有了他们我可以解决分布式系统下的时序问题,不会再因数据不一致问题手足无措,推拉结合的数据中心
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
拆解雪花算法生成规则 | 京东物流技术团队
雪花算法(Snowflake)是一种生成分布式全局唯一ID的算法,生成的ID称为SnowflakeIDs或snowflakes。这种算法由Twitter创建,并用于推文的ID。目前仓储平台生成ID是用的雪花算法修改后的版本。
Vitess全局唯一ID生成的实现方案 | 京东云技术团队
为了标识一段数据,通常我们会为其指定一个唯一id,比如利用MySQL数据库中的自增主键。但是当数据量非常大时,仅靠数据库的自增主键是远远不够的,并且对于分布式数据库只依赖MySQL的自增id无法满足全局唯一的需求。因此,产生了多种解决方案,如UUID,Sn
融云IM即时通讯 融云IM即时通讯
9个月前
融云IM干货丨IM聊天室中客户端如何确保消息同步的准确性?
客户端确保消息同步的准确性主要依赖于以下几个关键技术和策略:全局唯一的消息ID生成策略:为了保证消息可以通过ID进行识别和排重,IM系统采用全局唯一的消息ID生成策略。这种策略可以确保每条消息都有一个唯一的标识符,从而在消息的发送和接收过程中避免重复。客户
码界启航者
码界启航者
Lv1
不见穿针妇,空怀故国楼。
文章
2
粉丝
0
获赞
0