日照是什么海| 野兽之王是什么动物| 毛是什么意思| 玄猫是什么猫| 什么叫化疗| met什么意思| 九月二十号是什么星座| 海尔兄弟叫什么| ggdb是什么牌子| 开车穿什么鞋最好| 游泳为什么要穿泳衣| 吃了避孕药后几天出血是什么原因| 玉米属于什么类| 有代沟是什么意思| 小什么名字好听| 小孩便秘吃什么药| 吃什么 长高| nos是什么单位| cor是什么意思| 梦见长豆角是什么意思| 石敢当是什么意思| 泛醇是什么| 右肩膀和胳膊疼痛是什么原因| 稍高回声是什么意思| 厅级干部是什么级别| 高山茶属于什么茶| 小猫不能吃什么食物| 红楼梦是一部什么小说| 吃茶叶蛋有什么好处和坏处| 苹果什么季节成熟| 白酒泡什么补肾壮阳最好| 什么条件| 倒立对身体有什么好处| 开黄腔是什么意思| 孕妇吸氧对胎儿有什么好处| kohler是什么品牌| 泛是什么意思| 收割是什么意思| 6月6日什么星座| 脸皮最厚是什么生肖| 尿红细胞阳性什么意思| g50是什么高速| 脑血栓什么症状| 夏天喝什么解暑| flour什么意思| 蝉什么时候出现| 12什么意思| b型血的人是什么性格| 相什么并什么| 薄荷音是什么意思| 夏天为什么会下冰雹| 酒曲是什么| 女生的隐私部位长什么样| 玥字属于五行属什么| 皮神经炎是什么症状| 螺蛳粉有什么危害| 魔芋是什么东西做的| 屎壳郎吃什么| 人的五官是什么| 血脂高什么意思| 头疼是什么原因| 界定是什么意思| 嫡庶是什么意思| 宫颈多发潴留囊肿是什么意思| 免疫系统由什么组成| 阴虱病是什么原因引起的| 鹤立鸡群代表什么生肖| 社康是什么意思| 吃了紧急避孕药会有什么反应| 八七年属兔的是什么命| 迅雷不及掩耳之势是什么意思| 脸部痒是什么原因| 10月21是什么星座| 又字加一笔是什么字| 花痴是什么意思| 欧舒丹属于什么档次| 男性雄激素低吃什么药| 2007属什么| 子宫破裂有什么危险| 脂蛋白a高是什么原因| 新车上牌需要什么资料| 肺阴不足的症状是什么| 身不由己是什么生肖| 什么是鸡胸病症状图片| 岁月如梭是什么意思| 什么的灵魂| 国家能源局是什么级别| 壮阳吃什么| 性质是什么| 知恩图报是什么意思| 2月7号什么星座| 什么是福报| 放射科检查什么| 脑炎是什么症状| 世家是什么意思| 儿保是什么| 甲钴胺是什么药| 什么是卒中| 极乐是什么意思| 牵牛花什么时候开花| 吃葱有什么好处和坏处| 百折不挠的意思是什么| 什么药能治口臭| 尿酸高有什么症状| 回光返照什么意思| 什么是央企| fredperry是什么牌子| 脂肪肝吃什么药治疗| 中性粒细胞偏低是什么意思| 鲁迅字什么| 宫颈口在什么位置| 什么入伏| 什么时候断奶最合适| 左下腹是什么器官| 狼烟是什么意思| 鸽子炖什么补气血| 人死后为什么要盖住脸| 尿酸高能吃什么鱼| 嘉字五行属什么| 怀疑心梗做什么检查| 什么是团队| 口炎是什么字| 锄禾是什么意思| 七月初七是什么节日| 补铁的药什么时候吃最好| 红丝带的含义是什么| 舌苔很白是什么原因| 什么的花灯| 护肝养肝吃什么药| 脚板心发热是什么原因| 早晨嘴苦是什么原因引起的| 正月二十是什么星座| 吃皮是什么意思| 小猫打什么疫苗| 那的反义词是什么| 蒜苔炒什么好吃| 垂体催乳素高是什么原因| 一什么狮子| 头好出汗是什么原因| 伊始什么意思| 女性阳性是什么病| 什么的松脂| 什么气味能驱赶猫| 莫西沙星片主治什么病| 长脸适合什么刘海| 自缢死亡是什么意思| 骨穿是检查什么病| 冬天种什么蔬菜合适| 希腊人是什么人种| 滴虫是什么| 2007年属什么| 松鼠代表什么生肖| 上火耳鸣吃什么药最好| 肋骨下面是什么部位| 苟且是什么意思| 多愁善感的动物是什么生肖| 9月是什么季节| 什么是肝硬化| 印记是什么意思| 做梦被打了是什么意思| 公安和警察有什么区别| 剪短发什么发型好看| 睾丸痒是什么原因| 苹果a1660是什么型号| 怀孕第一个月有什么症状| 企鹅是什么意思| 什么的大娘| 什么人不适合做纹绣师| 老人脚背肿是什么原因| 感谢老师送什么花| 内膜薄吃什么补得最快| 排酸是什么意思| 补充蛋白质吃什么食物| 人妖是什么| 小孩吃指甲是什么原因造成的| 口腔苦味是什么原因| 右眼一直跳是因为什么原因| 孕妇梦见老公出轨是什么意思| 54年属什么| 不全骨折是什么意思| 满月回娘家有什么讲究| 为什么会长疣| 肺脓肿是什么病严重吗| 婴儿外阴粘连挂什么科| x片和ct有什么区别| 细胞学检查是什么| 什么是雷达| 白细胞2个加号是什么意思| 什么叫环比什么叫同比| 一什么泪珠| 女性尿道感染吃什么药| 梦见四条蛇是什么意思| 青色是什么颜色| 58年属什么| 甘油三酯高吃什么药好| 什么是梅雨季节| 球蛋白是什么意思| 过命之交是什么意思| 四次元是什么意思| 霉点用什么可以洗掉| 左侧脖子疼是什么原因| ppe是什么| 喝大麦茶有什么好处| 么么哒是什么意思| 弘字五行属什么| 一抹是什么意思| 十八反是什么意思| 草长莺飞是什么生肖| 仇在姓氏中读什么| 属龙的和什么属相最配| hpa是什么单位| 荔枝不能跟什么一起吃| 课代表是什么意思| 一什么斑点| 左侧卵巢囊性包块是什么意思| 终板炎是什么病| 怀孕分泌物是什么样的| 三伏是什么时候| 老年人吃饭老是噎着是什么原因| 腐生是什么意思| 检查贫血做什么检查| 实蛋是什么| 气血不足吃什么东西| 什么是穿刺| 衍生物是什么意思| 什么病不能吃山药| 1961年属什么生肖| 脚趾抽筋是什么原因引起的| 胃痛吃什么药效果最好| 胆囊病变是什么意思| 物心念什么| 中国现在是什么社会| 开黑是什么意思| 文爱 什么意思| 禳是什么意思| 什么人不能吃黄精| 孤芳不自赏什么意思| 勿误是什么意思| 葵水是什么意思| 什么快递可以寄宠物| 左眼皮肿是什么原因引起的| 尿蛋白高有什么危害| 高丽参适合什么人吃| 眼睛为什么会长麦粒肿| 456什么意思| 心脏ct能检查出什么| 频次是什么意思| 胶囊是什么原料做的| opec是什么意思| 最里面的牙齿叫什么牙| 今年农历是什么年号| 暗送秋波是什么意思| 补体c3偏高说明什么| 胆汁反流什么症状| 腺样体肥大吃什么药| 大便的颜色代表什么| 阳痿吃什么好| 胃窦糜烂是什么意思| 白无常叫什么名字| 月经来了不走是什么原因| ldh是什么| 灰白组织是什么意思| 什么3121919Z空间| 早醒是什么原因造成的| 百度

