侧边栏壁纸
  • 累计撰写 251 篇文章
  • 累计创建 138 个标签
  • 累计收到 16 条评论

目 录CONTENT

文章目录

雪花算法(snowflake)

Sherlock
2018-10-29 / 0 评论 / 0 点赞 / 5412 阅读 / 7054 字 / 编辑
温馨提示:
本文最后更新于 2023-10-09,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

简单描述

  • 最高位是符号位,始终为0,不可用。
  • 41位的时间序列,精确到毫秒级,41位的长度可以使用69年。时间位还有一个很重要的作用是可以根据时间进行排序。
  • 10位的机器标识,10位的长度最多支持部署1024个节点。
  • 12位的计数序列号,序列号即一系列的自增id,可以支持同一节点同一毫秒生成多个ID序号,12位的计数序列号支持每个节点每毫秒产生4096个ID序号。

这个算法很简洁,但依旧是一个很好的ID生成策略。其中,10位器标识符一般是5位IDC+5位machine编号,唯一确定一台机器。

收集的一段网友的代码(时间序列被改为了42位)

package snowflake;

import org.apache.commons.lang3.StringUtils;

import java.lang.management.ManagementFactory;  
import java.lang.management.RuntimeMXBean;  
import java.net.NetworkInterface;  
import java.net.SocketException;  
import java.util.Enumeration;

/**
 * 原始雪花算法简单描述: 
    + 最高位是符号位,始终为0,不可用。 
    + 41位的时间序列,精确到毫秒级,41位的长度可以使用69年。时间位还有一个很重要的作用是可以根据时间进行排序。 
    + 10位的机器标识,10位的长度最多支持部署1024个节点。 
    + 12位的计数序列号,序列号即一系列的自增id,可以支持同一节点同一毫秒生成多个ID序号,12位的计数序列号支持每个节点每毫秒产生4096个ID序号。
 */

/**
 * 雪花算法 (有小改动) 主键生成器
 * 可参考:https://blog.csdn.net/linghuanxu/article/details/78896317
 *
 * 整个ID的构成大概分为这么几个部分: 时间戳差值,机器编码,进程编码,序列号。 java的long是64位的,从左向右依次介绍是:
 * 时间戳差值,在我们这里占了42位(原算法占了41位); 机器编码5位; 进程编码5位; 序列号12位。
 * 所有的拼接用位运算拼接起来,于是就基本做到了每个进程中不会重复了。
 *
 * 整体设计
 * 为了最大程度的减少配置,方便实用,这个模块,我设计成了单例模式。之所以没有直接使用static方法,还是希望可以控制整个模块的生命周期,但是,模块的初始化,我使用了static块,因为它没有任何依赖。
 * 有个static的nextId方法,可以直接获得下一个ID,这个方法是线程安全的。同时这个模块的使用就是这么简单粗暴,也不用配置bean。
 *
 * 其实,这个方案中,机器码和进程编码是可能相同的,只是概率比较小,就凑合着用吧。
 */
public class SnowflakeKeyWorker {  
    /**
     * 起始的时间戳 作者写代码的时间戳(位数与实际时间戳不同,原始算法中时间戳占41位,该算法占42位。。)
     */
    private static final long TWEPOCH = 12888349746579L;

    // 机器标识位数
    private static final long WORKERIDBITS = 5L;
    // 数据中心标识位数
    private static final long DATACENTERIDBITS = 5L;

    // 毫秒内自增位数
    private static final long SEQUENCEBITS = 12L;
    // 机器ID偏左移12位
    private static final long WORKERIDSHIFT = SEQUENCEBITS;
    // 数据中心ID左移17位
    private static final long DATACENTERIDSHIFT = SEQUENCEBITS + WORKERIDBITS;
    // 时间毫秒左移22位
    private static final long TIMESTAMPLEFTSHIFT = SEQUENCEBITS + WORKERIDBITS + DATACENTERIDBITS;
    // sequence掩码,确保sequnce不会超出上限
    private static final long SEQUENCEMASK = -1L ^ (-1L << SEQUENCEBITS);
    // 上次时间戳
    private static long lastTimestamp = -1L;
    // 序列
    private long sequence = 0L;
    // 服务器ID
    private long workerId = 1L;

