分分钟都妙不可言是什么歌| 117是什么意思| 生日吃什么| 梦见白猫是什么预兆| 锁骨疼挂什么科| 淡泊名利是什么意思| 晚上尿多是什么病| 小五行属性是什么| 头皮上长疣是什么原因造成的| 八十岁叫什么之年| 惊蛰是什么季节的节气| 腋下发黑是什么原因| 衣带渐宽终不悔是什么意思| 来月经头疼是什么原因| 便秘什么意思| 血液粘稠吃什么药| 婴儿的腿为什么是弯弯的| 落幕是什么意思| 淼念什么| 右手无名指戴戒指是什么意思| 肝郁化火是什么意思| 氯化钠是什么东西| 胃痛吃什么好得快| 10月20是什么星座| 梦见金项链是什么意思| 朝三暮四是什么生肖| 牛奶盒属于什么垃圾| nautical什么牌子| 没有力气是什么原因| 花中之王是什么花| 天津市市长什么级别| 子宫肌瘤做什么检查能查出来| 吃什么疏通血管最快| 乳头痒什么原因| vca是什么牌子| 白袜子是什么意思| 1988年什么命| 农历五月初五是什么星座| 拔智齿后吃什么| 森达属于什么档次的鞋| 林冲是什么生肖| 眼睛有血丝是什么原因| 叶酸什么时候吃最好| 什么是三位一体| 骆驼奶有什么功效| 经典什么意思| 补牙是什么意思| 高三吃什么补脑抗疲劳| 八月十二是什么星座| 诗韵是什么意思| 双鱼座的上升星座是什么| 什么是夜店| 梨是什么季节的水果| 蚩尤姓什么| 阴历七月是什么星座| 什么是sm| 尿频吃什么药最快见效| 阴部潮湿是什么原因| 印字五行属什么| 断背山讲的是什么故事| 蛇盘疮什么原因引起的| cro公司是什么意思| 六月一日什么星座| 什么海翻江| 右眼皮一直跳是什么预兆| 手脚热是什么原因| 孩子流黄鼻涕吃什么药效果好| 秦皇岛有什么特色美食| 牙龈起包是什么原因| 肚脐眼上面疼是什么原因| 李世民属什么生肖| 清朝是什么时候灭亡的| 芹菜和什么一起炒好吃| 小case是什么意思| 甲状腺分泌什么激素| 什么是借读生| 日语一个一个是什么意思| 52什么意思| 什么是双一流| 藜麦是什么| 转呼啦圈有什么好处| 下象棋有什么好处| 走马观花是什么意思| 梦到兔子是什么征兆| 牙齿涂氟是什么意思| 六月初八是什么日子| 住院预交金是什么意思| 香肠炒什么菜好吃| 金钱草长什么样子图片| 椰浆是什么| 幼儿园中班学什么| 认真地什么| 小孩咬手指甲是什么原因| 白色搭配什么颜色好看| 办健康证在什么地方办| 11.1是什么星座| 纷至沓来是什么意思| a1微球蛋白偏高说明什么意思| 嘴唇干燥是什么原因| 脚趾甲凹凸不平是什么原因| 4个月念什么字| 低钾血症是什么病| 攀缘是什么意思| 什么是应激反应| 梦到墓地什么预兆| 金牛座和什么星座最配| 微商是什么意思| 我国的国花是什么| 麦芽是什么| 飞亚达手表什么档次| medium什么意思| 牙痛吃什么药效果最好| 草木皆兵指什么生肖| 肝藏血是什么意思| 颈椎病引起的头晕吃什么药| 外阴白斑瘙痒抹什么药| 瞳孔扩散意味着什么| 列文虎克发现了什么| 睡觉时胳膊和手发麻是什么原因| 坐飞机需要什么| 支原体培养及药敏是检查什么| 96年是什么年| 正常的白带是什么样的| NPY什么意思| 七月初七是什么节日| 幽门螺旋杆菌感染吃什么药| 什么玉最好有灵性养人| 什么现象证明你在长高| 牛跟什么生肖相冲| 右眼皮一直跳是什么预兆| 蓝色加红色是什么颜色| 94年是什么年| 全托是什么意思| 甜菜碱是什么东西| 大年初一是什么星座| 一 什么云| 随礼钱有什么讲究| 为什么会得甲状腺| 金蝉子是什么佛| 周杰伦为什么叫周董| 脸上长毛什么原因| 地心引力是什么意思| 眼睛怕光是什么原因| 龟头上有小红点是什么| 乙肝病毒表面抗体高是什么意思| 喝茶叶茶有什么好处和坏处| 午时右眼跳是什么预兆| 看到壁虎是什么征兆| 梦里见血代表什么预兆| 什么是麻疹| 火焰山为什么这么热| 馨是什么意思| 9月25号什么星座| 高烧不退有什么好办法| 880什么意思| 多吃黄瓜有什么好处和坏处| 棚户区改造和拆迁有什么区别| 身份证上x代表什么| 李白为什么叫青莲居士| 什么情况下需要做肠镜检查| 梦见着大火了是什么征兆| 什么星球最大| 爆栗什么意思| 手上十个簸箕代表什么| score什么意思| 滴滴是什么意思| 营养师是干什么的| 天然气什么味道| 男士内裤什么材质的好| 佛跳墙是什么东西| 鸟字旁的字大多和什么有关| 直肠肿瘤手术后吃什么| 值神天刑是什么意思| fsw是什么意思| 伟哥是什么药| 树大招风的意思是什么| 磨盘有什么风水说法| 金匮肾气丸治什么病| 推崇是什么意思| 发烧是什么感觉| 元朝是什么民族| 深度水解奶粉是什么意思| 华西医院院长什么级别| 六五年属什么| 吃地瓜叶有什么好处和坏处| 气化是什么意思| 4月2号是什么星座| 脚老是抽筋是什么原因| 为什么伴娘要未婚| 人越来越瘦是什么原因| 甲亢有什么症状表现| bossini是什么牌子| 干咳有痰是什么原因| 谢霆锋什么学历| 条索灶是什么意思| 纸是什么生肖| 蜂蜜和什么不能一起吃| 西康省是现在什么地方| 为什么一抽烟就想拉屎| 腋下有异味是什么原因导致的| 一代明君功千秋是什么生肖| 高血压会引起什么病症| 南乳是什么| 肝郁血瘀吃什么中成药| 头晕头疼挂什么科| 10月出生是什么星座| 大麻是什么| 心肌酶能查出什么病| 凶神宜忌是什么意思| 月经推迟挂什么科| 狗狗感冒吃什么药| 8月28号是什么日子| 什么的寒风| 低血糖是什么症状| 什么算高危性行为| 王俊凯什么星座| 吃什么对头发有好处| 阑尾炎吃什么药| 3月30日是什么星座| 梦见狗咬人是什么预兆| 爱做梦是什么原因应该怎样调理| 彼岸花又叫什么花| edsheeran为什么叫黄老板| 甲状腺结节挂什么科室| 蛋白尿是什么样子| 蛇的五行属什么| 尿素高什么原因| 579是什么意思| 天蝎什么象| 肠镜检查前需要做什么准备工作| 鸡胸是什么原因引起的| 送礼送什么烟比较好| 夏天流鼻血是什么原因| 平菇不能和什么一起吃| 血尿是什么原因| 众生是什么意思| 1月10号什么星座| 什么是白虎| 蝉什么时候出现| 对蚊子过敏是什么症状| 入殓师是干什么的| 磨牙齿有什么方法可以治| 做书桌用什么板材好| 病毒性疣是什么病| 法王是什么意思| 图什么| coat是什么意思中文| 扁桃体肿大是什么原因引起的| 大象灰是什么颜色| 梦见老牛是什么意思| 甲减喝什么药| 东是什么生肖| 花魁是什么意思| 韩字五行属什么| 姜字五行属什么| 肝脏的主要功能是什么| 庚寅五行属什么| 东是什么生肖| 老是流鼻血是什么原因| 梦见吃李子是什么意思| 见利忘义是什么生肖| 基底是什么意思| 血糖高要注意什么| 淋巴癌是什么| 百度