补肾最好的药是什么药

本文深入探讨了雪花算法的工作原理及其实现细节,包括ID生成、最大值计算、反解析等,并针对时钟回拨问题提出了多种解决方案。

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

一、前言

在日趋复杂的分布式系统中,数据量越来越大,数据库分库分表是一贯的垂直水平做法,但是需要一个全局唯一ID标识一条数据或者MQ消息,数据库id自增就显然不能满足要求了。因为场景不同,分布式ID需要满足以下几个条件:

  1. 全局唯一性,不能出现重复的ID。
  2. 趋势递增,在MySQL InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用B-tree的数据结构来存储索引数据,在主键的选择上应该尽量使用有序的主键保证写入性能。
  3. 单调递增,保证下一个ID一定大于上一个ID。例如分布式事务版本号、IM增量消息、排序等特殊需求。
  4. 信息安全,对于特殊业务,如订单等,分布式ID生成应该是无规则的,不能从ID上反解析出流量等敏感信息。

市面上对分布式ID生成大致有几种算法(一些开源项目都是围着这几种算法进行实现和优化):

  1. UUID:因为是本地生成,性能极高,但是生成的ID太长,16字节128位,通常需要字符串类型存储,且无序,所以很多场景不适用,也不适用于作为MySQL数据库的主键和索引(MySql官方建议,主键越短越好;对于InnoDB引擎,索引的无序性可能会引起数据位置频繁变动,严重影响性能)。
  2. 数据库自增ID:每次获取ID都需要DB的IO操作,DB压力大,性能低。数据库宕机对外依赖服务就是毁灭性打击,不过可以部署数据库集群保证高可用。
  3. 数据库号段算法:对数据库自增ID的优化,每次获取一个号段的值。用完之后再去数据库获取新的号段,可以大大减轻数据库的压力。号段越长,性能越高,同时如果数据库宕机,号段没有用完,短时间还可以对外提供服务。(美团的Leaf滴滴的TinyId
  4. 雪花算法:Twitter开源的snowflake,以时间戳+机器+递增序列组成,基本趋势递增,且性能很高,因为强依赖机器时钟,所以需要考虑时钟回拨问题,即机器上的时间可能因为校正出现倒退,导致生成的ID重复。(百度的uid-generator美团的Leaf

雪花算法和数据库号段算法用的最多,本篇主要对雪花算法原理剖析和解决时钟回拨问题讨论。

二、雪花算法snowflake

1、基本定义

全网都在用这个图-yyds

snowflake原理其实很简单,生成一个64bit(long)的全局唯一ID,标准元素以1bit无用符号位+41bit时间戳+10bit机器ID+12bit序列化组成,其中除1bit符号位不可调整外,其他三个标识的bit都可以根据实际情况调整:

  1. 41bit-时间可以表示(1L<<41)/(1000L360024*365)=69年的时间。
  2. 10bit-机器可以表示1024台机器。如果对IDC划分有需求,还可以将10-bit分5-bit给IDC,分5-bit给工作机器。这样就可以表示32个IDC,每个IDC下可以有32台机器。
  3. 12个自增序列号可以表示2^12个ID,理论上snowflake方案的QPS约为409.6w/s。

注:都是从0开始计数。

2、snowflake的优缺点

优点:

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

缺点:

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

三、Java代码实现snowflake

如下示例,41bit给时间戳,5bit给IDC,5bit给工作机器,12bit给序列号,代码中是写死的,如果某些bit需要动态调整,可在成员属性定义。计算过程需要一些位运算基础。

public class SnowflakeIdGenerator {

    public static final int TOTAL_BITS = 1 << 6;

    private static final long SIGN_BITS = 1;

    private static final long TIME_STAMP_BITS = 41L;

    private static final long DATA_CENTER_ID_BITS = 5L;

    private static final long WORKER_ID_BITS = 5L;

    private static final long SEQUENCE_BITS = 12L;

    /**
     * 时间向左位移位数 22位
     */
    private static final long TIMESTAMP_LEFT_SHIFT = WORKER_ID_BITS + DATA_CENTER_ID_BITS + SEQUENCE_BITS;

    /**
     * IDC向左位移位数 17位
     */
    private static final long DATA_CENTER_ID_SHIFT = WORKER_ID_BITS + SEQUENCE_BITS;

    /**
     * 机器ID 向左位移位数 12位
     */
    private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;

    /**
     * 序列掩码,用于限定序列最大值为4095
     */
    private static final long SEQUENCE_MASK =  -1L ^ (-1L << SEQUENCE_BITS);

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

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

    /**
     * Customer epoch
     */
    private final long twepoch;

    private final long workerId;

    private final long dataCenterId;

    private long sequence = 0L;

    private long lastTimestamp = -1L;

    /**
     *
     * @param workerId 机器ID
     * @param dataCenterId  IDC ID
     */
    public SnowflakeIdGenerator(long workerId, long dataCenterId) {
        this(workerId, dataCenterId, null);
    }

    /**
     *
     * @param workerId  机器ID
     * @param dataCenterId IDC ID
     * @param epochDate 初始化时间起点
     */
    public SnowflakeIdGenerator(long workerId, long dataCenterId, Date epochDate) {
        if (workerId > MAX_WORKER_ID || workerId < 0) {
            throw new IllegalArgumentException("worker Id can't be greater than "+ MAX_WORKER_ID + " or less than 0");
        }
        if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {
            throw new IllegalArgumentException("datacenter Id can't be greater than {" + MAX_DATA_CENTER_ID + "} or less than 0");
        }

        this.workerId = workerId;
        this.dataCenterId = dataCenterId;
        if (epochDate != null) {
            this.twepoch = epochDate.getTime();
        } else {
            //2025-08-06
            this.twepoch = 1286726400000L;
        }

    }

    public long genID() throws Exception {
        try {
            return nextId();
        } catch (Exception e) {
            throw e;
        }
    }

    public long getLastTimestamp() {
        return lastTimestamp;
    }

    /**
     * 通过移位解析出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;
    }

    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;
    }

    private synchronized long nextId() throws Exception {
        long timestamp = timeGen();
        // 1、出现时钟回拨问题,直接抛异常
        if (timestamp < lastTimestamp) {
            long refusedTimes = lastTimestamp - timestamp;
            // 可自定义异常类
            throw new UnsupportedOperationException(String.format("Clock moved backwards. Refusing for %d seconds", refusedTimes));
        }
        // 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;

        return allocate(timestamp - this.twepoch);
    }

    private long allocate(long deltaSeconds) {
        return (deltaSeconds << TIMESTAMP_LEFT_SHIFT) | (this.dataCenterId << DATA_CENTER_ID_SHIFT) | (this.workerId << WORKER_ID_SHIFT) | this.sequence;
    }

    private long timeGen() {
        long currentTimestamp = System.currentTimeMillis();
        // 时间戳超出最大值
        if (currentTimestamp - twepoch > MAX_DELTA_TIMESTAMP) {
            throw new UnsupportedOperationException("Timestamp bits is exhausted. Refusing ID generate. Now: " + currentTimestamp);
        }
        return currentTimestamp;
    }

    private long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    /**
     * 测试
     * @param args
     */
    public static void main(String[] args) throws Exception {
        SnowflakeIdGenerator snowflakeIdGenerator = new SnowflakeIdGenerator(1,2);
        long id = snowflakeIdGenerator.genID();

        System.out.println("ID=" + id + ", lastTimestamp=" + snowflakeIdGenerator.getLastTimestamp());
        System.out.println("ID二进制:" + Long.toBinaryString(id));
        System.out.println("解析ID:");
        System.out.println("Sequence=" + snowflakeIdGenerator.getSequence(id));
        System.out.println("WorkerId=" + snowflakeIdGenerator.getWorkerId(id));
        System.out.println("DataCenterId=" + snowflakeIdGenerator.getDataCenterId(id));
        System.out.println("GenerateDateTime=" + snowflakeIdGenerator.getGenerateDateTime(id));

        System.out.println("Sequence2=" + snowflakeIdGenerator.getSequence2(id));
        System.out.println("WorkerId2=" + snowflakeIdGenerator.getWorkerId2(id));
        System.out.println("DataCenterId2=" + snowflakeIdGenerator.getDataCenterId2(id));
        System.out.println("GenerateDateTime2=" + snowflakeIdGenerator.getGenerateDateTime2(id));
    }

}

雪花算法ID生成传统流程

1、组装生成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)。

2、计算最大值的几种方式

(1)注意到代码中分别对每个标识的最大值做了计算:

//序列掩码,用于限定序列最大值为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)了,也可以说是掩码。

Java运算符优先级列表

(2)其实有一种更容易理解的计算最大值的方式,比如计算12bit-序列号的最大值,那就是(2^12 -1)呀,但是位运算性能更高,用位运算的方式就是((1 << 12) -1)。1左移12位得到1 0000 0000 0000,减1也是可以得到1111 1111 1111,即4095。

(3)还看到一种计算最大值的方式,继续拿12bit-序列号举例,~(-1L << 12)~不知道怎么计算那就傻了:

-1L先向左移12位得到1111111111111111111111111111111111111111111111111111 000000 000000,然后进行~按位非运算(~ 1 = -2,~ 0 = -1 ,~n = - ( n+1 )),也可以理解为反转,1转为0,0转为1,然后也可以得到0000000000000000000000000000000000000000000000000000 111111 111111

3、反解析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位,移到低位得到0000000000001001110100000110100110111010100111101010001000001workerId有5-bit,那么除了低位5-bit,其他位置都是无效bit,转为0。000000000000100111010000011010011011101010011110101000100000111111&运算得到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;
}

