分布式ID方案SnowFlake雪花算法分析


1、算法

SnowFlake算法生成的数据组成结构如下:

在java中用long类型标识,共64位(每部分用-分开):
0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 0000000000 00

  • 1位标识,0表示正数。
  • 41位时间戳,当前时间的毫秒减去开始时间的毫秒数。可用 (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年。
  • 5位数据中心标识,可支持(1L << 5) = 32个数据中心。
  • 5位机器标识,每个数据中心可支持(1L << 5) = 32个机器标识。
  • 12位序列号,每个节点每一毫秒支持(1L << 12) = 4096个序列号。

2、Java版本实现


/**
 * 雪花算法<br>
 * 在java中用long类型标识,共64位(每部分用-分开):<br>
 * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 0000000000 00<br>
 * 1位标识,0表示正数。<br>
 * 41位时间戳,当前时间的毫秒减去开始时间的毫秒数。可用 (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年。<br>
 * 5位数据中心标识,可支持(1L << 5) = 32个数据中心。<br>
 * 5位机器标识,每个数据中心可支持(1L << 5) = 32个机器标识。<br>
 * 12位序列号,每个节点每一毫秒支持(1L << 12) = 4096个序列号。<br>
 */
public class SnowflakeIdWorker {
    /**
     * 机器标识
     */
    private long workerId;
    /**
     * 数据中心标识
     */
    private long dataCenterId;
    /**
     * 序列号
     */
    private long sequence;
    /**
     * 机器标识占用5位
     */
    private long workerIdBits = 5L;
    /**
     * 数据中心标识占用5位
     */
    private long dataCenterIdBits = 5L;
    /**
     * 12位序列号
     */
    private long sequenceBits = 12L;

    /**
     * 12位序列号支持的最大正整数
     * ....... 00001111 11111111
     * 2^12-1 = 4095
     */
    private long sequenceMask = ~(-1L << sequenceBits);

    /**
     * The Worker id shift.
     * 12位
     */
    private long workerIdShift = sequenceBits;
    /**
     * The Data center id shift.
     * 12 + 5 = 17位
     */
    private long dataCenterIdShift = sequenceBits + workerIdBits;
    /**
     * The Timestamp shift.
     * 12 + 5 + 5 = 22位
     */
    private long timestampShift = sequenceBits + workerIdBits + dataCenterIdBits;
    /**
     * 开始时间戳毫秒
     */
    private long startEpoch = 29055616000L;
    /**
     * The Last timestamp.
     */
    private long lastTimestamp = -1L;

    public SnowflakeIdWorker(long workerId, long dataCenterId, long sequence) {
        // 检查workerId是否正常
        /*
          机器标识最多支持的最大正整数
          -1的补码:
          11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111
          -1 左移 5 位,高位溢出,低位补0:
          11111111 11111111 11111111 11111111 11111111 11111111 11111111 11100000
          取反:
          00000000 00000000 00000000 00000000 00000000 00000000 00000000 00011111
          转10进制:
          16 + 8 + 4 + 2 + 1 = 31
         */
        long maxWorkerId = ~(-1L << workerIdBits);
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("工作Id不能大于%d或小于0", maxWorkerId));
        }

        /*
          数据中心最多支持的最大正整数31
         */
        long maxDataCenterId = ~(-1L << dataCenterIdBits);
        if (dataCenterId > maxDataCenterId || dataCenterId < 0) {
            throw new IllegalArgumentException(String.format("数据中心Id不能大于%d或小于0", maxDataCenterId));
        }

        this.workerId = workerId;
        this.dataCenterId = dataCenterId;
        this.sequence = sequence;
    }

    private synchronized long nextId() {
        //获取当前时间毫秒数
        long timestamp = timeGen();

        //如果当前时间毫秒数小于上一次的时间戳
        if (timestamp < lastTimestamp) {
            System.err.printf("时钟发生回调,拒绝生成ID,直到: %d.", lastTimestamp);
            throw new RuntimeException(String.format("时钟发生回调,  拒绝为 %d 毫秒生成ID。",
                    lastTimestamp - timestamp));
        }

        //当前时间毫秒数与上次时间戳相同,增加序列号
        if (lastTimestamp == timestamp) {
            //假设sequence=4095
            //(4095 + 1) & 4095
            //4096:  ....... 00010000 00000000
            //4095:  ....... 00001111 11111111
            //       ....... 00000000 00000000
            //最终sequence为0,即sequence发生溢出。
            sequence = (sequence + 1) & sequenceMask;
            //如果发生序列号为0,即当前毫秒数的序列号已经溢出,则借用下一毫秒的时间戳
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            //当前毫秒数大于上次的时间戳,序列号为0
            sequence = 0;
        }

        //更新
        lastTimestamp = timestamp;

        //生成ID算法,左移几位,则后面加几个0。
        //1、当前时间的毫秒数-开始时间的毫秒数,结果左移22位
        // 假设:timestamp - startEpoch = 1
        // 二进制:
        // 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
        // 左移22位:
        // 00000000 00000000 00000000 00000000 00000000 01000000 00000000 00000000
        //2、dataCenterId左移17位
        // 假设:dataCenterId = 1
        // 二进制:
        // 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
        // 左移17位:
        // 00000000 00000000 00000000 00000000 00000000 00000010 00000000 00000000

        //3、workerId左移12位
        // 假设:workerId = 1
        // 二进制:
        // 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
        // 左移12位:
        // 00000000 00000000 00000000 00000000 00000000 00000000 00010000 00000000
        //4、最后的所有结果按位`或`
        //假设:sequence = 1
        //00000000 00000000 00000000 00000000 00000000 01000000 00000000 00000000
        //00000000 00000000 00000000 00000000 00000000 00000010 00000000 00000000
        //00000000 00000000 00000000 00000000 00000000 00000000 00010000 00000000
        //00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
        //00000000 00000000 00000000 00000000 00000000 01000010 00010000 00000001
        //结果: 0 - 0000000 00000000 00000000 00000000 00000000 01 - 00001 - 00001 - 0000 00000001

        return ((timestamp - startEpoch) << timestampShift) |
                (dataCenterId << dataCenterIdShift) |
                (workerId << workerIdShift) |
                sequence;
    }

    /**
     * 获取下一秒
     *
     * @param lastTimestamp the last timestamp
     * @return the long
     */
    private long tilNextMillis(long lastTimestamp) {
        //获取当前毫秒数
        long timestamp = timeGen();
        //只要当前的毫秒数小于上次的时间戳,就一直循环,大于上次时间戳
        while (timestamp <= lastTimestamp) {
            //获取当前毫秒数
            timestamp = timeGen();
        }
        return timestamp;
    }

    /**
     * 获取当前毫秒数
     *
     * @return the long
     */
    private long timeGen() {
        return System.currentTimeMillis();
    }

    public static void main(String[] args) {
        SnowflakeIdWorker worker = new SnowflakeIdWorker(1, 1, 1);
        for (int i = 0; i < 10000; i++) {
            long id = worker.nextId();
            System.out.println(id);
            System.out.println(Long.toString(id).length());
            System.out.println(Long.toBinaryString(id));
            System.out.println(Long.toBinaryString(id).length());
        }
    }

}

