分布式ID概述和雪花算法snowflake详解

1. 背景

1.1 起源

它最早是twitter内部使用的分布式环境下的唯一ID生成算法。在2014年开源。开源的版本由scala编写,大家可以再找个地址找到这版本。可以查看github仓库:
https://github.com/twitter-archive/snowflake/tags

1.2 有何作用?

相信大家曾经有过疑问:为什么需要雪花算法,它在真正的业务系统中能解决什么问题?

在一般的中小型系统中,由于数据量不大,单库单表即可满足业务需求,而每条数据都要有唯一标识(如 user_id, order_id),单库单表场景下 采用主键自增的ID便可以实现业务。随着系统的推广,用户量飞涨,要拆分多库多表,这时候每个表的自增ID显然是无法在分布式场景下唯一标识一条数据的,就需要中心化生成分布式ID,然后再进行数据分片,这样多库多表才能分担数据库的读写压力。

1.3 算法特点

  • 能满足高并发分布式系统环境下ID不重复
  • 基于时间戳,可以保证基本有序递增
  • 不依赖第三方的库或者中间件
  • 性能极佳

2. 原理

2.1 实现原理

在同一个进程中,它首先是通过时间位保证不重复,如果时间相同则是通过序列位保证。 同时由于时间位是单调递增的,且各个服务器如果大体做了时间同步,那么生成的主键在分布式环境可以认为是总体有序的,这就保证了对索引字段的插入的高效性。 例如 MySQL 的 Innodb 存储引擎的主键。

雪花算法生成的实际是Long类型的数据,其在62位机器中,占满了64个比特位,包含 如下4 部分:

段名称位数(bit)说明
符号位1预留的符号位,1为负数,0为正数,我们生成的是ID都是正数,所以恒为零。
时间戳位4141 位的时间戳可以容纳的毫秒数是 2 的 41 次幂,Math.pow(2, 41) / (365 * 24 * 60 * 60 * 1000L); 约为 69.73年,PS:可以自己实现比如从2022年开始往后推,以满足可生成的最晚时间上限到2102年
工作机器ID位10该标志在 Java 进程内是唯一的,如果是分布式应用部署应保证每个工作进程的 id 是不同的,可以内部再拆分为不同的可用区.。最大为 1024
序列号位12该序列是用来在同一个毫秒内生成不同的 ID。如果在这个毫秒内生成的数量超过 4096 (2 的 12 次幂),那么生成器会等待到下个毫秒继续生成。

图示:

在这里插入图片描述

2.2 缺点

2.2.1 时钟回拨

服务器时钟回拨会导致产生重复序列,因此默认分布式主键生成器提供了一个最大容忍的时钟回拨毫秒数。 如果时钟回拨的时间超过最大容忍的毫秒数阈值,则程序报错;如果在可容忍的范围内,默认分布式主键生成器会等待时钟同步到最后一次主键生成的时间后再继续工作。 最大容忍的时钟回拨毫秒数的默认值为 0,可通过属性设置。

解决:
采用等待跟上次时间的一段范围,这种算是简单解决,可以接受的情况,但是要是等待一段时间之后又发生了时钟回拨,则抛异常,可以接受只能说是不算完全解决。

2.2.2 机器id分配与回收问题

机器id需要每台机器不一样,这样的分配方式需要有方案进行处理,同时也要考虑宕机之后的处理,如果宕机了对应的id分配后的回收问题。

解决:

  • 采用zookeeper的顺序节点分配,解决分配。回收采用zookeeper临时节点回收,但是临时节点不可靠,存在无故消失问题,因为也不是很可靠。
  • 采用中数据库中插入数据作为节点值,解决了分配,但是没有解决回收。

3. 实现

3.1 位运算基础

运算表达式说明
按位与a & b计算时将 十进制 转为 二进制 再进行计算;同位置为1,则结果为1,其余情况皆为0。n&(n-1) 会去除 n 的位级表示中最低的那一位 1
按位或a | b对应位上有一个为1,结果就为1。两个都为0,结果才得0,类似加的关系。
按位取反~a对每一位进行取反操作,如果是1则结果为0,是0则结果为1。即为反码
按位异或a ^ b当两个对应位不同时结果才为1,相同时得0。
  1. 任何数和 0做异或运算,结果仍然是原来的数,即a^0=a
  2. 任何数和其自身做异或运算,结果是 0,即 a^a=0
  3. 异或运算满足交换律和结合律
左移a << b将二进制位上的数向左移动,右边补0
带符号右移a >> b有符号整数最高位代表着数的正负,最高位为1代表负数,最高位为0代表正数。带符号右移是右移时,左边补充最高位上的值。
无符号右移a >> b二进制上的数向右移动,右移时左边补0

3.2 徒手撸一遍

3.2.1 算法代码