4、ID生成器使用方式

主要有两种方式,一种是发号器,一种是本地生成:

  • 发号器,就是把雪花算法ID生成封装成一个服务,部署在多台机器上,由外界请求发号器服务获取ID。这样做的好处,是机器不需要那么多,1024台完全足够了,相对ID的时间戳和序列号的bit就可以调大一些。但是因为需要远程请求获取ID,所以会受到网络波动的影响,性能上肯定是没有直接从本地生成获取高的,同时发号器一旦挂了,很多服务就不能对外提供服务了,所以发号器服务需要高可用,多实例,异地部署和容灾,发号器在发号的时候,也可以发布一段时间的ID,服务本地缓存起来,这样不仅提高性能,不需要每次都去请求发号器,也在一定程度上缓解了发号器故障带来的影响。
  • 本地生成ID,没有网络延迟,性能极高。只能通过机器id来保证生成的ID唯一性,所以需要提供足够多的机器id,每台机器可能部署多个服务,每个服务可能部署在多台机器,都需要分配不同的机器id,并且服务重启了也需要重新分配机器id。这样机器id就有了用后即毁的特点。需要足够多的机器id,就必须缩减时间bit和序列号bit。

可以利用MySql或者zk进行机器id的分配和管理。

四、时钟回拨问题和解决方案讨论