3、难点

Tips: 左移几位,则后面加几个0。

3.1、计算机器标识最多支持的最大正整数

private long workerIdBits = 5L;
long maxWorkerId = ~(-1L << workerIdBits);

计算过程:

  • -1的补码:

    11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111

  • -1 左移 5 位,高位溢出,低位补0:

    11111111 11111111 11111111 11111111 11111111 11111111 11111111 11100000

  • 取反:

    00000000 00000000 00000000 00000000 00000000 00000000 00000000 00011111

  • 转10进制:

    16 + 8 + 4 + 2 + 1 = 31

3.2、sequence溢出处理

sequence = (sequence + 1) & sequenceMask;

计算过程:

假设sequence=4095:

  • (4095 + 1) & 4095
  • 4096: ....... 00010000 00000000
  • 4095: ....... 00001111 11111111
  • 按位与 ....... 00000000 00000000
  • 最终sequence为0,即sequence发生溢出。

3.3、ID计算

((timestamp - startEpoch) << timestampShift) |
(dataCenterId << dataCenterIdShift) |
(workerId << workerIdShift) |
sequence

计算过程:

  • 当前时间的毫秒数-开始时间的毫秒数,结果左移22位

假设:timestamp - startEpoch = 1

二进制: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

左移22位: 00000000 00000000 00000000 00000000 00000000 01000000 00000000 00000000

  • dataCenterId左移17位

假设:dataCenterId = 1

二进制: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

左移17位: 00000000 00000000 00000000 00000000 00000000 00000010 00000000 00000000

  • workerId左移12位

假设:workerId = 1

二进制: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

左移12位: 00000000 00000000 00000000 00000000 00000000 00000000 00010000 00000000

  • 最后的所有结果按位

假设:sequence = 1

00000000 00000000 00000000 00000000 00000000 01000000 00000000 00000000

00000000 00000000 00000000 00000000 00000000 00000010 00000000 00000000

00000000 00000000 00000000 00000000 00000000 00000000 00010000 00000000

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

--------------------------------------------------------------------------
00000000 00000000 00000000 00000000 00000000 01000010 00010000 00000001

  • 结果:

0 - 0000000 00000000 00000000 00000000 00000000 01 - 00001 - 00001 - 0000 00000001

符合SnowFlake算法数据组成结构。

参考

理解分布式id生成算法SnowFlake
Twitter雪花算法SnowFlake算法的java实现



原文地址:https://www.cnblogs.com/clawhub/p/11991891.html

时间: 2024-08-30 05:07:24

分布式ID方案SnowFlake雪花算法分析的相关文章

自增ID算法snowflake(雪花)

