zoukankan      html  css  js  c++  java
  • 分布式 ID 生成算法 — SnowFlake

    一、概述

    分布式 ID 生成算法的有很多种,Twitter 的 SnowFlake 就是其中经典的一种。

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

    • 1 位,不用。二进制中最高位为 1 的都是负数,但是我们生成的 id 一般都使用整数,所以这个最高位固定是 0。
    • 41 位,用来记录时间戳(毫秒)。41 位可以表示 2^41 个数字;如果只用来表示正整数(计算机中正数包含 0),可以表示的数值范围是:0 至 2^41−1,也就是说 41 位可以表示 2^41 个毫秒的值,转化成单位年则是 2^41 / (1000 * 60 * 60 * 24 * 365) = 69年。
    • 10 位,用来记录工作机器 id,可以部署在 2^10 = 1024个节点,包括 5 位 datacenterId 和 5 位 workerId ;5 位(bit)可以表示的最大正整数是2^5-1 = 31,即可以用 0、1、2、3、....31 这 32 个数字,来表示不同的 datecenterId 或 workerId。
    • 12 位,序列号,用来记录同毫秒内产生的不同 id。12位(bit)可以表示的最大正整数是 2^12-1 =4095,即可以用 0、1、2、3、....4095 这 4096 个数字,来表示同一机器同一时间截(毫秒)内产生的 4096 个ID序号。

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

    SnowFlake 可以保证:

    • 所有生成的 ID 按时间趋势递增;
    • 整个分布式系统内不会产生重复id(因为有 datacenterId 和 workerId 来做区分);

    二、SnowFlake 算法的 JAVA 实现

    public class SnowFlake {
    
        /**
         * 起始的时间戳
         */
        private final static long START_STMP = 1480166465631L;
    
        /**
         * 每一部分占用的位数
         */
        private final static long SEQUENCE_BIT = 12; //序列号占用的位数
        private final static long MACHINE_BIT = 5;   //机器标识占用的位数
        private final static long DATACENTER_BIT = 5;//数据中心占用的位数
    
        /**
         * 每一部分的最大值
         */
        private final static long MAX_DATACENTER_NUM = ~(-1L << DATACENTER_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 DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
        private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;
    
        private long datacenterId;  //数据中心
        private long machineId;     //机器标识
        private long sequence = 0L; //序列号
        private long lastStmp = -1L;//上一次时间戳
    
        public SnowFlake(long datacenterId, long machineId) {
            if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
                throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_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
         *
         * @return
         */
        public synchronized long nextId() {
            long currStmp = getNewstmp();
            if (currStmp < lastStmp) {
                throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
            }
    
            if (currStmp == lastStmp) {
                //相同毫秒内,序列号自增
                sequence = (sequence + 1) & MAX_SEQUENCE;
                //同一毫秒的序列数已经达到最大
                if (sequence == 0L) {
                    currStmp = getNextMill();
                }
            } else {
                //不同毫秒内,序列号置为0
                sequence = 0L;
            }
    
            lastStmp = currStmp;
    
            return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分
                    | datacenterId << DATACENTER_LEFT       //数据中心部分
                    | machineId << MACHINE_LEFT             //机器标识部分
                    | sequence;                             //序列号部分
        }
    
        private long getNextMill() {
            long mill = getNewstmp();
            while (mill <= lastStmp) {
                mill = getNewstmp();
            }
            return mill;
        }
    
        private long getNewstmp() {
            return System.currentTimeMillis();
        }
    
        public static void main(String[] args) {
            SnowFlake snowFlake = new SnowFlake(2, 3);
    
            long start = System.currentTimeMillis();
            for (int i = 0; i < 1000; i++) {
                System.out.println(snowFlake.nextId());
            }
            System.out.println(System.currentTimeMillis() - start);
        }
    }
    

    三、SnowFlake 算法的生产实践

    通过前面的讲述我们知道 SnowFlake 算法能产生全局唯一 ID 主要是通过 5 位 datacenterId 和 5 位 workerId 来做区分,而在生产实践中,我们常常将机器 ID(hostid)来当成 datacenterId,将机器上运行的实例ID(pid)来当成 workerId。

    如今的分布式部署都在强调无状态化,那么给每台机器绑定一个 hostid 显然不太现实,假设又是在容器化环境下,没有固定的 IP,并且容器实例每次重新启动后的唯一 ID 还不一致。综上,基于机器 ID 的思路不可行。所以经常的做法是,利用 ZK/Redis/DB 等作为一个全局的 datacenterId/workerId 发号器,由发号器来分配唯一的 datacenterId/workerId 。(可以参考百度分布式唯一 ID 生成器 UidGenerator)

    四、SnowFlake 算法的问题思考

    1. 时间回拨问题

    由于机器的时间是动态的调整的,有可能会出现时间跑到之前几毫秒,如果这个时候获取到了这种时间,则会出现数据重复。

    这个问题的解决方案是采用“历史时间”。在进程启动后,我们会将当前时间(实际处理采用了延迟10ms启动),作为该业务这台机器进程的时间戳中的起始时间字段,后续的自增是在序列号自增到最大值时,时间戳增 1,而序列号重新归为 0。

    2. 机器 id 分配及回收

    目前机器 id 需要每台机器不一样,这样的方式分配需要有方案进行处理,同时也要考虑,如果该机器宕机了,对应的 datacenterId/workerId 分配后的回收问题。

    这个问题的解决方案是:每个实例启动,扩容,直接从 ZK/Redis/DB 等发号器取一个 id 作为 datacenterId/workerId,下线不销毁;并且维护一个活动节点队列,在地址空间耗尽的时候,指针指回队列头部,当分配的 id 存在于活动节点队列则跳过取下一个可用空间,达到复用原地址空间的目的。

  • 相关阅读:
    Leetcode 121. Best Time to Buy and Sell Stock
    Leetcode 120. Triangle
    Leetcode 26. Remove Duplicates from Sorted Array
    Leetcode 767. Reorganize String
    Leetcode 6. ZigZag Conversion
    KMP HDU 1686 Oulipo
    多重背包 HDU 2844 Coins
    Line belt 三分嵌套
    三分板子 zoj 3203
    二分板子 poj 3122 pie
  • 原文地址:https://www.cnblogs.com/jmcui/p/14302623.html
Copyright © 2011-2022 走看看