什么是雪花算法?简述一下雪花算法的原理?

什么是雪花算法?

雪花算法(Snowflake Algorithm)是一种用于生成分布式系统中唯一ID的算法。起初由Twitter设计,用于解决分布式系统中唯一ID的需求。

这一算法的目标是生成全局唯一、有序的64位整数ID,以确保数据不冲突、不重复。

雪花算法生成的ID由以下部分组成:

1) 41位时间戳:精确到毫秒级,记录ID生成的时间。
2) 10位机器ID:用于标识不同机器或节点,可自定义分配。
3) 12位序列号:解决同一毫秒内生成多个ID的冲突问题。

图示:

 

雪花算法的原理

1.获取当前时间戳的41位,并将其左移22位,以获取时间戳部分。
2.获取机器ID,并将其左移12位,以获取机器ID部分。
3.生成序列号,同一毫秒内生成多个ID时,序列号递增。
4.合并时间戳、机器ID和序列号,形成64位整数ID。

手写java中的实现:

public class MySnowflakeIdGenerator {
    // ==============================字段==============================
    private final long workerId;          // 机器ID
    private final long datacenterId;      // 数据中心ID
    private long sequence = 0L;           // 序列号

    // 配置参数
    private static final long MAX_WORKER_ID = 31L;
    private static final long MAX_DATACENTER_ID = 31L;
    private static final long WORKER_ID_BITS = 5L;
    private static final long DATACENTER_ID_BITS = 5L;
    private static final long SEQUENCE_BITS = 12L;

    // 位移偏移量
    private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;
    private static final long WORKER_ID_SHIFT = SEQUENCE_BITS + DATACENTER_ID_BITS;
    private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS;

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

    // ==============================构造函数==============================
    /**
     * 构造函数
     * @param workerId     机器ID (0 - 31)
     * @param datacenterId 数据中心ID (0 - 31)
     */
    public MySnowflakeIdGenerator(long workerId, long datacenterId) {
        // 检查workerId和datacenterId是否合法
        if (workerId > MAX_WORKER_ID || workerId < 0) {
            throw new IllegalArgumentException("Worker ID must be between 0 and " + MAX_WORKER_ID);
        }
        if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
            throw new IllegalArgumentException("Datacenter ID must be between 0 and " + MAX_DATACENTER_ID);
        }

        // 设置workerId和datacenterId
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    // ==============================方法==============================
    /**
     * 生成唯一ID
     * @return 唯一ID
     */
    public synchronized long generateUniqueId() {
        long timestamp = getCurrentTimestamp();

        // 检查时钟回退情况
        if (timestamp < lastTimestamp) {
            throw new RuntimeException("Clock moved backwards. Refusing to generate ID.");
        }

        // 同一毫秒内自增序列号
        if (timestamp == lastTimestamp) {
            sequence = (sequence + 1) & ((1 << SEQUENCE_BITS) - 1);  // 自增并掩码
            if (sequence == 0) {
                // 序列号用完,等待下一毫秒
                timestamp = getNextTimestamp(lastTimestamp);
            }
        } else {
            sequence = 0L;  // 不同毫秒重置序列号
        }

        lastTimestamp = timestamp;

        // 组合生成唯一ID
        return ((timestamp << TIMESTAMP_SHIFT) |
                (datacenterId << DATACENTER_ID_SHIFT) |
                (workerId << WORKER_ID_SHIFT) |
                sequence);
    }

    /**
     * 获取当前时间戳
     * @return 当前时间戳(毫秒)
     */
    private long getCurrentTimestamp() {
        return System.currentTimeMillis();
    }

    /**
     * 等待下一毫秒,直到获得新的时间戳
     * @param lastTimestamp 上次生成ID的时间戳
     * @return 新的时间戳
     */
    private long getNextTimestamp(long lastTimestamp) {
        long timestamp = getCurrentTimestamp();
        while (timestamp <= lastTimestamp) {
            timestamp = getCurrentTimestamp();
        }
        return timestamp;
    }

}

尝试调用:

public class TestCode {
    // ==============================测试==============================
    public static void main(String[] args) {
        // 创建一个雪花算法生成器,传入机器ID和数据中心ID
        MySnowflakeIdGenerator idGenerator = new MySnowflakeIdGenerator(0, 0);
        // 生成10个唯一ID并打印
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                long uniqueId = idGenerator.generateUniqueId();
                System.out.println("Generated Unique ID: " + uniqueId);
            }).start();
        }
    }
}

 

源码中的代码示例

[该源码是第三方包 idworker 的源码,原本打算是看mybatis-puls里面的,但是发现就是用的第三方包,所以就直接看第三方包的源码吧]:

package com.imadcn.framework.idworker.algorithm;