首先看看时钟为什么会发生回拨?机器本地时钟可能会因为各种原因发生不准的情况,网络中提供了NTP服务来做时间校准,做校准的时候就会发生时钟的跳跃或者回拨的问题。

因为雪花算法强依赖机器时钟,所以难以避免受到时钟回拨的影响,有可能产生ID重复。原标准实现代码中是直接抛异常,短暂停止对外服务,这样在实际生产中是无法忍受的。所以要尽量避免时钟回拨带来的影响,解决思路有两个:

  • 不依赖机器时钟驱动,就没时钟回拨的事儿了。即定义一个初始时间戳,在初始时间戳上自增,不跟随机器时钟增加。时间戳何时自增?当序列号增加到最大时,此时时间戳+1,这样完全不会浪费序列号,适合流量较大的场景,如果流量较小,可能出现时间断层滞后。
  • 依然依赖机器时钟,如果时钟回拨范围较小,如几十毫秒,可以等到时间回到正常;如果流量不大,前几百毫秒或者几秒的序列号肯定有剩余,可以将前几百毫秒或者几秒的序列号缓存起来,如果发生时钟回拨,就从缓存中获取序列号自增。

(时钟回拨问题,可通过手动调整电脑上的时钟进行模拟测试。)

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

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);
}

起始时间可以构造器里指定,也可以用默认的,而sequence初始为-1,是为了不想浪费sequence+1=0这一序列号。