向心性肥胖是什么意思

本文围绕分布式ID生成展开,介绍了分布式ID的特性及常见算法,重点阐述雪花算法。它能生成全局唯一递增ID,灵活性高、速度快,但依赖机器时钟,存在时钟回拨问题。文中给出Java代码实现,还探讨了时钟回拨的解决办法,如时间戳自增、缓存序列号、等待校正等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

雪花算法snowflake是一种优秀的分布式ID生成方案,其优点突出:它能生成全局唯一且递增的ID,确保了数据的一致性和准确性;同时,该算法灵活性强,可自定义各部分bit位,满足不同业务场景的需求;此外,雪花算法生成ID的速度快,效率高,能有效应对高并发场景,是分布式系统中不可或缺的组件。

前言

分布式ID的基本特性

在分布式系统的复杂环境下,数据量的持续激增对数据库架构提出了新的挑战。

传统的垂直与水平分库分表策略虽然能有效应对数据存储和扩展的问题,但却引出了一个新的需求:需要一个全局唯一标识符(ID)来标定每条数据或消息队列(MQ)中的消息

在这种情况下,依赖数据库自身的ID自增功能显然已经无法满足需求,因此,必须设计出符合以下要求的分布式ID生成策略:

1、全局唯一性,所生成的ID必须在全局范围内是独一无二的,以确保数据的精准标识,避免任何可能的ID冲突。

2、趋势递增,鉴于多数关系型数据库管理系统(RDBMS)MySQL的InnoDB引擎使用B树结构来存储索引数据,并依赖聚集索引进行优化,最好择有序的ID以保证高效的写入性能,因此生成的ID应具备整体递增的趋势。

3、单调递增,在特定场景下,如分布式事务的版本控制、即时通讯的增量消息传递或数据排序等,需要确保每个新生成的ID都严格大于前一个ID,从而实现数据的线性顺序增长。

4、信息安全,针对某些敏感业务,如订单处理等,生成的分布式ID必须是无规则的,以防止外部通过解析ID获取到流量信息或其他商业机密,这就需要ID生成策略在保持唯一性和递增性的同时,还要具备足够的信息隐匿性。

分布式ID常见的算法

在分布ID生成的领域,目前市面上主要流行几种算法,这些算法也是众多开源项目进行优化和实现的基础:

1、UUID方式,UUID由于是在本地生成,因此具有极高的性能,然而,它生成的ID长度较长,达到16字节128位,通常需要使用字符串类型进行存储。