import java.util.Random;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Snowflake的结构如下(每部分用-分开): <br>
 * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
 * <b> · </b>1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0 <br>
 * <b> · </b>41位时间戳(毫秒级),注意,41位时间戳不是存储当前时间的时间戳,而是存储时间戳的差值(当前时间戳 -
 * 开始时间戳)得到的值),这里的的开始时间戳,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序epoch属性)。41位的时间戳,可以使用69年 <br>
 * <b> · </b>10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId <br>
 * <b> · </b>12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间戳)产生4096个ID序号 加起来刚好64位,为一个Long型。
 * <p>
 * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
 * <p>
 * 注意这里进行了小改动: <br>
 * <b> · </b>Snowflake是5位的datacenter加5位的机器id; 这里变成使用10位的机器id (b) <br>
 * <b> · </b>对系统时间的依赖性非常强,需关闭ntp的时间同步功能。当检测到ntp时间调整后,将会拒绝分配id
 * 
 * @author imadcn
 * @since 1.0.0
 */
public class Snowflake {

    private static final Logger logger = LoggerFactory.getLogger(Snowflake.class);

    /**
     * 机器ID
     */
    private final long workerId;
    /**
     * 时间起始标记点,作为基准,一般取系统的最近时间,默认2017-01-01
     */
    private final long epoch = 1483200000000L;
    /**
     * 机器id所占的位数(源设计为5位,这里取消dataCenterId,采用10位,既1024台)
     */
    private final long workerIdBits = 10L;
    /**
     * 机器ID最大值: 1023 (从0开始)
     */
    private final long maxWorkerId = -1L ^ -1L << workerIdBits;
    /**
     * 序列在id中占的位数
     */
    private final long sequenceBits = 12L;
    /**
     * 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095),12位
     */
    private final long sequenceMask = -1L ^ -1L << sequenceBits;
    /**
     * 机器ID向左移12位
     */
    private final long workerIdShift = sequenceBits;
    /**
     * 时间戳向左移22位(5+5+12)
     */
    private final long timestampLeftShift = sequenceBits + workerIdBits;
    /**
     * 并发控制,毫秒内序列(0~4095)
     */
    private long sequence = 0L;
    /**
     * 上次生成ID的时间戳
     */
    private long lastTimestamp = -1L;
    /**
     * 100,000
     */
    private final int HUNDRED_K = 100_000;
    /**
     * sequence随机种子(兼容低并发下,sequence均为0的情况)
     */
    private static final Random RANDOM = new Random();

    /**
     * @param workerId 机器Id
     */
    private Snowflake(long workerId) {
        if (workerId > maxWorkerId || workerId < 0) {
            String message = String.format("worker Id can't be greater than %d or less than 0", maxWorkerId);
            throw new IllegalArgumentException(message);
        }
        this.workerId = workerId;
    }

    /**
     * Snowflake Builder
     * 
     * @param workerId 机器Id
     * @return Snowflake Instance
     */
    public static Snowflake create(long workerId) {
        return new Snowflake(workerId);
    }

    /**
     * Snowflake Builder
     * 
     * @param workerId 机器Id
     * @param lowConcurrency 是否低并发模式
     * @return Snowflake Instance
     */
    @Deprecated
    public static Snowflake create(long workerId, boolean lowConcurrency) {
        return create(workerId);
    }

    /**
     * 批量获取ID
     * 
     * @param size 获取大小,最多10万个
     * @return SnowflakeId
     */
    public long[] nextId(int size) {
        if (size <= 0 || size > HUNDRED_K) {
            String message = String.format("Size can't be greater than %d or less than 0", HUNDRED_K);
            throw new IllegalArgumentException(message);
        }
        long[] ids = new long[size];
        for (int i = 0; i < size; i++) {
            ids[i] = nextId();
        }
        return ids;
    }

    /**
     * 获得ID
     * 
     * @return SnowflakeId
     */
    public synchronized long nextId() {
        long timestamp = timeGen();
        // 如果上一个timestamp与新产生的相等,则sequence加一(0-4095循环);
        if (lastTimestamp == timestamp) {
            // 对新的timestamp,sequence从0开始
            sequence = sequence + 1 & sequenceMask;
            // 毫秒内序列溢出
            if (sequence == 0) {
                // 阻塞到下一个毫秒,获得新的时间戳
                sequence = RANDOM.nextInt(100);
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            // 时间戳改变,毫秒内序列重置
            sequence = RANDOM.nextInt(100);
        }
        // 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
        if (timestamp < lastTimestamp) {
            String message = String.format("Clock moved backwards. Refusing to generate id for %d milliseconds.",
                (lastTimestamp - timestamp));
            logger.error(message);
            throw new RuntimeException(message);
        }
        lastTimestamp = timestamp;
        // 移位并通过或运算拼到一起组成64位的ID
        return timestamp - epoch << timestampLeftShift | workerId << workerIdShift | sequence;
    }

    /**
     * 等待下一个毫秒的到来, 保证返回的毫秒数在参数lastTimestamp之后
     * 
     * @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();
    }

}

 

posted @ 2025-04-28 17:53  忧愁的chafry  阅读(259)  评论(0)    收藏  举报