sequence = 0排除掉初始sequence=-1 +1 = 0的情况就是sequence超过最大值了,此时时间戳startTimestamp自增。

代码和思路都很简单,就是完全脱离机器时钟,彻底解决了时钟回拨问题。显而易见的优点,每一毫秒4096个序列号([0,4095])没有浪费,同时因为时间自增由程序自己掌控,所以可以利用未来时间,预先生成一些ID放在缓存里,外界从缓存中直接获取ID,快消费完了再生产,这样就形成了永动的生产-消费者模式,获取ID省去了生成的过程,性能也会大大提升。

但是时间戳完全自控,也有很明显的缺点,ID生成的时间,并不是真实的时间,如果流量较小,时间可能会滞后很多。如果对从ID解析出来的时间戳没有什么利用意义,这个缺点也不需要关心。

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

// 记录近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回拨,就抛异常,故障转移,将请求分配到正常机器。

  • 若获取的历史sequence+1之后超过了最大值,则重新获取时间戳,重新获取缓存sequence
  • 极端情况下,获取很多次缓存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+序列号生成64bit的ID,整体趋势递增,且全局唯一,性能也不错。每种组成标识的bit都可以自定义,灵活性很高,如果需要更高的QPS,可以相对的把序列号bit调大一些。
  3. 因为雪花算法强依赖机器时钟,就难以避免时钟回拨问题,解决的方式很多,无非从避免和缓解两个角度出发,常用的方式有,时间戳自增脱离机器时钟依赖,利用缓存序列号,或者等待时钟校正等,各有各的特点,正确利用其优点,才能最大提高性能。
  4. 雪花算法ID生成器的使用方式有两种,一种是远程发号器,需要做到高可用。另一种就是直接本地生成ID,省去了远程请求过程,性能自然也是比远程发号器高的,但是机器id用后即毁,需要分配足够多的机器id。机器id的管理和分配可以利用MySql或者ZK

