• 分布式id生成(UUID、雪花算法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 = 2^122^ 计算出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 理论上的总数为16^32^ =2^128^,约等于3.4 x 10^123^。也就是说若每纳秒产生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位可以表示2^41^ -1个数字;
      • 2^41^ -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的读写性能
    • 分表分库,数据迁移合并等比较麻烦

    4.Redis的INCR

    当使用数据库来生成ID性能不够要求的时候,我们可以尝试使用Redis来生成ID。

    这主要依赖于Redis是单线程的,所以也可以用生成全局唯一的ID。可以用Redis的原子操作 INCR和INCRBY来实现。

    比较适合使用Redis来生成每天从0开始的流水号。比如订单号=日期+当日自增长号。可以每天在Redis中生成一个Key,使用INCR进行累加。

    redis加lua脚本也可以实现twitter的snowflake算法。

    优点
    • 不依赖于数据库,灵活方便,且性能优于数据库。

    • 数字ID天然排序,对分页或者需要排序的结果很有帮助。

    缺点
    • 如果系统中没有Redis,还需要引入新的组件,增加系统复杂度。

    • 需要编码和配置的工作量比较大。

    5.参考链接

    https://www.jianshu.com/p/da6dae36c290

    https://blog.csdn.net/nawenqiang/article/details/82684001

    https://segmentfault.com/a/1190000011282426

    https://youzhixueyuan.com/how-to-generate-distributed-unique-id.html

  • 相关阅读:
    dell R610 idrac6 无法登陆全网最全解决方法
    数据挖掘——序列数据
    git pull 指定文件
    K8s+云原生
    Java8新特性学习
    什么是三星索引?
    Mongodb学习笔记
    JUC 学习笔记
    SQL函数Group_concat用法
    MySQL索引下推
  • 原文地址:https://www.cnblogs.com/kuotian/p/12869914.html
Copyright © 2020-2023  润新知