package com.lfc.util;

/**
 * 符号位-1: 正数是0,负数是1,所以id一般是正数,最高位是0
 * 毫秒位-41: 41位时间戳不是存储当前时间的时间戳,而是存储时间戳的差值(当前时间戳 - 开始时间戳),可以延伸时间上限
 * 机器位-10: 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId
 * 序列位-12: 毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号
 */
public class SnowflakeId {
    /**
     * 工作机器ID(0~31)
     */
    private long workerId;

    /**
     * 数据中心ID(0~31)
     */
    private long datacenterId;

    /**
     * 毫秒内序列(0~4095)
     */
    private long sequence = 0L;

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

    /**
     * 开始时间戳 2022-05-10
     */
//    private final long twepoch = 1652112000000L;
    private final long twepoch = 1273584846000L;

    /**
     * 机器id所占的位数
     */
    private final long workerIdBits = 5L;

    /**
     * 数据标识id所占的位数
     */
    private final long datacenterIdBits = 5L;

    /**
     * 序列在id中占的位数
     */
    private final long sequenceBits = 12L;

    /**
     * 5位二进制所支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
     */
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

    /**
     * 支持的最大数据标识id,结果是31
     */
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

    /**
     * 机器ID 低位偏移量: 12位
     */
    private final long workerIdShift = sequenceBits;

    /**
     * 数据标识ID 低位偏移量: 17位(12+5)
     */
    private final long datacenterIdShift = sequenceBits + workerIdBits;

    /**
     * 时间戳 低位偏移量: 22位(12+5+5)
     */
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    /**
     * 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
     */
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);

    /**
     * 构造函数
     *
     * @param workerId 工作机器ID
     * @param datacenterId 数据中心ID
     */
    public SnowflakeId(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("workerId 不能大于 %d 或 小于0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenterId 不能大于 %d 或 小于0", maxDatacenterId));
        }

        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    /**
     * 获取下一个ID
     */
    public synchronized long nextId() {
        long timestamp = currentMillis();

        // 如果当前的时间小于上一次生成ID时间,说明发生了时间回拨,不够的有好的方案是抛出异常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(String.format("时钟回拨 %d 毫秒,不可以生成雪花ID", lastTimestamp - timestamp));
        }

        // 如果是同一毫秒内生成的,则在毫秒内生成序列
        if (timestamp == lastTimestamp) {
            sequence = (sequence + 1) & sequenceMask;
            // 毫秒内溢出
            if (sequence == 0L) {
                timestamp = tilNextMillis(lastTimestamp);
            }
            // 时间戳改变,毫秒内序列归0
        } else {
            sequence = 0L;
        }

        // 上次生成ID的时间戳
        lastTimestamp = timestamp;

        // 移位并通过“按位或”运算拼到一起组成64位(12+5+5+22)的ID
        return ((timestamp - twepoch) << timestampLeftShift)
                | (datacenterId << datacenterIdShift)
                | (workerId << workerIdShift)
                | sequence;
    }

    /**
     * 阻塞到下一个毫秒,直到获取新的时间戳
     *
     * @param lastTimestamp 上次生成ID的时间戳
     * @return timestamp 当前时间戳
     */
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = currentMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = currentMillis();
        }

        return timestamp;
    }

    /**
     * 获取毫秒时间戳
     */
    protected long currentMillis() {
        return System.currentTimeMillis();
    }

}

该算法类的使用:

SnowflakeId idWorker = new SnowflakeId(1, 3);
        for (int i = 0; i < 100; i++) {
            long id = idWorker.nextId();
            System.out.println(Long.toBinaryString(id));
            System.out.println(id);
            Thread.sleep(1);
        }

输出:

1011000001100000010001111000110001010000001100001000000000000
1588654602050736128
1011000001100000010001111000110001010110001100001000000000000
1588654602063319040
1011000001100000010001111000110001011010001100001000000000000
1588654602071707648
1011000001100000010001111000110001011100001100001000000000000
1588654602075901952
1011000001100000010001111000110001100000001100001000000000000
1588654602084290560
1011000001100000010001111000110001100100001100001000000000000
1588654602092679168
1011000001100000010001111000110001101000001100001000000000000
1588654602101067776
1011000001100000010001111000110001101100001100001000000000000
1588654602109456384
1011000001100000010001111000110001110000001100001000000000000
1588654602117844992
1011000001100000010001111000110001110100001100001000000000000
1588654602126233600

3.2.2 难点讲解

根据二进制位数求能表示的最大值
负数的二进制

 	0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000001 (1的二进制)
 	1 - 1111111111 1111111111 1111111111 1111111111 1 - 11111 - 11111 - 111111111110 (1的二进制取反)
 -----------------------------------------------------------------------------------------------------------
 	1 - 1111111111 1111111111 1111111111 1111111111 1 - 11111 - 11111 - 111111111111 (加1变成-1的二进制)
 ^
 	1 - 1111111111 1111111111 1111111111 1111111111 1 - 11111 - 11111 - 111111100000 (-1的二进制左移5位)
 =   0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000011111 (对应的10进制为31)