参考:

如若文章有错误理解,欢迎批评指正,同时非常期待你的评论、点赞和收藏。

评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

徐同学呀

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

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

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

打赏作者

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

抵扣说明:

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

余额充值
什么是牙周炎 蟋蟀喜欢吃什么 感冒拉肚子吃什么药 朱砂痣代表什么 手淫是什么意思
四离日是什么意思 大姨妈可以吃什么水果 肾有问题有什么症状 老公的妈妈叫什么 217是什么意思
布灵布灵是什么意思 吃什么降三高最快 代偿期和失代偿期是什么意思 为什么有眼袋是什么原因引起的 amiri是什么牌子
吃糖醋蒜有什么好处和坏处 羊字五行属什么 血糖高吃什么降得快 jeans是什么意思 121是什么意思
蓝色配什么色好看hcv8jop5ns2r.cn 宫颈炎和阴道炎有什么区别hcv7jop9ns9r.cn 淋巴结发炎挂什么科1949doufunao.com 检查耳朵挂什么科hcv8jop8ns4r.cn 三文鱼不能和什么一起吃hcv8jop8ns3r.cn
什么是避孕套hcv9jop0ns3r.cn c1是什么意思hcv8jop5ns2r.cn ivy什么意思hcv8jop9ns1r.cn 夏天摆摊适合卖什么hcv8jop4ns1r.cn 八段锦什么时候练最好gysmod.com
子宫内膜炎什么症状hcv9jop6ns0r.cn 上面一个日下面一个立是什么字hcv7jop6ns7r.cn 缺铁性贫血吃什么好hcv9jop6ns5r.cn invent是什么意思hcv9jop6ns4r.cn 儿茶酚胺是什么gangsutong.com
微光是什么意思hcv8jop2ns5r.cn 脱落细胞学检查是什么hcv9jop3ns8r.cn 高血压吃什么水果hcv9jop2ns1r.cn 怀孕生化了是什么原因hcv9jop0ns0r.cn 茶学专业学什么hcv7jop4ns6r.cn
百度