雪花算法揭秘时刻

雪花算法的诞生?

Twitter的分布式自增ID算法snowflake,每秒能产生26万个自增可排序ID

  1. twitter的SnowFlake生成ID能够按照时间有序生成
  2. SnowFlake算法生成id的结果是一个64bit大小的整数,为一个Long类型
  3. 分布式系统内不会产生ID碰撞并且效率较高

分布式系统中,有些需要使用全局唯一ID的场景,生成ID的基本要求

  1. 在分布式的环境下必须全局唯一
  2. 一般需要单调递增,因为一般唯一ID都会存到数据库,而Innodb的特性就是将内容存储在主键索引树的叶子节点,而且是从左到右递增的,所以考虑到数据库的性能,一般生成的id也最好是单调递增。为防止ID冲突可以使用36位的UUID,但是UUID一般是无序的并且相对较长

组成结构:
雪花算法揭秘时刻_第1张图片
时间范围:2^41/365 * 24 * 60 * 60 * 1000L)=69.72年
工作进程数:2^10=1024
生成不碰撞序列的TPS:2^12 * 1000=409.6万

1bit符号位:永远不用,因为二进制中最高位是符号位,1表示负数,0表示正数,由于生成的id一般都是用正数,所以最高位一般固定为0
41bit-时间戳:用来记录时间戳,毫秒级别
41位可以表示2^41-1 个数字
如果只用来表示正整数,可以表示的数字范围是0~2^41-1

10bit-工作机器id,用来记录工作机器id
可以部署在1024个结点,包括5位datacenterId和5位workerId
5位可以表示的最大正整数是2^5-1=31,可以表示不同的datacenterId和workerId

12bit-序列号,用来记录同毫秒内产生的不同id
12bit可以表示最大正整数是2^12-1=4095,表示同一机器同一时间戳内产生的4095个ID序号

SnowFlake可以保证:
所有的生成的id按时间趋势递增
整个分布式系统内不会产生重复ID

gitHub链接

优点:

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

缺点:

  1. 依赖机器的时间,如果机器时间回拨,会导致重复ID生成
  2. 在单机上是递增的,但是由于设计到分布式环境,每台机器上时钟可能完全同步,有时会出现不同全局递增的情况

优化:
百度开源的分布式唯一ID生成器UidGenerator
美团点评分布式ID生成系统

hutool工具包中的雪花算法的使用

  1. 依赖引用

    
        com.xiaoleilu
        hutool-all
        3.0.1
    
    
  2. 方法调用:Snowflake

mybatisplus中的雪花算法使用

  1. 依赖引用

    
         com.baomidou
         mybatis-plus-boot-starter
         3.2.0
    
    
  2. 方法调用

    IdWorker.getIdStr()
    
  3. 源码解析:
    IdWorker类:

    public class IdWorker {
        private static Sequence WORKER = new Sequence();
        public static String getIdStr() {
            return String.valueOf(WORKER.nextId());
        }
    }
    

    Sequence 类:

    public class Sequence {
    	// 定义日志
        private static final Log logger = LogFactory.getLog(Sequence.class);
        // 开始时间戳
        private final long twepoch = 1288834974657L;
        // 机器id所占位数
        private final long workerIdBits = 5L;
        // 数据标识id所占位数
        private final long datacenterIdBits = 5L;
        // 支持的最大机器id
        private final long maxWorkerId = 31L;
        // 支持的最大数据标识id
        private final long maxDatacenterId = 31L;
        // 序列在id中所占的位数
        private final long sequenceBits = 12L;
        // 机器id向左移12位
        private final long workerIdShift = 12L;
        // 数据标识id向左移12位
        private final long datacenterIdShift = 17L;
        // 时间截向左移22位(5+5+12)
        private final long timestampLeftShift = 22L;
        // 生成序列的掩码,这里为4095
        private final long sequenceMask = 4095L;
        // 工作机器ID(0~31)
        private final long workerId;
        // 数据中心id
        private final long datacenterId;
        // 毫秒内序列
        private long sequence = 0L;
        //  上次生成ID的时间截
        private long lastTimestamp = -1L;
        /**
         * 无参构造函数
         */
        public Sequence() {
            this.datacenterId = getDatacenterId(31L);
            this.workerId = getMaxWorkerId(this.datacenterId, 31L);
        }
        /**
         * 构造函数
         * @param workerId 工作ID (0~31)
         * @param dataCenterId 数据中心ID (0~31)
         */	
        public Sequence(long workerId, long datacenterId) {
            Assert.isFalse(workerId > 31L || workerId < 0L, String.format("worker Id can't be greater than %d or less than 0", 31L), new Object[0]);
            Assert.isFalse(datacenterId > 31L || datacenterId < 0L, String.format("datacenter Id can't be greater than %d or less than 0", 31L), new Object[0]);
            this.workerId = workerId;
            this.datacenterId = datacenterId;
        }
    
        protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) {
            StringBuilder mpid = new StringBuilder();
            mpid.append(datacenterId);
            String name = ManagementFactory.getRuntimeMXBean().getName();
            if (StringUtils.isNotEmpty(name)) {
                mpid.append(name.split("@")[0]);
            }
    
            return (long)(mpid.toString().hashCode() & '\uffff') % (maxWorkerId + 1L);
        }
    
        protected static long getDatacenterId(long maxDatacenterId) {
            long id = 0L;
    
            try {
                InetAddress ip = InetAddress.getLocalHost();
                NetworkInterface network = NetworkInterface.getByInetAddress(ip);
                if (network == null) {
                    id = 1L;
                } else {
                    byte[] mac = network.getHardwareAddress();
                    if (null != mac) {
                        id = (255L & (long)mac[mac.length - 1] | 65280L & (long)mac[mac.length - 2] << 8) >> 6;
                        id %= maxDatacenterId + 1L;
                    }
                }
            } catch (Exception var7) {
                logger.warn(" getDatacenterId: " + var7.getMessage());
            }
    
            return id;
        }
     	 /**
         * 获得下一个ID (该方法是线程安全的)
         * @return SnowflakeId
         */
        public synchronized long nextId() {
            long timestamp = this.timeGen();
            if (timestamp < this.lastTimestamp) {
                long offset = this.lastTimestamp - timestamp;
                if (offset > 5L) {
                    throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
                }
    
                try {
                    this.wait(offset << 1);
                    timestamp = this.timeGen();
                    if (timestamp < this.lastTimestamp) {
                        throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
                    }
                } catch (Exception var6) {
                    throw new RuntimeException(var6);
                }
            }
    
            if (this.lastTimestamp == timestamp) {
                this.sequence = this.sequence + 1L & 4095L;
                if (this.sequence == 0L) {
                    timestamp = this.tilNextMillis(this.lastTimestamp);
                }
            } else {
                this.sequence = ThreadLocalRandom.current().nextLong(1L, 3L);
            }
    
            this.lastTimestamp = timestamp;
            return timestamp - 1288834974657L << 22 | this.datacenterId << 17 | this.workerId << 12 | this.sequence;
        }
        /**
         * 阻塞到下一个毫秒,直到获得新的时间戳
         * @param lastTimestamp 上次生成ID的时间截
         * @return 当前时间戳
         */
        protected long tilNextMillis(long lastTimestamp) {
            long timestamp;
            for(timestamp = this.timeGen(); timestamp <= lastTimestamp; timestamp = this.timeGen()) {
            }
    
            return timestamp;
        }
        /**
         * 返回以毫秒为单位的当前时间
         * @return 当前时间(毫秒)
         */
        protected long timeGen() {
            return SystemClock.now();
        }
    }
    
    