此外,UUID是无序的,这使得它在许多场景中并不适用,尤其是在作为MySQL数据库的主键和索引时。MySQL官方建议主键应尽可能短,而对于InnoDB引擎来说,索引的无序性可能会导致数据位置频繁变动,从而严重影响性能。

2、数据库自增ID方式,这种方法的缺点是每次获取ID都需要进行数据库IO操作,给数据库带来较大压力,且性能较低。如果数据库发生宕机,对依赖其服务的外部系统将是毁灭性的打击。

尽管可以通过部署数据库集群来提高可用性,但问题并未从根本上解决。

3、数据库号段算法,这是对数据库自增ID的一种优化方法。通过每次获取一个号段的值,可以大大减少与数据库的交互次数,从而显著降低数据库的压力。号段越长,性能越高。即使数据库发生宕机,只要号段未用完,系统仍能在短时间内继续提供服务。

美团的Leaf和滴滴的TinyId就是采用这种算法的典型例子。

4、雪花算法,Twitter开源的snowflake算法,以时间戳、机器标识和递增序列为基础生成ID。这种算法生成的ID基本呈趋势递增,且性能很高。然而,由于它强烈依赖于机器时钟,因此需要考虑时钟回拨问题。即当机器上的时间因校正而发生倒退时,可能会导致生成的ID重复。

目前,百度的uid-generator和美团的Leaf也都在使用或优化这种算法。

雪花算法snowflake

snowflake定义

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Snowflake算法的原理相对直观,它负责生成一个64位(long型)的全局唯一ID,这个ID的构成包括:1位无用的符号位、41位的时间戳、10位的机器ID以及12位的序列号,除了固定的1位符号位之外,其余的三个部分都可以根据实际需求进行调整:

1、41位时间戳:这部分能够表示的时间跨度为(1L<<41)/(1000L*3600*24*365),即大约69年。

2、10位机器ID:可以唯一标识最多1024台机器,如果需要对互联网数据中心(IDC)进行划分,可以将这10位拆分为两部分,例如各5位,这样,系统就能够表示最多32个IDC,且每个IDC下可以容纳32台机器。

3、12位自增序列号:用于在同一毫秒内生成多个ID,最多可以表示2^12个不同的ID,因此,理论上,Snowflake算法能够达到的每秒查询率(QPS)约为409.6万。

snowflake优缺点

优点:

  • 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
  • 可以不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也非常高。
  • 可以根据自身业务特性分配bit位,非常灵活。

缺点:

  • 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务处于不可用状态。

Java代码实现snowflake

1、不考虑时间戳回拨


/**
 * Twitter_Snowflake<br>
 * SnowFlake的结构如下(每部分用-分开):<br>
 * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
 * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br>
 * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
 * 得到的值),这里的的开始时间截,一般是的id生成器开始使用的时间,由程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
 * 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br>
 * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br>
 * 加起来刚好64位,为一个Long型。<br>
 * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
 */
public class SnowflakeIdWorkerV1 {

    // ==============================Fields===========================================
    /** 开始时间截 (2025-08-07) */
    private final long twepoch = 1420041600000L;

    /** 机器id所占的位数 */
    private final long workerIdBits = 5L;

    /** 数据标识id所占的位数 */
    private final long datacenterIdBits = 5L;

    /** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

    /** 支持的最大数据标识id,结果是31 */
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

    /** 序列在id中占的位数 */
    private final long sequenceBits = 12L;

    /** 机器ID向左移12位 */
    private final long workerIdShift = sequenceBits;

    /** 数据标识id向左移17位(12+5) */
    private final long datacenterIdShift = sequenceBits + workerIdBits;

    /** 时间截向左移22位(5+5+12) */
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    /** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);

    /** 工作机器ID(0~31) */
    private long workerId;

    /** 数据中心ID(0~31) */
    private long datacenterId;

    /** 毫秒内序列(0~4095) */
    private long sequence = 0L;

    /** 上次生成ID的时间截 */
    private long lastTimestamp = -1L;