毫秒内溢出
说明该毫秒内生成的序列号已经超过了4095(变成了4096),但是为什么以0判断?
(sequence + 1) & sequenceMask 溢出的时候等价于 4096 & 4095

	0111111111111 (4095)
&
	1000000000000 (4095)
=	0000000000000 (0)
PS:括号内为十进制数值,前面为对应的二进制

通过“按位 或”组合生成ID
为了方便计算,作如下假设

  • timestamp - twepoch 等于1
  • datacenterId 等于1
  • workerId 等于1
  • sequence 等于1
# 说明 1的二进制表示
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

# timestamp - twepoch 等于1
		00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
<<22
=		00000000 00000000 00000000 00000000 00000000 01000000 00000000 00000000

# datacenterId 等于1
		00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
<<17
=		00000000 00000000 00000000 00000000 00000000 00000010 00000000 00000000


# 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 00000000 00000000 00000001


# 所有的值进行或运算
		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

根据以上运算的过程可以总结如下两点:

  1. twepoch起始时间离现在太近了,会导致雪花算法生成的位数偏小,可能为15位及以上。
  2. 为什么最后通过按位或运算组装?:因为将各段数据向左移之后,占据各自的位长度(41+5+5+12),剩余的位都是0,根据或运算的特性,可以保持各段的位数据只保留应有的。
  3. 每毫秒内的第一个序列号都是0,这就可能导致并发不高的时候,ID生成会产生数据倾斜。PS:这是在使用shardingsphere中遇到的问题,之后的文章会给出解决方案。

3.3 中间件的实现

3.3.1 shardingsphere-proxy中的实现

org.apache.shardingsphere.sharding.algorithm.keygen.SnowflakeKeyGenerateAlgorithm类中:

package org.apache.shardingsphere.sharding.algorithm.keygen;

import com.google.common.base.Preconditions;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import org.apache.shardingsphere.infra.config.algorithm.ShardingSphereInstanceRequiredAlgorithm;
import org.apache.shardingsphere.infra.instance.InstanceContext;
import org.apache.shardingsphere.sharding.spi.KeyGenerateAlgorithm;
import java.util.Calendar;
import java.util.Properties;

public final class SnowflakeKeyGenerateAlgorithm implements KeyGenerateAlgorithm, ShardingSphereInstanceRequiredAlgorithm {
    
    public static final long EPOCH;
    
    // 毫秒内的最大序列号,可以自定义
    private static final String MAX_VIBRATION_OFFSET_KEY = "max-vibration-offset";
    
    private static final String MAX_TOLERATE_TIME_DIFFERENCE_MILLISECONDS_KEY = "max-tolerate-time-difference-milliseconds";
    
    private static final long SEQUENCE_BITS = 12L;
    
    // 并没有区分数据中心
    private static final long WORKER_ID_BITS = 10L;
    
    private static final long SEQUENCE_MASK = (1 << SEQUENCE_BITS) - 1;
    
    private static final long WORKER_ID_LEFT_SHIFT_BITS = SEQUENCE_BITS;
    
    private static final long TIMESTAMP_LEFT_SHIFT_BITS = WORKER_ID_LEFT_SHIFT_BITS + WORKER_ID_BITS;
    
    private static final long WORKER_ID_MAX_VALUE = 1L << WORKER_ID_BITS;
    
    private static final int DEFAULT_VIBRATION_VALUE = 1;
    
    private static final int MAX_TOLERATE_TIME_DIFFERENCE_MILLISECONDS = 10;
    
    private static final long DEFAULT_WORKER_ID = 0;
    
    @Setter
    private static TimeService timeService = new TimeService();
    
    @Getter
    @Setter
    private Properties props = new Properties();
    
    private int maxVibrationOffset;
    
    private int maxTolerateTimeDifferenceMilliseconds;
    
    private int sequenceOffset = -1;
    
    private long sequence;
    
    private long lastMilliseconds;
    
    private InstanceContext instanceContext;
    
    static {
        Calendar calendar = Calendar.getInstance();
        calendar.set(2016, Calendar.NOVEMBER, 1);
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        EPOCH = calendar.getTimeInMillis();
    }
    
    @Override
    public void init() {
        maxVibrationOffset = getMaxVibrationOffset();
        maxTolerateTimeDifferenceMilliseconds = getMaxTolerateTimeDifferenceMilliseconds();
    }
    
