解锁雪花算法snowflake的新玩法————之隐式添加位标记

1. 背景

工作中当前项目采用的是雪花算法作为主键生成策略;

业务中碰到过一种情况:

个体表user,和团队表team,它们都有一个雪花算法生成的id,如何做到根据id就知道这个id是属于个体还是团体呢???
常规的做法我知道的有两种:
1.另加一个字段,字段type一直跟着这个id;
2.另加一个表,表中一个字段为id,一个字段为类型,麻烦的是要维护这个表的id和类型映射关系;

我们能不能控制雪花的生成算法去更简单有效地控制这点呢???

2. 简述雪花算法原理

SnowFlake算法生成id的结果是一个64bit大小的整数,它的结构如下图:
在这里插入图片描述

1) 1位,不用。二进制中最高位为1的都是负数,但是我们生成的id一般都使用整数,所以这个最高位固定是0
2) 41位,用来记录时间戳(毫秒)。
3) 41位可以表示2^41−1个数字,如果只用来表示正整数(计算机中正数包含0),可以表示的数值范围是:0 至 2^41−1,减1是因为可表示的数值范围是从0开始算的,而不是1。
也就是说41位可以表示2^41−1个毫秒的值,转化成单位年则是(2^41−1)/(1000∗60∗60∗24∗365)=69年
4) 10位,用来记录工作机器id。可以部署在2^10=1024个节点,包括5位datacenterId和5位workerId
    5位(bit)可以表示的最大正整数是2^5−1=31,即可以用0、1、2、3、....31这32个数字,来表示不同的datecenterId或workerId
6) 12位,序列号,用来记录同毫秒内产生的不同id。
   12位(bit)可以表示的最大正整数是2^12−1=4095,即可以用0、1、2、3、....4094这4095个数字,来表示同一机器同一时间截(毫秒)内产生的4095个ID序号
由于在Java中64bit的整数是long类型,所以在Java中SnowFlake算法生成的id就是long来存储的。

看到雪花算法的组成部分,我第一感觉就是可以在这【10bit-工作机器id】上做文章;
原因:

  • 1bit符号位是不能动的
  • 41bit时间戳我也不太敢动
  • 最后的12bit序列号讲道理是可以动的,因为我们系统没可能达到单位时间需要生成2^12-1个id
  • 最重要的是我能确定我们系统不需要5bit来区别机房,用5bit来区别机器

我考虑先拿两个bit来作为自定义标志位,即可以标记2^2=4种情况;

话不多说,我先贴出原本的雪花算法生成的代码:

@Slf4j
public class IdWorker {
    // 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)
    private final static long twepoch = 1288834975687L;
    // 机器标识位数
    private final static long workerIdBits = 5L;
    // 数据中心标识位数
    private final static long datacenterIdBits = 5L;
    // 机器ID最大值
    private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);
    // 数据中心ID最大值
    private final static long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
    // 毫秒内自增位
    private final static long sequenceBits = 12L;
    // 机器ID偏左移12位
    private final static long workerIdShift = sequenceBits;
    // 数据中心ID左移17位
    private final static long datacenterIdShift = sequenceBits + workerIdBits;
    // 时间毫秒左移22位
    private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
    /* 上次生产id时间戳 */
    private static long lastTimestamp = -1L;
    // 0,并发控制
    private long sequence = 0L;

    private final long workerId;
    // 数据标识id部分
    private final long datacenterId;

    public IdWorker(){
        this.datacenterId = getDatacenterId(maxDatacenterId);
        this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);
    }
    /**
     * @param workerId
     *            工作机器ID
     * @param datacenterId
     *            序列号
     */
    public IdWorker(long workerId, long datacenterId) {
        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));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }
    /**
     * 获取下一个ID
     * @return
     */
    public synchronized long nextId() {
        long timestamp = timeGen();
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }
        if (lastTimestamp == timestamp) {
            // 当前毫秒内,则+1
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                // 当前毫秒内计数满了,则等待下一秒
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }
        lastTimestamp = timestamp;
        // ID偏移组合生成最终的ID,并返回ID
        long nextId = ((timestamp - twepoch) << timestampLeftShift)
                | (datacenterId << datacenterIdShift)
                | (workerId << workerIdShift) | sequence;
        return nextId;
    }


    private long tilNextMillis(final long lastTimestamp) {
        long timestamp = this.timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = this.timeGen();
        }
        return timestamp;
    }

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

    /**
     * <p>
     * 获取 maxWorkerId
     * </p>
     */
    protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) {
        StringBuffer mpid = new StringBuffer();
        mpid.append(datacenterId);
        String name = ManagementFactory.getRuntimeMXBean().getName();
        if (!name.isEmpty()) {
         /*
          * GET jvmPid
          */
            mpid.append(name.split("@")[0]);
        }
      /*
       * MAC + PID 的 hashcode 获取16个低位
       */
        return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
    }

    /**
     * <p>
     * 数据标识id部分
     * </p>
     */
    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();
                id = ((0x000000FF & (long) mac[mac.length - 1])
                        | (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
                id = id % (maxDatacenterId + 1);
            }
        } catch (Exception e) {
            log.info(" getDatacenterId: " + e.getMessage());
        }
        return id;
    }
}

