分布式全局唯一ID生成

1.为什么需要分布式全局唯一ID

分布式系统下唯一ID生成。

2.ID生成规则部分硬性要求

  • 全局唯一
    不能出现重复ID号
  • 趋势递增
    在MySQL的INODB引擎中使用的是聚簇索引,由于多数RDBMS使用的Btree的数据结构来存储索引数据,在主键的选择上应尽量使用有序的主键保证写入性能
  • 单调递增
    尽量保证下一个的ID尽量大于下一个,例如事务版本号、IM增量消息、排序等特殊需求
  • 信息安全
    如果ID是连续的,恶意用户爬取数据比较方便。所以在一些场景下需要ID无规则,让竞争对手不好猜
  • 含时间戳
    最好包含一个时间戳,方便获知ID生成时间

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

  • 高可用
    服务器保证99.999%的情况下创建一个唯一的分布式ID
  • 低延迟
    服务要快
  • 高QPS
    服务器要顶住且一下创建10万个分布式ID

4.分布式ID通用生成方案

(1).UUID

如果只考虑唯一性是OK的,但是由于无序导致入库性能差。

注意:
1.分布式Id一般用来作为主键,但是MySQL官网推荐主键尽量越短越好,36位的UUID不太合适
2.分布式Id作为主键,主键是包含索引的,MySQL通过B+树来生成索引,每次新的UUID数据插入,为了查询优化,都会对索引底层的B+数进行修改,因为UUID是无序的,所以每次UUID数据的插入都会对B+树进行很大的修改,这一点很不友好,插入完全无序,不但会导致一些中间节点产生分裂,也会白白创造出很多不饱和节点,这样大大降低了数据库插入性能。

结论:只能保证唯一性

(2).数据库自增主键

在分布式里,数据库自增ID的机制主要原理是:数据库自增ID和MySQL数据库的replace into实现。
replace into 首先尝试插入数据,如果发现有此行数据(根据主键或唯一索引判断)则先删除再插入。

注意:
1.系统水平扩展比较困难,通过设置初始值和步长勉强可以满足
2.数据库压力大,每次获取ID都读写一遍数据库,非常影响性能

结论:唯一性、自增满足,但是使用mysql在高QPS情况下性能较差

(3).基于Redis生成全局id策略

因为Redis是单线程的天生保证原子性,可以使用原子操作INCR和INCRBY来实现

注意:
1.Redis集群情况下需要设置步长,同时key一定要设置有效期
假设一个集群5台Redis服务器,可以初始化Redis值为1,2,3,4,5,然后步长是5,各RedisID生成:
A:1,6,11
B:2,7,12
C:3,8,13
D:4,9,14
E:5,10,15
2.横向扩展差,Redis集群维护复杂

5.雪花算法snowflake(推荐使用)

(1).概设

Twitter的分布式自增主键Id算法snowflake:
1.SnowFlake生成的Id能够按照时间顺序生成
2.SnowFlake算法生成id的结果是一个64bit大小的整数,为一个Long型(转换成String后长度最多19位)
3.分布式系统不会产生Id碰撞(有datacenter-数据中心和workid-机器码做区分),并且效率较高

(2).结构


号段解析:

  • 1bit
    生成的id一般都是用整数,所以最高位固定为0
  • 41bit。时间戳,用来记录时间戳,毫秒级
    41位可以表示2^{41}-1个数字
    如果用来表示整数(机器中正数包括0)可以表示的数值范围是0-2^{41}-1,减1因为表示的数值范围从0开始,而不是1
    41位可表示 2^{41}-1 个毫秒值,转换成单位年则是(2^{41}-1)/(1000606024365)=69年,从1970年开始可用到2039-09-07
  • 10bit。工作机器id,用来记录工作机器id
    可以部署在2^{10}=1024个节点上,包括5位datacenterid和5位workid
    5位(bit)可以表示的最大整数是2^5-1=31个,即可以用0,1,2…31这32个数字来表示不同的datacenterid和workid
  • 12bit。序列号,用来记录同毫秒内生成的不同id。
    12位(bit)可以表示的最大正整数是2^{12}-1=4095,即可以用0,1,2…4094这4095个数字来表示同一机器同一时间戳(毫秒)内产生的4095个id序号
(3).源码

https://github.com/twitter-archive/snowflake

(4).工作落地

1.可使用糊涂工具包:https://github.com/looly/hutool
2.自己封装工具类

public class IdWorker {

    /**
     * 机器id所占的位数
     */
    private final long workerIdBits = 5L;

    /**
     * 数据标识id所占的位数
     */
    private final long datacenterIdBits = 5L;

    /**
     * 工作机器ID(0~31)
     */
    private long workerId;

    /**
     * 数据中心ID(0~31)
     */
    private long datacenterId;

    /**
     * 毫秒内序列(0~4095)
     */
    private long sequence = 0L;

