分布式唯一ID之“雪花算法“

50 篇文章 0 订阅
44 篇文章 0 订阅

雪花算法是啥?它和分布式唯一ID有什么关系?为什么需要分布式ID?不要着急哈,等洪爵和你一一道来。

​ 首先说下什么是分布式唯一ID,随着互联网的发展,服务化的演进,我们的单体服务已经不能满足时代的需求,业务上的一张表数据量也越来越大,大大的降低了查询、插入效率。单库单表已经不能支撑现有业务了,这个时候就出现了主从同步,写库和读库通过主从同步进行读写分离

在这里插入图片描述

​ 但读库和写库在表结构上,还是同样的数据,只是分开了查询和写操作的压力,不能从根本上解决问题,表的数据量依然很大,这时候就需要对数据库进行分库分表了,但分库分表就存在一个问题,每一个数据库中的每一张表理论上都属于同一个业务,它们需要有唯一的一个标识去表示每一条数据、定位到每一条数据

在这里插入图片描述

​ 那要怎么操作?数据分布在不同的数据库,不同的表,我的天啊?!这能实现吗?或许你会说SQL语句在执行前会先经过服务器,可以在系统里的代码逻辑去保证它的唯一,比如在系统里专门写一个方法生成唯一的code,这好像可以实现,但要考虑并发的情况,同时进来两个线程进行插入那要怎么办呢?**你会说加锁呀,保证同一个时间点只有一个线程进来。**咦?这好像很有道理啊,但是这也是有局限性的,因为能涉及到分库分表的业务场景,单体应用肯定不能满足,系统肯定已经是分布式的应用了,每个分布式节点又有集群来保证它的高可用。在这样的一个情况下,如何保证每个系统生成不同且唯一的id呢

​ 这个时候,某个同学A举起了手,洪爵点名让他回答,同学A说:洪爵,我觉得可以维护一个中心,每次需要生成唯一id就去RPC调用该服务去获取到唯一的id,又或者这个中心可以是一个Redis服务等等。咦?这个好像可行。但也有很多不足的地方,第一个是说每次都需要进行通信,网络传输时间消耗占比比较大;第二个是说只由一个中心去维护,承受的QPS太高,并且还要保证不生成重复的id,单独维护这样一个中心的成本较高

​ **那有没有一种方法,可以让每个服务器自己内部去生成一个id,并且在全局来看是唯一的,同时生成的这个id有一定的自增能力。**啥意思呢?比如说A服务器在10:10分生成了一个唯一Id:1001,B服务器在10:11分生成了一个唯一Id:998,这个是我们要尽量去避免的,唯一Id和它插入的时间应该保持增长趋势。

什么?世界上竟然还有这种算法?真的吗?我年纪还小,你别骗我啊!

​ 可以的,请相信洪爵,洪爵来告诉你其中一种实现方式就是雪花算法!一想到雪花,不禁潸然泪下,洪爵长这么大,还没见过在天上飘着的雪,如果杭州下雪了,我希望能去西湖看一眼断桥残雪… …咳咳,走题了,咱们回到主题来。

​ 既然雪花算法是分布式唯一ID生成策略之一,那么肯定有它过人之处,咱们细细道来,认真看完本篇文章,保证可以理解雪花算法的实现,以及怎么保证全局的唯一性。

​ 雪花算法是Twitter开源的分布式Id生成算法,算法生成的Id是一个64bit大小的整数,它的构成如下(每个部分的bit数并不是完全固定,可以按照对应的业务场景进行修改):

在这里插入图片描述

1、最高位为符号位,1表示负数、0表示正数,生成唯一Id一般都为正整数,所以最高位固定为0。

2、41bit-时间戳,用来记录毫秒级的时间点(比如说当前时间点 - 一个固定的时间点),减去的固定时间点越大,那么41bit就有更多的空间去存储,当然也可以不减。有了时间戳能在一定程度上保证唯一Id的递增,41bit的大小理论上可以使用(2^41 - 1) / 1000 / 60 / 60 / 24 / 365 约等于 49年。

3、10bit-机器Id,表示不同的机器Id,10bit可以表示1024个机器节点,一般会把这10个bit拆分成2部分,每个部分5bit,第一个部分代表机房的id,第二个部分代表机器id,这样就保证了每个机器生成的Id都区分开来了。

4、12bit-序列号,表示在同一个毫秒内,每个机器最多能产生的id数量,这种情况发生在并发的时候,可以生产2^12-1=4095个Id数量。