    //==============================Constructors=====================================
    /**
     * 构造函数
     * @param workerId 工作ID (0~31)
     * @param datacenterId 数据中心ID (0~31)
     */
    public SnowflakeIdWorkerV1(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    // ==============================Methods==========================================
    /**
     * 获得下一个ID (该方法是线程安全的)
     * @return SnowflakeId
     */
    public synchronized long nextId() {
        long timestamp = timeGen();

        //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(
                    String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        //如果是同一时间生成的,则进行毫秒内序列
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            //毫秒内序列溢出
            if (sequence == 0) {
                //阻塞到下一个毫秒,获得新的时间戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        }
        //时间戳改变,毫秒内序列重置
        else {
            sequence = 0L;
        }

        //上次生成ID的时间截
        lastTimestamp = timestamp;

        //移位并通过或运算拼到一起组成64位的ID
        return ((timestamp - twepoch) << timestampLeftShift) //
                | (datacenterId << datacenterIdShift) //
                | (workerId << workerIdShift) //
                | sequence;
    }

    /**
     * 阻塞到下一个毫秒,直到获得新的时间戳
     * @param lastTimestamp 上次生成ID的时间截
     * @return 当前时间戳
     */
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    /**
     * 返回以毫秒为单位的当前时间
     * @return 当前时间(毫秒)
     */
    protected long timeGen() {
        return System.currentTimeMillis();
    }

    //==============================Test=============================================
    /** 测试 */
    public static void main(String[] args) {
        SnowflakeIdWorkerV1 idWorker = new SnowflakeIdWorkerV1(0, 0);
        for (int i = 0; i < 1000; i++) {
            long id = idWorker.nextId();
            System.out.println(Long.toBinaryString(id));
            System.out.println(id);
        }
    }
}

2、考虑时间戳回拨

/**
 * Twitter_Snowflake<br>
 * SnowFlake的结构如下(每部分用-分开):<br>
 * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
 * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br>
 * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
 * 得到的值),这里的的开始时间截,一般是的id生成器开始使用的时间,由程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T =
 * (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
 * 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br>
 * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br>
 * 加起来刚好64位,为一个Long型。<br>
 * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
 */
public class SnowflakeIdWorkerV2 {

  // ==============================Fields===========================================
  /** 开始时间截 (2025-08-07) */
  private final long twepoch = 1420041600000L;

  /** 机器id所占的位数 */
  private final long workerIdBits = 5L;

  /** 数据标识id所占的位数 */
  private final long datacenterIdBits = 5L;

  /** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
  private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

  /** 支持的最大数据标识id,结果是31 */
  private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

  /** 序列在id中占的位数 */
  private final long sequenceBits = 12L;

  /** 机器ID向左移12位 */
  private final long workerIdShift = sequenceBits;

  /** 数据标识id向左移17位(12+5) */
  private final long datacenterIdShift = sequenceBits + workerIdBits;

  /** 时间截向左移22位(5+5+12) */
  private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

  /** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
  private final long sequenceMask = -1L ^ (-1L << sequenceBits);

  /** 工作机器ID(0~31) */
  private long workerId;

  /** 数据中心ID(0~31) */
  private long datacenterId;

  /** 毫秒内序列(0~4095) */
  private long sequence = 0L;

  /** 上次生成ID的时间截 */
  private long lastTimestamp = -1L;

  private long startTimestamp = -1L;

  // ==============================Constructors=====================================
  /**
   * 构造函数
   *
   * @param workerId 工作ID (0~31)
   * @param datacenterId 数据中心ID (0~31)
   */
  public SnowflakeIdWorkerV2(long workerId, long datacenterId, long startTimestamp) {
    if (workerId > maxWorkerId || workerId < 0) {
      throw new IllegalArgumentException(
          String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
    }
    if (datacenterId > maxDatacenterId || datacenterId < 0) {
      throw new IllegalArgumentException(
          String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
    }
    this.workerId = workerId;
    this.datacenterId = datacenterId;
    this.startTimestamp = startTimestamp;
  }

  // ==============================Methods==========================================
  /**
   * 获得下一个ID (该方法是线程安全的)
   *
   * @return SnowflakeId
   */
  public synchronized long nextId() {
    long sequenceTmp = sequence;
    sequence = (sequence + 1) & sequenceMask;
    if (sequence == 0 && sequenceTmp >= 0) {
      // sequence自增到最大了,时间戳自增1
      startTimestamp += 1;
    }

    // 移位并通过或运算拼到一起组成64位的ID
    return ((startTimestamp - twepoch) << timestampLeftShift) //
        | (datacenterId << datacenterIdShift) //
        | (workerId << workerIdShift) //
        | sequence;
  }

  /**
   * 返回以毫秒为单位的当前时间
   *
   * @return 当前时间(毫秒)
   */
  protected static long timeGen() {
    return System.currentTimeMillis();
  }

  // ==============================Test=============================================
  /** 测试 */
  public static void main(String[] args) {
    SnowflakeIdWorkerV2 idWorker = new SnowflakeIdWorkerV2(0, 0, timeGen());
    for (int i = 0; i < 1000; i++) {
      long id = idWorker.nextId();
      System.out.println(Long.toBinaryString(id));
      System.out.println(id);
    }
  }
}

3、逻辑实现流程

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

组装生成id

生成id的过程,就是把每一种标识(时间、机器、序列号)移到对应位置,然后相加,如下:

long id = (deltaTime << TIMESTAMP_LEFT_SHIFT) | (this.dataCenterId << DATA_CENTER_ID_SHIFT) | (this.workerId << WORKER_ID_SHIFT) | this.sequence;
  • deltaTime向左移22位(IDC-bit+机器bit+序列号bit)。
  • dataCenterId向左移17位(机器bit+序列号bit)。
  • workerId向左移12位(序列号bit)。
  • sequence不用移,中间的|以运算规律就相当于+求和(1 | 1 = 1,1 | 0 = 1,0 | 1 = 1,0 | 0 = 0)

计算最大值的几种方式

代码中分别对每个标识的最大值做了计算:

//序列掩码,用于限定序列最大值为4095 ((2^12)-1) ,从0开始算就有4096个序列
private static final long SEQUENCE_MASK =  -1L ^ (-1L << SEQUENCE_BITS);

//最大支持机器节点数0~31,一共32个  (2^5)-1
private static final long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS);

//最大支持数据中心节点数0~31,一共32个   (2^5)-1
private static final long MAX_DATA_CENTER_ID = -1L ^ (-1L << DATA_CENTER_ID_BITS);

//最大时间戳 2199023255551   (2^41)-1
private static final long MAX_DELTA_TIMESTAMP = -1L ^ (-1L << TIME_STAMP_BITS);

上方的计算就是利用二进制的运算逻辑,拿-1L ^ (-1L << SEQUENCE_BITS)举例:

先看看从哪个方向开始计算:-1L ^ (-1L << 12)-1L和(-1L <<12)做^按位异或运算(1 ^ 1 = 0,1 ^ 0 = 1,0 ^ 1 = 1,0 ^ 0 = 0)

  • -1L的二进制为64个1:1111111111111111111111111111111111111111111111111111111111111111
  • -1L左移12位得到:1111111111111111111111111111111111111111111111111111 000000 000000
  • 最后11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 000000 000000^运算得到0000000000000000000000000000000000000000000000000000 111111 111111(前面有52个0),这就得到序列号的最大值(4095)了,也可以说是掩码。

反解析ID

(1)通过已经生成的ID解析出时间、机器和序列号:

public long getSequence(long id) {
    return id & ~(-1L << SEQUENCE_BITS);
}

public long getWorkerId(long id) {
    return id >> WORKER_ID_SHIFT & ~(-1L << WORKER_ID_BITS);
}

public long getDataCenterId(long id) {
    return id >> DATA_CENTER_ID_SHIFT & ~(-1L << DATA_CENTER_ID_BITS);
}

public long getGenerateDateTime(long id) {
    return (id >> TIMESTAMP_LEFT_SHIFT & ~(-1L << 41L)) + twepoch;
}

因为sequence本身就在低位,所以不需要移动,其他机器和时间都是需要将id向右移动,使得自己的有效位置在低位,至于和自己的最大值做&运算,是为了让不属于自己bit的位置无效,即都转为0。

例如:生成的id为1414362783486840832,转为二进制1001110100000110100110111010100111101010001000001000000000000,想解析出workerIdworkerId有效位为[13, 17],那就将id向右移12位,移到低位得到0000000000001001110100000110100110111010100111101010001000001workerId5-bit,那么除了低位5-bit,其他位置都是无效bit,转为0000000000000100111010000011010011011101010011110101000100000111111&运算得到1(左边都是0可以省掉)。

(2)不过还有一种解析的思路更易于理解,就是运用两次移位运算,把无效位置移除:

1bit-sign + 41bit-time + 5bit-IDC + 5bit-workerId + 12bit-sequence

/**
 * 通过移位解析出sequence,sequence有效位为[0,12]
 * 所以先向左移64-12,然后再像右移64-12,通过两次移位就可以把无效位移除了
 * @param id
 * @return
 */
public long getSequence2(long id) {
    return (id << (TOTAL_BITS - SEQUENCE_BITS)) >>> (TOTAL_BITS - SEQUENCE_BITS);
}
/**
 * 通过移位解析出workerId,workerId有效位为[13,17], 左右两边都有无效位
 * 先向左移 41+5+1,移除掉41bit-时间,5bit-IDC、1bit-sign,
 * 然后右移回去41+5+1+12,从而移除掉12bit-序列号
 * @param id
 * @return
 */
public long getWorkerId2(long id) {
    return (id << (TIME_STAMP_BITS + DATA_CENTER_ID_BITS + SIGN_BITS)) >>> (TIME_STAMP_BITS + DATA_CENTER_ID_BITS + SEQUENCE_BITS + SIGN_BITS);
}
/**
 * 通过移位解析出IDC_ID,dataCenterId有效位为[18,23],左边两边都有无效位
 * 先左移41+1,移除掉41bit-时间和1bit-sign
 * 然后右移回去41+1+5+12,移除掉右边的5bit-workerId和12bit-序列号
 * @param id
 * @return
 */
public long getDataCenterId2(long id) {
    return (id << (TIME_STAMP_BITS + SIGN_BITS)) >>> (TIME_STAMP_BITS + WORKER_ID_BITS + SEQUENCE_BITS + SIGN_BITS);
}
/**
 * 41bit-时间,左边1bit-sign为0,可以忽略,不用左移,所以只需要右移,并加上起始时间twepoch即可。
 * @param id
 * @return
 */
public long getGenerateDateTime2(long id) {
    return (id >>> (DATA_CENTER_ID_BITS + WORKER_ID_BITS + SEQUENCE_BITS)) + twepoch;
}

ID生成器使用方式

在分布式ID生成的策略中,通常采用两种方式:一是基于发号器的方案,二是本地生成方案。

1、发号器方案

该方案将雪花算法ID的生成逻辑封装成一个独立的服务,并部署在多个服务器上,外部系统通过请求发号器服务来获取ID,此方案的优势在于,无需部署大量的服务器,仅需确保服务器数量满足需求即可,默认的1024台已经足够。

但是,由于ID的获取涉及到远程请求,因此会受到网络波动的影响,其性能自然无法与直接从本地生成相比,同时,发号器服务的稳定性至关重要,一旦服务出现故障,将影响到许多依赖其的外部服务。为了确保发号器的高可用性,需要采取多实例部署、异地容灾等策略。

此外,发号器在设计时还可以考虑一次性发布一段时间内的ID供服务本地缓存,这样既能提升性能,减少对发号器的频繁请求,也能在一定程度上降低发号器故障所带来的风险。

2、本地生成方案

在此方案中,ID是在本地直接生成的,因此不存在网络延迟问题,性能极高。但是,为了确保生成的ID具有全局唯一性,需要依赖机器ID来进行区分。因此每台机器以及机器上部署的每个服务都需要分配不同的机器ID。

此外,服务重启后也需要重新分配新的机器ID,这种特性使得机器ID具有“用后即弃”的性质,为了满足对大量机器ID的需求,不得不减少时间戳和序列号的位数,这样做虽然会牺牲一部分ID的丰富性,但却是保证本地生成方案可行性的必要妥协。

运算符相关概念

运算符&

"&"表示按位与操作,它用于操作整数类型的位模式,并将每个操作数中的每一位进行与操作,它会将两个整数的二进制表示进行按位与运算,只有当两个数的相应位都为1时,结果的相应位才为1,否则为0。这种运算符通常用于低级位操作,如设置、清除或翻转二进制位。

int a = 60;  // 60 = 0011 1100  
int b = 13;  // 13 = 0000 1101  
int c = a & b;  // c will be 12 = 0000 1100

运算符|

"|"表示按位或操作,它用于操作整数类型的位模式,并将每个操作数中的每一位进行或操作,它会将两个整数的二进制表示进行按位或运算,只要两个数的相应位中有一个为1,结果的相应位就为1,否则为0。

int a = 60;  // 60 = 0011 1100  
int b = 13;  // 13 = 0000 1101  
int c = a | b;  // c will be 61 = 0011 1101

运算符^

运算符“^”表示按位异或(XOR)操作,它是一个二元运算符,用于对两个整数的二进制位进行异或运算,异或运算的规则是:如果两个相应的二进制位相同,则结果为0;如果两个相应的二进制位不同,则结果为1。

按位异或运算符“^”通常用于处理整数的位级操作,当对两个整数进行异或运算时,它们的每一个二进制位都会根据异或规则进行运算,生成一个新的整数作为结果。

除了整数类型,按位异或运算符也可以应用于布尔类型。在布尔上下文中,运算符“^”表示逻辑异或操作。当应用于两个布尔值时,如果两个值不同,则结果为true;如果两个值相同,则结果为false。

int a = 15; // 二进制表示为 0000 1111  
int b = 8;  // 二进制表示为 0000 1000  
int c = a ^ b; // 进行按位异或运算  
System.out.println("Result: " + c); // 输出结果为 7(二进制表示为 0000 0111)

在这个示例中,整数a和b的二进制表示分别为0000 1111和0000 1000,对它们进行按位异或运算后,得到的结果为0000 0111,即十进制数7。

运算符<<

运算符“<<”表示左移运算,也被称为位移左移运算符或左移位运算符。这个运算符用于将整数的二进制表示向左移动指定的位数。

当你对一个整数应用“<<”运算符并指定一个位移量时,该整数的二进制形式中的所有位都会向左移动指定的位数,在移动过程中,最左边的位会被丢弃,而右边空出的位则用0来填充。

左移运算的效果等同于将原始数值乘以2的位移量次方,这是因为每向左移动一位,就相当于将原始数值乘以2(在二进制中,左移一位等同于将数值翻倍)。例如,如果你有一个整数8(在二进制中表示为1000),将其左移两位,结果将是32(在二进制中表示为100000),这是因为8乘以2的2次方等于32。

int originalValue = 8;  
int shiftedValue = originalValue << 2; // 结果是32

时钟回拨问题及其解决方案探讨

在深入了解时钟回拨问题之前,首先需要探讨为什么机器的本地时钟会出现回拨现象。

由于各种潜在因素,机器的本地时钟可能会变得不准确。为了校准这些时间偏差,网络提供了NTP(网络时间协议)服务。然而,在进行时间校准时,时钟的跳跃或回拨问题可能会随之出现。

雪花算法由于其强烈依赖于机器时钟,因此难以避免受到时钟回拨的影响,这可能导致生成重复的ID。

在原标准实现中,当时钟回拨发生时,系统会直接抛出异常并短暂停止对外服务。然而,这种处理方式在实际生产环境中是不可接受的。因此,需要探索有效的解决方案来最小化时钟回拨带来的影响。以下是两种可能的解决思路:

1、摆脱对机器时钟的依赖:可以定义一个初始时间戳,并在需要时自增该时间戳,而不是跟随机器时钟的增加。那么,何时自增这个时间戳呢?一个合理的策略是,当序列号增加到最大值时,将时间戳加1。这种方法能够充分利用序列号,特别适用于流量较大的场景。然而,在流量较小的情况下,可能会出现时间断层或滞后的现象。

2、优化对机器时钟的依赖:如果仍然选择依赖机器时钟,那么对于小范围的时钟回拨(例如几十毫秒),可以选择等待时钟恢复正常。此外,如果流量不大,可以考虑缓存前几百毫秒或几秒的序列号。当时钟回拨发生时,可以从缓存中获取并自增序列号,从而有效地应对时钟回拨问题。这种方法在保持对机器时钟依赖的同时,降低了时钟回拨对系统的影响。

时间戳自增彻底解决时钟回拨问题

private long sequence = -1L;
private long startTimestamp = 1623947387000L;
private synchronized  long nextId2() {
    long sequenceTmp = sequence;
    sequence = (sequence + 1) & SEQUENCE_MASK;
    // sequence =0 有可能是初始+1=0,也可能是超过了最大值等于0
    // 所以把 初始+1=0排除掉
    if (sequence == 0 && sequenceTmp >= 0) {
        // sequence自增到最大了,时间戳自增1
        startTimestamp += 1;
    }
    // 生成id
    return allocate(startTimestamp - twepoch);
}

这段Java代码定义了一个用于生成唯一ID的方法nextId2(),该方法不依赖于机器时钟,从而彻底解决了时钟回拨问题。代码中包含两个关键变量:sequencestartTimestamp

sequence初始化为-1,这是为了避免浪费sequence+1=0这一序列号。在每次生成ID时,sequence会自增,并通过与SEQUENCE_MASK进行位与操作来确保其在有效范围内循环。当sequence自增到最大值并回绕到0时,如果之前的sequenceTmp(即sequence自增前的值)是非负的,那么说明不是初始情况下的自增,此时会将startTimestamp时间戳自增1,以确保生成的ID的唯一性。

startTimestamp是一个初始时间戳,可以在构造器中指定,也可以使用默认值。该时间戳用于与twepoch(一个预设的常量值,表示时间起点)相减,从而得到一个相对时间差,这个相对时间差与序列号结合使用,通过allocate()方法生成最终的唯一ID。

这种方法的优点在于,它完全摆脱了机器时钟的限制,因此不会受到时钟回拨问题的影响。

同时,由于时间戳的自增是由程序自己控制的,因此可以充分利用未来时间,预先生成一些ID并存储在缓存中。

这样,外部系统可以直接从缓存中获取ID,而无需等待ID的生成过程,从而大大提高了性能。

此外,该方法还确保了每一毫秒内可以生成多达4096个序列号(范围从0到4095),且没有浪费。

*然而,这种方法也存在一个明显的缺点,即生成的ID中的时间戳并不是真实的时间。*如果系统的流量较小,那么时间戳可能会滞后很多。

因此,如果对从ID中解析出来的时间戳有特定的利用需求(例如用于记录事件发生的时间),那么这个缺点可能会成为问题。但如果时间戳的利用意义不大,那么这个缺点也就可以忽略不计了。

缓存历史序列号缓解时钟回拨问题

// 记录近2S的毫秒数的sequence的缓存
private int LENGTH = 2000;
// sequence缓存
private long[] sequenceCycle = new long[LENGTH];

private synchronized long nextId() throws Exception {
    long timestamp = timeGen();
    int index = (int)(timestamp % LENGTH);
    // 1、出现时钟回拨问题,获取历史序列号自增
    if (timestamp < lastTimestamp) {
        long sequence = 0;
        do {
           if ((lastTimestamp - timestamp) > LENGTH) {
               // 可自定义异常、告警等,短暂不能对外提供,故障转移,将请求转发到正常机器。
               throw new UnsupportedOperationException("The timeback range is too large and exceeds 2000ms caches");
            }
            long preSequence = sequenceCycle[index];
            sequence = (preSequence + 1) & SEQUENCE_MASK;
            if (sequence == 0) {
                // 如果取出的历史序列号+1后已经达到超过最大值,
                // 则重新获取timestamp,重新拿其他位置的缓存
                timestamp = tilNextMillis(lastTimestamp);
                index = (int)(timestamp % LENGTH);
            } else {
            	// 更新缓存
                sequenceCycle[index] = this.sequence;            
                return allocate((timestamp - this.twepoch), sequence);
            }
        } while (timestamp < lastTimestamp);
        // 如果在获取缓存的过程中timestamp恢复正常了,就走正常流程
    }
    // 2、时间等于lastTimestamp,取当前的sequence + 1
    if (timestamp == lastTimestamp) {
        sequence = (sequence + 1) & SEQUENCE_MASK;
        // Exceed the max sequence, we wait the next second to generate id
        if (sequence == 0) {
            timestamp = tilNextMillis(lastTimestamp);
            index = (int)(timestamp % LENGTH);
        }
    } else {
        // 3、时间大于lastTimestamp没有发生回拨, sequence 从0开始
        this.sequence = 0L;
    }
    // 缓存sequence + 更新lastTimestamp
    sequenceCycle[index] = this.sequence;
    lastTimestamp = timestamp;
    // 生成id
    return allocate(timestamp - this.twepoch);
}

这里缓存了2000ms的序列号,如果发生时钟回拨,且回拨范围在2000ms内,就从缓存中取序列号自增,超过2000ms回拨,就抛异常,故障转移,将请求分配到正常机器。

  1. 若获取的历史sequence+1之后超过了最大值,则重新获取时间戳,重新获取缓存sequence。
  2. 极端情况下,获取很多次缓存sequence+1都超过了最大值,就会一直循环获取,这样可能会影响性能,所以实际生产中可以限定重新获取次数。
  3. 在这个重新获取的过程中,时钟可能恢复正常了,则此时也要退出循环,走正常流程。

等待时钟校正

private synchronized  long nextId3() {
    long timestamp = timeGen();
    // 1、出现时钟回拨问题,如果回拨幅度不大,等待时钟自己校正
    if (timestamp < lastTimestamp) {
        int sleepCntMax = 2;
        int sleepCnt = 0;
        do {
            long sleepTime = lastTimestamp - timestamp;
            if (sleepCnt > sleepCntMax) {
                // 可自定义异常类
                throw new UnsupportedOperationException(String.format("Clock moved backwards. Refusing for %d seconds", sleepTime));
            }
            if (sleepTime <= 500) {
                try {
                    Thread.sleep(sleepTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    sleepCnt++;
                    timestamp = tilNextMillis(lastTimestamp);
                }
            } else {
                // 可自定义异常类
                throw new UnsupportedOperationException(String.format("Clock moved backwards. Refusing for %d seconds", sleepTime));
            }
        } while (timestamp < lastTimestamp);
    }
    // 2、时间等于lastTimestamp,取当前的sequence + 1
    if (timestamp == lastTimestamp) {
        sequence = (sequence + 1) & SEQUENCE_MASK;
        // Exceed the max sequence, we wait the next second to generate id
        if (sequence == 0) {
            timestamp = tilNextMillis(lastTimestamp);
        }
    } else {
        // 3、时间大于lastTimestamp没有发生回拨, sequence 从0开始
        this.sequence = 0L;
    }
    lastTimestamp = timestamp;
    // 生成id
    return allocate(timestamp - this.twepoch);
}

等待时钟自己校正来解决时钟回拨问题,适用于回拨幅度小的场景。

比如回拨时长小于500ms,那就睡眠500ms,等时间恢复到正常,如果这个过程中又发生了时钟回拨,不可能一直等它校正,实际生产中可限定校正的次数,超过最大校正次数,那就抛异常吧,这属于极端情况。

小结

解决时钟回拨问题的方法还有很多,无非就是避免和缓解。

每种方式有各自的特点和适用场景,可以两两结合使用,比如时钟回拨幅度小,就休眠校正,回拨幅度大或者出现多次回拨,也不抛异常,获取缓存sequence对外提供服务,也可以当发生时钟回拨时,用备用机器id生成ID等。

要点总结

1、在分布式系统中生成全局唯一的ID至关重要,目前,业界广泛采用的方案包括数据库号段算法和雪花算法,这些算法不仅在大厂中得到了广泛应用,还催生了诸多开源项目,例如百度的uid-generator、美团的Leaf以及滴滴的TinyId等。

2、雪花算法以其简单高效而著称,其核心在于结合时间戳、机器ID和序列号来生成64位的唯一ID,该算法生成的ID整体上呈递增趋势,确保了全局唯一性,同时性能表现优异。雪花算法提供了高度的灵活性,允许根据实际需求自定义各个组成部分的bit位数,若需提升QPS(每秒查询率),可适当增加序列号所占的bit位数。

3、尽管雪花算法具有诸多优点,但它对机器时钟的强依赖性也带来了时钟回拨问题,为应对这一问题,开发者们提出了多种解决方案,主要分为避免和缓解两大类。*常见的方法包括使时间戳自增以摆脱对机器时钟的依赖、利用缓存来存储序列号,或等待时钟自行校正等。*每种方法都有其独特之处,只有正确利用它们的优点,才能最大限度地提升系统性能。

4、雪花算法ID生成器的使用方式灵活多样,其中远程发号器和本地直接生成ID是两种主要方式。*远程发号器需要确保高可用性,以满足分布式系统中的需求;而本地直接生成ID则省去了远程请求的开销,因此在性能上更具优势。*然而,本地生成方式也带来了机器ID管理的挑战,因为一旦某个机器ID被使用,它就不能再被其他机器重复使用。为了应对这一挑战,开发者们通常利用MySQL或ZooKeeper等工具来进行机器ID的管理和分配。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

END!
END!
END!

往期回顾

精品文章

深度思考:总结SOA、WSDL、SOAP、REST、UDDI之间的关系

Spring揭秘:@import注解应用场景及实现原理!

Java并发基础:原子类之AtomicMarkableReference全面解析!

Java并发基础:concurrent Flow API全面解析

Java并发基础:CopyOnWriteArraySet全面解析

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员古德

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值
12388是什么电话 抖是什么意思 脚心痒是什么预兆 云南白药里面的保险子有什么用 椰子煲鸡汤放什么材料
绿豆汤是什么颜色 减肥可以吃什么零食 笑面虎什么意思 飞蚊症是什么原因造成的能治愈吗 pde是什么意思
霸天虎和威震天是什么关系 飞蚊症用什么药 手抓饼里面夹什么好吃 孟母三迁告诉我们什么道理 餐后血糖高是什么原因
一个月一个办念什么 解酒喝什么最好 梦见自己鞋子破了是什么意思 图谋不轨什么意思 吊孝是什么意思
注会什么时候考试hcv8jop7ns8r.cn 做什么生意挣钱hcv8jop3ns5r.cn 脸部肌肉跳动是什么原因hlguo.com 人体缺硒会有什么症状hcv9jop0ns8r.cn 一级军士长什么待遇hcv7jop9ns4r.cn
咳嗽能吃什么水果beikeqingting.com 金酒是什么酒hcv8jop3ns3r.cn 脖子淋巴结肿大是什么原因hcv7jop6ns8r.cn dh什么意思hcv7jop9ns0r.cn 胎儿左心室点状强回声是什么意思hcv9jop4ns1r.cn
白化病是什么原因引起的hcv9jop0ns5r.cn 属猪和什么属相相克hcv9jop2ns5r.cn 今年25岁属什么生肖sscsqa.com 什么情况不能献血hcv9jop4ns9r.cn 为什么突然得了荨麻疹hcv7jop7ns4r.cn
突然头晕目眩是什么原因hcv9jop6ns9r.cn ig什么意思hcv8jop2ns2r.cn 拉肚子可以吃什么hcv8jop0ns8r.cn ai是什么元素hcv9jop7ns9r.cn 猪吃什么hcv9jop5ns3r.cn
百度 技术支持:克隆侠蜘蛛池 www.kelongchi.com