集群高并发情况下如何保证分布式唯一全局id生成?

ID生成规则部分硬性要求

  1. 全局唯一
    不能出现重复的ID号,是唯一标识
  2. 趋势递增
    在Mysql的InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用Btree的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能
  3. 单调递增
    保证下一个ID一定大于上一个ID
  4. 信息安全
    如果ID连续的,恶意用户的扒取工作就非常容易了,直接按照顺序下载指定URL即可;
    如果是订单号的话,竞争对手就可以直接知道我们一天的订单量;
    所以在一些场景下,需要ID无规则不规则,让竞争对手没有那么容易知道
  5. 含时间戳
    快速了解分布式ID的生成时间

ID号生成系统的可用性要求

高可用 低延迟 高QPS

ID生成?

  • UUID:唯一性和趋势递增
    标准形式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字节
    例子:

    550e8400-e29b-41d4-a716-446655440000
    

    性能非常高,本地生成,没有网络消耗

    进入数据库的性能比较差,无序

    为什么无序的UUID会导致入库性能变差呢?

    1. 无序,无法预测他生成顺序,不能生成递增有序的数字
    2. 主键,ID作为主键时在特定的环境会存在一些问题
      例如在MYSQL中,官方建议主键尽量越短越好。而UUID字符长36个,过长,不适合使用
    3. 索引,B+树索引的分裂
      分布式ID作为主键,主键中包含索引,然而mysql的索引是通过B+树来实现的,每一次新的UUID数据的插入,mysql为了查询优化,都会对索引底层的B+树进行一次修改。由于UUID是无序的,所以每一次UUID数据的插入都会对主键中B+树进行很大的修改。
  • 数据库自增主键:唯一性,递增
    主要原理:数据库自增ID和mysql数据库的replace into实现的
    replace into 的含义是插入一条记录,如果表中唯一索引值遇到冲突,则替换老数据

    数据库自增ID机制适合作分布式ID吗?

    答:不太适合

  1. 系统水平扩展比较困难
    例如:现在有一台机器发号是1,2,3,4,5,6(步长是1),但是需要扩展一台机器,我们可以将第二胎的初始值设置的比第一台超过很多。那如果线上有100台机器,这个时候要扩容,简直就是噩梦。
  2. 数据库压力很大。
    每次获取ID都得读写一次数据库,非常影响性能,不符合分布式ID的延迟低和高QPS的规则
  • 基于redis生成全局id策略
    天生保证原子性,可以使用原子操作INCR和INCRBY来实现
    注意:在redis集群情况下,同样和MySQL一样需要设置不同的增长步长,同时key一定要设置有效期。可以使用Redis集群来获取更高的吞吐量。

你可能感兴趣的:(java)