​ 当你需要支持并发量更大的场景,可以增加序列号的位数,如果需要更多的机器节点,可以提高机器Id所占用的位数,当然也要有所取舍,增加了其中一个部分的位数,另一个部分所占的位数就会减少,按照对应场景去考虑即可

看下具体代码实现:

public class SnowFlake {

    /**
     * 组成部分
     */

    // 一个固定的时间戳 2017-09-04 15:00:00 可以为0, 具体值可以自己定义
    private final long fixedTimeStamp = 1504508400L;

    // 机房Id
    private long computerRoomId;

    // 机器Id
    private long machineId;

    // 序列号 0~4095
    private long sequence = 0L;

    /**
     * 所占的bit大小
     */

    // 时间戳在高位 所以就是其余组成部分所占bit剩余的部分

    // 机房Id占用bit数量
    private final long computerRoomIdBitCnt = 5L;

    // 机器Id占用bit数量
    private final long machineIdBitCnt = 5L;

    // 序列号占用bit数量
    private final long sequenceBitCnt = 12L;

    /**
     * 各组成部分需要位移的位数
     */

    // 机器Id需要向左移的位数
    private final long machineIdShift = sequenceBitCnt;

    // 机房Id需要向左移的位数
    private final long computerRoomIdShift = machineIdShift + machineIdBitCnt;

    // 时间戳需要向左移的位数
    private final long timeStampShift = computerRoomIdShift + computerRoomIdBitCnt;

    /**
     * 剩余一些聚合的信息
     * -1 ^ (-1 << x) 代表 2 ^ x - 1 感兴趣的同学可以自行推演
     */

    // 支持最大的机房Id大小 31
    private final long maxComputerRoomId = -1 ^ (-1 << computerRoomIdBitCnt);

    // 支持最大的机器Id大小 31
    private final long maxMachineId = -1 ^ (-1 << machineIdBitCnt);

    // 序列号掩码
    private final long sequenceMask = -1 ^ (-1 << sequenceBitCnt);

    // 上一次生成的时间戳
    private long lastTimeStamp = -1L;

    /**
     * 构造函数 初始化机房Id 和 机器Id
     * @param computerRoomId
     * @param machineId
     */
    public SnowFlake(long computerRoomId, long machineId) {
        if(computerRoomId < 0 || maxComputerRoomId < computerRoomId) {
            throw new IllegalArgumentException("computerRoomId out of range");
        }
        if(machineId < 0 || maxMachineId < machineId) {
            throw new IllegalArgumentException("machineId out of range");
        }

        this.computerRoomId = computerRoomId;
        this.machineId = machineId;
    }

    /**
     * 返回当前时间(毫秒级)
     * @return
     */
    protected long getCurrentTime() {
        return System.currentTimeMillis();
    }

    public synchronized long getNextId() {
        long currentTimeStamp = getCurrentTime();

        // 同一个毫秒内
        if(currentTimeStamp == lastTimeStamp) {
            sequence = (sequence + 1) & sequenceMask;
            // 表示轮了一圈 等待下一个毫秒再生成一个Id
            if(sequence == 0) {
                currentTimeStamp = getNextMillis();
            }
        } else {
            // 不是同一个毫秒内 初始化sequence
            sequence = 0;
        }

        // 维护当前时间戳
        lastTimeStamp = currentTimeStamp;

        return ((currentTimeStamp - fixedTimeStamp) << timeStampShift)
                | (computerRoomId << computerRoomIdShift)
                | (machineId << machineIdShift)
                | sequence;
    }
    
    protected long getNextMillis() {
        long currentTimeStamp = getCurrentTime();
        // 如果还是同一个毫秒 则继续等待
        while(currentTimeStamp <= lastTimeStamp) {
            currentTimeStamp = getCurrentTime();
        }

        return currentTimeStamp;
    }

    public static void main(String[] args) {
        SnowFlake snowFlake = new SnowFlake(1, 2);
        System.out.println(snowFlake.getNextId());
    }

}

​ **代码中涉及到的位运算符&,|,^,<<等等,还没了解的童鞋,赶紧去了解呀!!!**洪爵这里就不做扩展了,如果想要洪爵出一期位运算符的可以公众号私信我呀!

​ 代码中的机房Id和机器Id怎么保证一定不同的问题,有很多种办法,比如维护一种中心,由中心进行下发和分配等等。

​ 好了,希望本期对你有所帮助,别忘记自己敲一遍哦!

在这里插入图片描述愿每个人都能带着怀疑的态度去阅读文章并探究其中原理。

道阻且长,往事作序,来日为章。

期待我们下一次相遇!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

KnightHONG

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值