zoukankan      html  css  js  c++  java
  • 分布式序号发生器-snowflake雪花算法

    全局唯一ID,目的是让分布式系统中的所有元素都能有唯一的识别信息。

    1.UUID

    UUID概述

    UUID (Universally Unique Identifier),通用唯一识别码。UUID是基于当前时间、计数器(counter)和硬件标识(通常为无线网卡的MAC地址)等数据计算生成的。

    格式 & 版本

    UUID由以下几部分的组合:

    1. 当前日期和时间,UUID的第一个部分与时间有关,如果你在生成一个UUID之后,过几秒又生成一个UUID,则第一个部分不同,其余相同。
    2. 时钟序列。
    3. 全局唯一的IEEE机器识别号,如果有网卡,从网卡MAC地址获得,没有网卡以其他方式获得。
     

    UUID 是由一组32位数的16进制数字所构成,以连字号分隔的五组来显示,形式为 8-4-4-4-12,总共有 36个字符(即三十二个英数字母和四个连字号)。例如:

    aefbbd3a-9cc5-4655-8363-a2a43e6e6c80
    xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
    

    数字 M的表示 UUID 版本,当前规范有5个版本,M可选值为1, 2, 3, 4, 5 ;

    数字 N的一至四个最高有效位(bit)表示 UUID 变体( variant ),有固定的两位10xx,因此N只可能取值8, 9, a, b

    UUID版本通过M表示,当前规范有5个版本,M可选值为1, 2, 3, 4, 5。这5个版本使用不同算法,利用不同的信息来产生UUID,各版本有各自优势,适用于不同情景。具体使用的信息

    • version 1, date-time & MAC address

      基于时间的UUID通过计算当前时间戳、随机数和节点标识:机器MAC地址得到。由于在算法中使用了MAC地址,这个版本的UUID可以保证在全球范围的唯一性。但与此同时,使用MAC地址会带来安全性问题,这就是这个版本UUID受到批评的地方。同时, Version 1没考虑过一台机器上起了两个进程这类的问题,也没考虑相同时间戳的并发问题,所以严格的Version1没人实现,Version1的变种有Hibernate的CustomVersionOneStrategy.java、MongoDB的ObjectId.java、Twitter的snowflake等。

    • version 2, date-time & group/user id

      DCE(Distributed Computing Environment)安全的UUID和基于时间的UUID算法相同,但会把时间戳的前4位置换为POSIX的UID或GID。这个版本的UUID在实际中较少用到。

    • version 3, MD5 hash & namespace

      基于名字的UUID通过计算名字和名字空间的MD5散列值得到。这个版本的UUID保证了:相同名字空间中不同名字生成的UUID的唯一性;不同名字空间中的UUID的唯一性;相同名字空间中相同名字的UUID重复生成是相同的。

    • version 4, pseudo-random number

      根据随机数,或者伪随机数生成UUID。

    • version 5, SHA-1 hash & namespace

      和版本3的UUID算法类似,只是散列值计算使用SHA1(Secure Hash Algorithm 1)算法。

    ​ 使用较多的是版本1和版本4,其中版本1使用当前时间戳和MAC地址信息。版本4使用(伪)随机数信息,128bit中,除去版本确定的4bit和variant确定的2bit,其它122bit全部由(伪)随机数信息确定。若希望对给定的一个字符串总是能生成相同的 UUID,使用版本3或版本5。

    重复几率

    Java中 UUID 使用版本4进行实现,所以由java.util.UUID类产生的 UUID,128个比特中,有122个比特是随机产生,4个比特标识版本被使用,还有2个标识变体被使用。利用生日悖论,可计算出两笔 UUID 拥有相同值的机率约为
    p(n) ≈ 1 - e -n*n/2x

    其中x为 UUID 的取值范围,n为 UUID 的个数。

    以下是以 x = 2122 计算出n笔 UUID 后产生碰撞的机率:

    n机率
    68,719,476,736 = 236 0.0000000000000004 (4 x 10-16)
    2,199,023,255,552 = 241 0.0000000000004 (4 x 10-13)
    70,368,744,177,664 = 246 0.0000000004 (4 x 10-10)

    产生重复 UUID 并造成错误的情况非常低,是故大可不必考虑此问题。

    机率也与随机数产生器的质量有关。若要避免重复机率提高,必须要使用基于密码学上的强伪随机数产生器来生成值才行。

    UUID 是由一组32位数的16进制数字所构成,是故 UUID 理论上的总数为1632 =2128,约等于3.4 x 10123。也就是说若每纳秒产生1百万个 UUID,要花100亿年才会将所有 UUID 用完。

    Java实现
    /**
     * Static factory to retrieve a type 4 (pseudo randomly generated) UUID.
     * 使用静态工厂来获取版本4(伪随机数生成器)的 UUID
     * The {@code UUID} is generated using a cryptographically strong pseudo
     * 这个UUID生成使用了强加密的伪随机数生成器(PRNG)
     * random number generator.
     *
     * @return  A randomly generated {@code UUID}
     */
    public static UUID randomUUID() {
        SecureRandom ng = Holder.numberGenerator;
    
        byte[] randomBytes = new byte[16];
        ng.nextBytes(randomBytes);
        randomBytes[6]  &= 0x0f;  /* clear version        */
        randomBytes[6]  |= 0x40;  /* set to version 4     */
        randomBytes[8]  &= 0x3f;  /* clear variant        */
        randomBytes[8]  |= 0x80;  /* set to IETF variant  */
        return new UUID(randomBytes);
    }
    
    /**
     * Static factory to retrieve a type 3 (name based) {@code UUID} based on
     * the specified byte array.
     * 静态工厂对版本3的实现,对于给定的字符串(name)总能生成相同的UUID
     * @param  name
     *         A byte array to be used to construct a {@code UUID}
     *
     * @return  A {@code UUID} generated from the specified array
     */
    public static UUID nameUUIDFromBytes(byte[] name) {
        MessageDigest md;
        try {
            md = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException nsae) {
            throw new InternalError("MD5 not supported", nsae);
        }
        byte[] md5Bytes = md.digest(name);
        md5Bytes[6]  &= 0x0f;  /* clear version        */
        md5Bytes[6]  |= 0x30;  /* set to version 3     */
        md5Bytes[8]  &= 0x3f;  /* clear variant        */
        md5Bytes[8]  |= 0x80;  /* set to IETF variant  */
        return new UUID(md5Bytes);
    }
    
    生成UUID
    // Java语言实现
    import java.util.UUID;
    
    public class UUIDProvider{
        public static void main(String[] args) {
            // 利用伪随机数生成版本为4,变体为9的UUID
            System.out.println(UUID.randomUUID());
            
            // 对于相同的命名空间总是生成相同的UUID,版本为3,变体为9
            // 命名空间为"xxx"时生成的UUID总是为f561aaf6-ef0b-314d-8208-bb46a4ccb3ad
            System.out.println(UUID.nameUUIDFromBytes("xxx".getBytes()));
        }
    } 
    
    优点
    • 简单,代码方便。
    • 生成ID性能非常好,基本不会有性能问题。本地生成,没有网络消耗。
    • 全球唯一,在遇见数据迁移,系统数据合并,或者数据库变更等情况下,可以从容应对。
    缺点
    • 采用无意义字符串,没有排序,无法保证趋势递增。
    • UUID使用字符串形式存储,数据量大时查询效率比较低
    • 存储空间比较大,如果是海量数据库,就需要考虑存储量的问题。

    2.雪花算法(twitter/snowflake)

    雪花算法概述

    SnowFlake 算法,是 Twitter 开源的分布式 id 生成算法。其核心思想就是:使用一个 64 bit 的 long 型的数字作为全局唯一 id。在分布式系统中的应用十分广泛,且ID 引入了时间戳,基本上保持自增的。其原始版本是scala版,后面出现了许多其他语言的版本如Java、C++等。

    格式

    • 1bit - 首位无效符

    • 41bit - 时间戳(毫秒级)

      • 41位可以表示241 -1个数字;
      • 241 -1毫秒,换算成年就是表示 69 年的时间
    • 10bit - 工作机器id

      • 5bit - datacenterId机房id
      • 5bit - workerId机器 id
    • 12bit - 序列号

      序列号,用来记录同一个datacenterId中某一个机器上同毫秒内产生的不同id。

    特点(自增、有序、适合分布式场景)
    • 时间位:可以根据时间进行排序,有助于提高查询速度。
    • 机器id位:适用于分布式环境下对多节点的各个节点进行标识,可以具体根据节点数和部署情况设计划分机器位10位长度,如划分5位表示进程位等。
    • 序列号位:是一系列的自增id,可以支持同一节点同一毫秒生成多个ID序号,12位的计数序列号支持每个节点每毫秒产生4096个ID序号

    snowflake算法可以根据项目情况以及自身需要进行一定的修改

    Twitter算法实现

    ​ Twitter算法实现(Scala)

    Java算法实现
    public class IdWorker{
    
        //10bit的工作机器id
        private long workerId;    // 5bit
        private long datacenterId;   // 5bit
    
        private long sequence; // 12bit 序列号
    
        public IdWorker(long workerId, long datacenterId, long sequence){
            // sanity check for workerId
            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));
            }
            System.out.printf("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
                    timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);
    
            this.workerId = workerId;
            this.datacenterId = datacenterId;
            this.sequence = sequence;
        }
    
        //初始时间戳
        private long twepoch = 1288834974657L;
    
        //长度为5位
        private long workerIdBits = 5L;
        private long datacenterIdBits = 5L;
        //最大值 -1 左移 5,得结果a,-1 异或 a:利用位运算计算出5位能表示的最大正整数是多少。
        private long maxWorkerId = -1L ^ (-1L << workerIdBits); //31
        private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); // 31
        //序列号id长度
        private long sequenceBits = 12L;
        //序列号最大值
        private long sequenceMask = -1L ^ (-1L << sequenceBits); //4095
    
        //workerId需要左移的位数,12位
        private long workerIdShift = sequenceBits; //12
        //datacenterId需要左移位数 
        private long datacenterIdShift = sequenceBits + workerIdBits; // 12+5=17
        //时间戳需要左移位数 
        private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; // 12+5+5=22
    
        //上次时间戳,初始值为负数
        private long lastTimestamp = -1L;
    
        public long getWorkerId(){
            return workerId;
        }
    
        public long getDatacenterId(){
            return datacenterId;
        }
    
        public long getTimestamp(){
            return System.currentTimeMillis();
        }
    
        //下一个ID生成算法
        public synchronized long nextId() {
            long timestamp = timeGen();
    
            //获取当前时间戳如果小于上次时间戳,则表示时间戳获取出现异常
            if (timestamp < lastTimestamp) {
                System.err.printf("clock is moving backwards.  Rejecting requests until %d.", lastTimestamp);
                throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds",
                        lastTimestamp - timestamp));
            }
    
            //获取当前时间戳如果等于上次时间戳(同一毫秒内),则在序列号加一;否则序列号赋值为0,从0开始。
            if (lastTimestamp == timestamp) {
                // 通过位与运算保证计算的结果范围始终是 0-4095
                sequence = (sequence + 1) & sequenceMask; 
                if (sequence == 0) {
                    timestamp = tilNextMillis(lastTimestamp);
                }
            } else {
                sequence = 0;
            }
    
            //将上次时间戳值刷新
            lastTimestamp = timestamp;
    
            /**
             * 返回结果:
             * (timestamp - twepoch) << timestampLeftShift) 表示将时间戳减去初始时间戳,再左移相应位数
             * (datacenterId << datacenterIdShift) 表示将数据id左移相应位数
             * (workerId << workerIdShift) 表示将工作id左移相应位数
             * | 是按位或运算符,例如:x | y,只有当x,y都为0的时候结果才为0,其它情况结果都为1。
             * 因为个部分只有相应位上的值有意义,其它位上都是0,所以将各部分的值进行 | 运算就能得到最终拼接好的id
             */
            return ((timestamp - twepoch) << timestampLeftShift) |
                    (datacenterId << datacenterIdShift) |
                    (workerId << workerIdShift) |
                    sequence;
        }
    
        //获取时间戳,并与上次时间戳比较
        private long tilNextMillis(long lastTimestamp) {
            long timestamp = timeGen();
            while (timestamp <= lastTimestamp) {
                timestamp = timeGen();
            }
            return timestamp;
        }
    
        //获取系统时间戳
        private long timeGen(){
            return System.currentTimeMillis();
        }
    
        //---------------测试---------------
        public static void main(String[] args) {
            IdWorker worker = new IdWorker(1,1,1);
            for (int i = 0; i < 30; i++) {
                System.out.println(worker.nextId());
            }
        }
    
    }
    
    优点
    • 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
    • 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
    • 可以根据自身业务特性分配bit位,非常灵活。
    缺点
    • 雪花算法在单机系统上ID是递增的,但是在分布式系统多节点的情况下,所有节点的时钟并不能保证不完全同步,所以有可能会出现不是全局递增的情况。如果系统时间被回调,或者改变,可能会造成id冲突或者重复。

    3.利用数据库的auto_increment特性

    以MySQL举例,利用给字段设置auto_increment_increment和auto_increment_offset来保证ID自增,每次业务使用下列SQL读写MySQL得到ID号

    优点
    • 非常简单,利用现有数据库系统的功能实现,成本小,有DBA专业维护。
    • ID号单调自增,可以实现一些对ID有特殊要求的业务。
    缺点
    • 强依赖DB,当DB异常时整个系统不可用,属于致命问题。配置主从复制可以尽可能的增加可用性,但是数据一致性在特殊情况下难以保证。主从切换时的不一致可能会导致重复发号。
    • ID发号性能瓶颈限制在单台MySQL的读写性能
    • 分表分库,数据迁移合并等比较麻烦
    Keep moving forwards~
  • 相关阅读:
    leetcode 131. Palindrome Partitioning
    leetcode 526. Beautiful Arrangement
    poj 1852 Ants
    leetcode 1219. Path with Maximum Gold
    leetcode 66. Plus One
    leetcode 43. Multiply Strings
    pytorch中torch.narrow()函数
    pytorch中的torch.repeat()函数与numpy.tile()
    leetcode 1051. Height Checker
    leetcode 561. Array Partition I
  • 原文地址:https://www.cnblogs.com/-X-peng/p/15534070.html
Copyright © 2011-2022 走看看