上面的雪花算法的java实现相信大家在网上一搜一大把;
那么重点来了,我们如何在雪花算法中做点手脚,起到标记的作用呢???
我的计划是这样的,在下图的这两个bit上做标记
在这里插入图片描述

重点解析nextId()方法
    public synchronized long nextId() {
        long timestamp = timeGen();
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }
        if (lastTimestamp == timestamp) {
            // 当前毫秒内,则+1
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                // 当前毫秒内计数满了,则等待下一秒
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }
        lastTimestamp = timestamp;
        // ID偏移组合生成最终的ID,并返回ID
        long nextId = ((timestamp - twepoch) << timestampLeftShift)   <<<---------------
                | (datacenterId << datacenterIdShift)
                | (workerId << workerIdShift) | sequence;
        return nextId;
    }

重点关注一下返回前组合nextId的那个位移操作

			// (41bit)时间毫秒左移22位
long nextId = ((timestamp - twepoch) << timestampLeftShift)   
			// (5bit-->4bit)数据中心ID左移(17位-->18位)
                | (datacenterId << datacenterIdShift)
            // (5bit-->4bit)机器ID偏左移(12位--14位>),空出2位
                | (workerId << workerIdShift) 
    //(2bit标志位)偏左移12位    
           		| (USER << sequenceBits)  //user的标志位     
                | sequence; //最后12位序列号无需移动

整个nextId的组成使用了向左位移 << ** 和位或 | **两种位操作
而改造的方法就是腾出两个位,并放入我们自己的位标志

    /**
     * 预留了2个位,即可作为 00 | 01 | 10 | 11 四种状态
     * USER    :     用户类型的标志位
     * TEAM    :    团队类型的标志位
     **/
    private static final long USER   =  0 ; //----------------> 00
    private static final long TEAM   =  1 ; //----------------> 01

行文至此,我们做手脚算是做完了
但,做完手脚,我们自己怎么去解析标志位呢???

解析标志位

    /**
     * 通过自定义标志位解析出ID的类型
     * @return 标志位的数值 
     */
    public long parseTypeFromId(Long id){
        if (null == id || id <0)
            throw new BusinessException(StatusCode.PARAM_HAS_ERROR,"snowflake id is invalid!");
        long status = (id & (0x00003000)) >>sequenceBits;
        return status;
    }

这里先将id和0x00003000这个十六进制的数进行位与运算,在进行一个右位移运算,前面的逆过程;
示例如下:

00010001111111101110010010010111  01110100  00000100  00010000  00000101  <---id
00000000000000000000000000000000  00000000  00000000  00110000  00000000  <---0x00003000
假设id中第13位是1,第十四位是0,那么经过位与和位右移最后得到的是
00000000000000000000000000000000  00000000  00000000  00000000  00000001  <---结果

和我们定义的TEAM=1相同,那么我们就能判断这个id属于team表的id;

3. 总结

经过改造雪花算法,我们在其中做下手脚即加入自定义的标志位,使用时通过进过改造的nextId方法生成id,解析的时候通过位运算这种极其快速的操作读取出标志位,巧妙得玩出了新花样!!!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值