全局唯一ID算法之雪花算法

雪花算法


话不多说,直接上代码,看注释:

import java.sql.Date;
import java.time.DateTimeException;
import java.util.concurrent.CountDownLatch;

/*
 * @Description 雪花算法实践
 * @author fangqi
 * @date 2021/10/17
 */
public class SnowFlake {
    //雪花算法:组成ID的由64位二进制(8字节=Long的长度)来表示,主要由以下部分组成:
    //第一部分:1bit = 0,表示正整数
    //第二部分:41bit ,表示时间戳(毫秒),最大可以表示(2^41-1)/(365 * 24 * 60 * 60 * 1000) = 69年
    //第三部分:10bit ,又可以分为 5bit,表示机房号,5bit表示机房中的机器号,由于机房号和机器号都可以从0开始,所以 10bit能够表示的最大机器数 = 2^5*2^5=2^10=1024
    //第四部分:12bit ,表示同一毫秒内生成的id数,最大可以生成 2^12-1=4095 个ID(id必须大于0)

    private static final int fixBit = 1;
    private static final int timeStampBit = 41;
    private static final int dataCenterIdBit = 5;
    private static final int workIdBit = 5;
    private static final int sequenceIdBit = 12;

    private static final int zero = 0;
    //最大的机房ID
    private static final long maxDataCenterId = -1 ^ (-1 << dataCenterIdBit);
    //最大的机器ID
    private static final long maxWorkId = -1 ^ (-1 << workIdBit);
    //序列码,即组成雪花算法的最后12bit表示的最大值,该值与任何值进行'按位于'操作,使得序列ID始终小于等于4095
    private static final long sequenceMask = -1L ^ (-1L << sequenceIdBit);
    //默认偏移时间毫秒为1分钟
    private static final long defaultOffsetTimeMillis = 1 * 60 * 1000;
    //偏移时间大于该值,这告警,打印warn日志
    private static final long warningOffsetTimeMillis = 2 * 60 * 1000;
    //默认的起始时间
    private static final long defaultTimeEpoch = Date.valueOf("2021-10-19").getTime();

    //机房ID>=0
    private long dataCenterId;
    //机器ID>=0
    private long workId;
    //序列号ID
    private long sequenceId;
    //最近一次生成ID的时间戳,用于跟当前时间戳对比,如果相等则sequenceId加1,表示统一毫秒内生成的ID
    private long lastTimeMillis;

    //起始时间,最大可以记录2^41 - 1的时间戳,即69年,不能大于当前时间戳
    //注意:该值不能随时变动,如果是针对同一个业务获取ID,那么该值设置后就不能在变动,否则会导致最终生成的ID可能会出现重复
    //假设:
    // 第一次设置起始时间=1000,当执行一段时间后的时间为5000,那么5000-1000=4000;
    // 第二次设置起始时间=6000,执行一段时间后的时间为10000,那么10000-6000=4000;
    // 这样就会导致41位时间戳出现重复
    private long timeEpoch=defaultTimeEpoch;
    //允许当前lastTimeMillis跟当前时间戳的最大偏移毫秒数(lastTimeMillis-currentTimeMillis<=lastTimeMillis)
    //如果在允许范围内,可以一直循环获取当前时间,知道当前时间大于最后一次生成ID的时间
    //避免因为相差太大,导致获取下一个时间时,一直在循环中,所以该值需要慎重
    private long offsetTimeMills=defaultOffsetTimeMillis;


    public SnowFlake(long dataCenterId, long workId, long randomNum){

        this.validateDataCenterId(dataCenterId);

        this.validateWorkId(workId);

        if(randomNum < 0){
            throw new IllegalArgumentException("随机值不能小于0");
        }

        this.dataCenterId = dataCenterId;
        this.workId = workId;
        this.sequenceId = randomNum;
    }

    //设置起始时间
    public void setTimeEpoch(long timeEpoch){
        if(timeEpoch > this.curerrentTimeMillis()){
            throw new IllegalArgumentException("初始时间戳不能大于当前时间戳");
        }
        this.timeEpoch = timeEpoch;
    }