    private long getWorkerId() {
        if (null == instanceContext) {
            return DEFAULT_WORKER_ID;
        }
        long result = instanceContext.getWorkerId();
        Preconditions.checkArgument(result >= 0L && result < WORKER_ID_MAX_VALUE, "Illegal worker id.");
        return result;
    }
    
    private int getMaxVibrationOffset() {
        int result = Integer.parseInt(props.getOrDefault(MAX_VIBRATION_OFFSET_KEY, DEFAULT_VIBRATION_VALUE).toString());
        Preconditions.checkArgument(result >= 0 && result <= SEQUENCE_MASK, "Illegal max vibration offset.");
        return result;
    }
    
    private int getMaxTolerateTimeDifferenceMilliseconds() {
        return Integer.parseInt(props.getOrDefault(MAX_TOLERATE_TIME_DIFFERENCE_MILLISECONDS_KEY, MAX_TOLERATE_TIME_DIFFERENCE_MILLISECONDS).toString());
    }
    
    @Override
    public synchronized Comparable<?> generateKey() {
        long currentMilliseconds = timeService.getCurrentMillis();
        if (waitTolerateTimeDifferenceIfNeed(currentMilliseconds)) {
            currentMilliseconds = timeService.getCurrentMillis();
        }
        if (lastMilliseconds == currentMilliseconds) {
            if (0L == (sequence = (sequence + 1) & SEQUENCE_MASK)) {
                currentMilliseconds = waitUntilNextTime(currentMilliseconds);
            }
        } else {
            vibrateSequenceOffset();
            sequence = sequenceOffset;
        }
        lastMilliseconds = currentMilliseconds;
        return ((currentMilliseconds - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) | (getWorkerId() << WORKER_ID_LEFT_SHIFT_BITS) | sequence;
    }
    
    @SneakyThrows(InterruptedException.class)
    private boolean waitTolerateTimeDifferenceIfNeed(final long currentMilliseconds) {
        if (lastMilliseconds <= currentMilliseconds) {
            return false;
        }
        long timeDifferenceMilliseconds = lastMilliseconds - currentMilliseconds;
        Preconditions.checkState(timeDifferenceMilliseconds < maxTolerateTimeDifferenceMilliseconds, 
                "Clock is moving backwards, last time is %d milliseconds, current time is %d milliseconds", lastMilliseconds, currentMilliseconds);
        Thread.sleep(timeDifferenceMilliseconds);
        return true;
    }
    
    private long waitUntilNextTime(final long lastTime) {
        long result = timeService.getCurrentMillis();
        while (result <= lastTime) {
            result = timeService.getCurrentMillis();
        }
        return result;
    }
    
    private void vibrateSequenceOffset() {
        sequenceOffset = sequenceOffset >= maxVibrationOffset ? 0 : sequenceOffset + 1;
    }
    
    @Override
    public String getType() {
        return "SNOWFLAKE";
    }
    
    @Override
    public boolean isDefault() {
        return true;
    }
    
    @Override
    public void setInstanceContext(final InstanceContext instanceContext) {
        this.instanceContext = instanceContext;
    }
}

4. 替代方案

4.1 百度 UidGenerator

https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md

UidGenerator 以组件形式工作在应用项目中,支持自定义 WorkerID 位数和初始化策略,从而适用于 Docker 等虚拟化环境下实例自动重启、漂移等场景。

在实现上,UidGenerator 通过借用未来时间来解决 sequence 天然存在的并发限制;采用 RingBuffer 来缓存已生成的 UID,并行化 UID 的生产和消费,同时对 CacheLine 补齐,避免了由 RingBuffer 带来的硬件级「伪共享」问题。最终单机 QPS 可达 600 万。

4.2 美团 leaf-snowflake

Leaf-snowflake方案完全沿用snowflake方案的bit位设计,即是“1+41+10+12”的方式组装ID号。对于workerID的分配,当服务集群数量较小的情况下,完全可以手动配置。Leaf服务规模较大,动手配置成本太高。所以使用Zookeeper持久顺序节点的特性自动对snowflake节点配置wokerID。Leaf-snowflake是按照下面几个步骤启动的:

  1. 启动Leaf-snowflake服务,连接Zookeeper,在leaf_forever父节点下检查自己是否已经注册过(是否有该顺序子节点)。
  2. 如果有注册过直接取回自己的workerID(zk顺序节点生成的int类型ID号),启动服务。
  3. 如果没有注册过,就在该父节点下面创建一个持久顺序节点,创建成功后取回顺序号当做自己的workerID号,启动服务

5. 总结

在分库分表的时代,需要不依赖于库表的生成的ID,那么雪花算法可以满足这样的中心化的ID生成方案。但是在NewSQL数据中的却可以分布式环境下可以自增,这个有兴趣的可以去钻研下:

参考文档
pdai: https://pdai.tech/md/algorithm/alg-domain-id-snowflake.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值