在数据库主键设计上,比较常见的方法是采用自增ID(1开始,每次加1)和生成GUID.生成GUID的方式虽然简单,但是由于采用的是无意义的字符串,推测会在数据量增大时造成访问过慢,在基础互联网的系统设计中都不推荐采用.自增ID的方法虽然比较适合大数据量的场景,当时由于自增ID是按照顺序增加的,数据记录都是可以根据ID号进行推测出来,对于一些数据敏感的场景,不建议采用 最近在一篇文章中看到P2P网站处理订单流水号的思路还不错.该平台设计时希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成

分布式ID解决方案

开发十年,就只剩下这套Java开发体系了 >>>    在游戏开发中,我们使用分布式ID.有很多优点 便于合服 便于ID管理 等等 一.单服各自ID系统的弊端 1. 列如合服 在游戏上线后,合服是避免不了的事情.如果按照传统的数据库表自增来作为数据的唯一ID.或者每个游戏中是相同的自增.这样在合服的时候你就会相当的麻烦了. 比如上图我们要把GameServer_2的数据合并到GameServer_1中,它们都有一个Player ID = 1的玩家,这个时候你就必须要重建GameServe

snowflake 雪花算法 分布式实现全局id生成

snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID. 这种方案大致来说是一种以划分命名空间(UUID也算,由于比较常见,所以单独分析)来生成ID的一种算法,这种方案把64-bit分别划分成多段,分开来标示机器.时间等. 其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号,最后还有一个符号位,永远是0. 比如在snowflake中的64-bit分别表示如下图(图片来自网络)

我爱java系列之---【分布式ID生成解决方案:UUID、Redis生成id、snowflake】

唯一id生成方案: a. 使用UUID生成唯一主键: 优点: 全局唯一. 缺点: 因为生成的内容是字符串, 不能排序, 不能按照时间先后排序,因为生成的是字符串类型的id, 可读性差. b. 使用redis来生成全局唯一主键: 优点: redis是内存操作, 速度快, 生成的是数字, 可读性好, 并且可以按照生成的时间先后排序. 缺点: 如果整个系统没有用到redis技术, 那么这里使用redis会增加系统的技术复杂度.   应用服务器到redis服务器获取唯一id, 增加网络io. c. sn

分布式唯一id:snowflake算法思考

匠心零度 转载请注明原创出处,谢谢! 缘起 为什么会突然谈到分布式唯一id呢?原因是最近在准备使用RocketMQ,看看官网介绍: 一句话,消息可能会重复,所以消费端需要做幂等.为什么消息会重复后续RocketMQ章节进行详细介绍,本节重点不在这里. 为了达到业务的幂等,必须要有这样一个id存在,需要满足下面几个条件: 同一业务场景要全局唯一. 该id必须是在消息的发送方进行产生发送到MQ. 消费端根据该id进行判断是否重复,确保幂等. 在那里产生,和消费端进行判断等和这个id没有关系,这个id

搞懂分布式技术12:分布式ID生成方案

搞懂分布式技术12:分布式ID生成方案 ## 转自: 58沈剑 架构师之路 2017-06-25 一.需求缘起 几乎所有的业务系统,都有生成一个唯一记录标识的需求,例如: 消息标识:message-id 订单标识:order-id 帖子标识:tiezi-id 这个记录标识往往就是数据库中的主键,数据库上会建立聚集索引(cluster index),即在物理存储上以这个字段排序. 这个记录标识上的查询,往往又有分页或者排序的业务需求,例如: 拉取最新的一页消息 select message-id/

分布式唯一ID生成算法-雪花算法

在我们的工作中,数据库某些表的字段会用到唯一的,趋势递增的订单编号,我们将介绍两种方法,一种是传统的采用随机数生成的方式,另外一种是采用当前比较流行的“分布式唯一ID生成算法-雪花算法”来实现. 一.时间戳随机数生成唯一ID 我们写一个for循环,用RandomUtil.generateOrderCode()生成1000个唯一ID,执行结果我们会发现出现重复的ID. /** * 随机数生成util **/ public class RandomUtil { private static fina

分布式ID生成方案

分布式ID生成方案(分布式数据库) 背景:在互联网应用中,应用需要为每一个用户分配一个id,在使用分布式数据库情况下,已经不能依靠自增主键来生成唯一性id了... 根据特定算法生成唯一ID 可重现的id生成方案:使用用户提供的特定的数据源(登录凭证),通过某种算法生成id,这个过程是可重现的,只要用户提供的数据源是唯一的,那么生成的id也是唯一的. 例如通过用户注册的email+salt,使用摘要算法(md5/sha)生成128bit的数据,然后通过混合因子转变为一个long类型的数据是64bi

一种基于Orleans的分布式Id生成方案

基于Orleans的分布式Id生成方案,因Orleans的单实例.单线程模型,让这种实现变的简单,贴出一种实现,欢迎大家提出意见 public interface ISequenceNoGenerator : Orleans.IGrainWithIntegerKey { Task<Immutable<string>> GetNext(); } public class SequenceNoGenerator : Orleans.Grain, ISequenceNoGenerator