    /**
     * 上次生成ID的时间截
     */
    private long lastTimestamp = -1L;
    private static IdWorker idWorker;

    static {
        idWorker = new IdWorker();
    }

    /**
     * 每次调用产生一个新的ID并返回
     *
     * @return 最新的ID
     */
    public static synchronized long getNextId() {
        return idWorker.nextId();
    }

    private IdWorker() {
        this.workerId = getWorkerId();
        this.datacenterId = getDatacenterId();
        //支持的最大机器id,31
        long maxWorkerId = -1L ^ (-1L << workerIdBits);
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        //支持的最大数据标识id,31
        long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        }
    }


    /**
     * 获得下一个ID (该方法是线程安全的)
     *
     * @return SnowflakeId
     */
    private synchronized long nextId() {
        long timestamp = timeGen();

        //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(
                    String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        //序列在id中占的位数
        long sequenceBits = 12L;
        //如果是同一时间生成的,则进行毫秒内序列
        if (lastTimestamp == timestamp) {
            //生成序列的掩码,4095 (0b111111111111=0xfff=4095)
            long sequenceMask = -1L ^ (-1L << sequenceBits);
            sequence = (sequence + 1) & sequenceMask;
            //毫秒内序列溢出
            if (sequence == 0) {
                //阻塞到下一个毫秒,获得新的时间戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        }
        //时间戳改变,毫秒内序列重置
        else {
            sequence = 0L;
        }

        //上次生成ID的时间截
        lastTimestamp = timestamp;


        //开始时间截 (2018-01-01 00:00:00)
        long twepoch = 1514736000000L;

        //数据标识id向左移17位(12+5)
        long datacenterIdShift = sequenceBits + workerIdBits;
        // 时间截向左移22位(5+5+12)
        long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
        return ((timestamp - twepoch) << timestampLeftShift)
                //移位并通过或运算拼到一起组成64位的ID
                | (datacenterId << datacenterIdShift)
                | (workerId << sequenceBits)
                | sequence;
    }

    /**
     * 阻塞到下一个毫秒,直到获得新的时间戳
     *
     * @param lastTimestamp 上次生成ID的时间截
     * @return 当前时间戳
     */
    private long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    /**
     * 返回以毫秒为单位的当前时间
     *
     * @return 当前时间(毫秒)
     */
    private long timeGen() {
        return System.currentTimeMillis();
    }

    /**
     * 获取机器编码
     *
     * @return workerId
     */
    private long getWorkerId() {
        long machineId;
        StringBuilder sb = new StringBuilder();
        Enumeration<NetworkInterface> e = null;
        try {
            e = NetworkInterface.getNetworkInterfaces();
        } catch (SocketException e1) {
            e1.printStackTrace();
        }
        while (e != null && e.hasMoreElements()) {
            NetworkInterface ni = e.nextElement();

            Enumeration<InetAddress> addrs;
            addrs = ni.getInetAddresses();
            if (addrs == null) {
                continue;
            }

            // 获取IP地址(获取不到IP地址时使用网卡名)
            String ipStr = "";
            InetAddress ip;
            while (addrs.hasMoreElements()) {
                ip = addrs.nextElement();
                if (!ip.isLoopbackAddress() && ip.isSiteLocalAddress() && ip.getHostAddress().indexOf(":") == -1) {
                    try {
                        ipStr = ip.toString();
                    } catch (ArrayIndexOutOfBoundsException aioe) {
                        ipStr = ni.toString();
                    }
                }
            }
            sb.append(ipStr);
        }
        machineId = sb.toString().hashCode();
        //工作机器ID掩码
        long workerIdMask = -1L ^ (-1L << workerIdBits);
        return machineId & workerIdMask;
    }

    /**
     * 获取数据中心Id
     *
     * @return datacenterId
     */
    private long getDatacenterId() {
        //获取进程编码
        RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
        long datacenterId = Long.valueOf(runtimeMXBean.getName().split("@")[0]);
        //数据中心ID掩码
        long datacenterIdMask = -1L ^ (-1L << datacenterIdBits);
        return datacenterId & datacenterIdMask;
    }

    public static String next() {
        return String.valueOf(getNextId());
    }

    /**
     * get uuid
     *
     * @return return id like 'c79454d6-9e0c-4b4a-a2bc-62f559f71570'
     * @since v0.2.22
     */
    public static String uuid() {
        return UUID.randomUUID().toString();
    }

    /**
     * get uuid
     *
     * @return return id like 'c79454d69e0c4b4aa2bc62f559f71570'
     * @since v0.2.22
     */
    public static String simpleUUID() {
        return uuid().replaceAll("-", "");
    }

}
(5).缺点

依赖时钟,如果时钟回拨,会导致生成重复的id
在单机上是递增的,但是到分布式环境,每台机器时钟不可能完全同步,有时候会出现不是全局递增的情况(此缺点可以认为无所谓的,一般分布式id只要求趋势递增,并不会严格要求递增)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值