    /**
     * 如何确保不超出位数限制,我们拿workerId举个例子:this.workerId=workerId & workerMask;
     */
    /**
     * workerMask 是啥 ??? 它先执行的是-1L << workerIdBits,workerIdBits是5。这又是什么意思呢?
     * 注意,这是位运算,long用的是补码, -1L,就是64个1,这里使用-1是为了格式化所有位数 ===>
     * 11111...(省略54个1)...11111
     * <<是左移运算,-1L左移五位,低位补零,也就是左移空出来的会自动补0,于是就低位五位是0,其余是1。 ===>
     * 11111...(省略54个1)...00000
     * 然后^这个符号,是异或,也是位运算,位上相同则为0,不通则为1,和-1做异或,则把所有的0和1颠倒了一下。 ===>
     * 00000...(省略54个0)...11111
     * 
     * 这时候,我们再看,workerId & workerMask,与操作,两个位上都为1的才能唯一,否则为零,
     * workerMask高位都是0,所以,不管workerId高位是什么,都是0;
     * 而workerMask低位都是1,所以,不管workerId低位是什么,都会被保留,于是,我们就控制了workerId的范围。
     */
    private static long workerMask = -1L ^ (-1L << WORKERIDBITS);
    // 进程编码
    private long processId = 1L;
    private static long processMask = -1L ^ (-1L << DATACENTERIDBITS);
    private static SnowflakeKeyWorker keyWorker = null;

    static {
        keyWorker = new SnowflakeKeyWorker();
    }

    public static synchronized long nextId() {
        return keyWorker.getNextId();
    }

    private SnowflakeKeyWorker() {

        // 获取机器编码
        this.workerId = this.getMachineNum();
        // 获取进程编码
        RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
        this.processId = Long.valueOf(runtimeMXBean.getName().split("@")[0]).longValue();

        // 避免编码超出最大值
        this.workerId = workerId & workerMask;
        this.processId = processId & processMask;
    }

    // 最后的异常
    /**
     * 这里,时间戳,保证了不通毫秒不同,然后机器编码进程编码保证了不同进程不通,
     * 再然后,序列,在同一毫秒内,如果获取第二个ID,则序列号+1,到下一毫秒后重置。至此,唯一性ok。
     * 
     * 但是,还有问题,序列号用完了怎么办?代码里的解决方案是,等到下一毫秒。
     */
    public synchronized long getNextId() {
        // 获取时间戳
        long timestamp = timeGen();
        // 如果时间戳小于上次时间戳则报错
        if (timestamp < lastTimestamp) {
            try {
                throw new Exception("Clock moved backwards.  Refusing to generate id for " + (lastTimestamp - timestamp)
                        + " milliseconds");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        // 如果时间戳与上次时间戳相同
        if (lastTimestamp == timestamp) {
            // 当前毫秒内,则+1,与sequenceMask确保sequence不会超出上限
            sequence = (sequence + 1) & SEQUENCEMASK;
            if (sequence == 0) {
                // 当前毫秒内计数满了,则等待下一秒
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0;
        }
        lastTimestamp = timestamp;
        // ID偏移组合生成最终的ID,并返回ID(原始算法中时间戳占41位,该算法占42位。。)
        return ((timestamp - TWEPOCH) << TIMESTAMPLEFTSHIFT) | (processId << DATACENTERIDSHIFT)
                | (workerId << WORKERIDSHIFT) | sequence;
    }

    /**
     * 再次获取时间戳直到获取的时间戳与现有的不同
     * 
     * @param lastTimestamp
     * @return 下一个时间戳
     */
    private long tilNextMillis(final long lastTimestamp) {
        long timestamp = this.timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = this.timeGen();
        }
        return timestamp;
    }

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

    /**
     * 获取机器编码
     */
    private long getMachineNum() {
        long machinePiece;
        StringBuilder sb = new StringBuilder();
        Enumeration<NetworkInterface> e = null;
        try {
            e = NetworkInterface.getNetworkInterfaces();
        } catch (SocketException e1) {
            e1.printStackTrace();
        }
        while (e.hasMoreElements()) {
            NetworkInterface ni = e.nextElement();
            sb.append(ni.toString());
        }
        machinePiece = sb.toString().hashCode();
        return machinePiece;
    }

    public static void main(String[] args) {
        System.out.println(System.currentTimeMillis());
    }

    // 长整型转二进制字符串(高位补0到64位)
    private static String long2BinaryStr(Long l) {
        return StringUtils.leftPad(Long.toBinaryString(l), 64, '0');
    }
}
0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区