    //设置时间偏移量
    public void setOffsetTimeMills(long offsetTimeMills){
        if(offsetTimeMills < 0){
            throw new IllegalArgumentException("偏移时间戳不能小于0");
        }
        if(offsetTimeMills > warningOffsetTimeMillis){
            System.out.println("当前偏移时间值偏大,请确认是否进行修改......");
        }
        this.offsetTimeMills = offsetTimeMills;
    }

    /**
     * 获取下个ID
     * @return
     */
    public synchronized Long nextId(){
        int timeLeftMoveBits = dataCenterIdBit + workIdBit + sequenceIdBit;
        int dataCenterIdLeftMoveBits = workIdBit + sequenceIdBit;
        int workIdLeftMoveBits = sequenceIdBit;

        long currentTimeMillis = this.curerrentTimeMillis();
        if(lastTimeMillis < currentTimeMillis){
            sequenceId = 0;
        }else{
            //如果时间戳的差值大于了允许的最大偏移值,那么抛出异常,否则循环获取时间戳
            if(lastTimeMillis - currentTimeMillis > offsetTimeMills) {
                throw new DateTimeException(String.format("最后生成ID的时间戳与当前时间戳的差值已经大于设置的offsetTimeMills=%d", offsetTimeMills));
            }else{
                //该操作保证sequenceId始终是小于等于sequenceMask
                sequenceId = (sequenceId + 1) & sequenceMask;
                if(sequenceId == zero || lastTimeMillis - currentTimeMillis <= offsetTimeMills) {
                    currentTimeMillis = this.nextTimeMillis();
                }
            }
        }
        lastTimeMillis = currentTimeMillis;

        //计算最终生成的ID:
        //lastTimeMillis-timeEmpoch,目的是为了可以让41位二进制表示的时间更久(当然也可以不用减去timeEmpoch)
        //上面已经将对应每个部分的值都算出了,只需要往左移动响应的位数,然后将左移后的值一次做'按位或'操作,即可得到最终的ID
        return  ((lastTimeMillis - timeEpoch) << timeLeftMoveBits) |
                (this.dataCenterId << dataCenterIdLeftMoveBits) |
                (this.workId << workIdLeftMoveBits) |
                (this.sequenceId);
    }

    private void validateDataCenterId(long dataCenterId){
        if(dataCenterId > maxDataCenterId || dataCenterId < zero){
            throw new IllegalArgumentException(String.format("传入的机房ID不能大于%d或小于%d", maxDataCenterId, zero));
        }
    }

    private void validateWorkId(long workId){
        if(workId > maxWorkId || workId < zero){
            throw new IllegalArgumentException(String.format("传入的机器ID不能大于%d或小于%d", maxWorkId, zero));
        }
    }

    private long nextTimeMillis(){
        long currentTimeMillis = 0L;
        do{
            currentTimeMillis = this.curerrentTimeMillis();

        }while (currentTimeMillis <= lastTimeMillis);

        return currentTimeMillis;
    }

    private long curerrentTimeMillis(){
        return System.currentTimeMillis();
    }

    public static void main(String[] args) throws InterruptedException {

//        System.out.println(4095 & 4099);
//        System.out.println(4095 & 4);
//        System.out.println( -1 ^ (-1 << 12) );
//        System.out.println(String.format("传入的机房ID不能大于%d或小于%d", 1, 2));
//        System.out.println(Date.valueOf("2021-10-19").getTime());

        SnowFlake snowFlake = new SnowFlake(0, 0, 3);
        CountDownLatch countDownLatch = new CountDownLatch(10000);
        for (int i = 0; i < 10000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(snowFlake.nextId());
                }
            }).start();
            countDownLatch.countDown();
        }
        countDownLatch.await();
    }

参考文献:https://blog.csdn.net/lq18050010830/article/details/89845790

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值