雪花算法简述

简介


现在的服务基本是分布式、微服务形式的,而且大数据量也导致分库分表的产生,对于水平分表就需要保证表中 id 的全局唯一性。

对于 MySQL 而言,一个表中的主键 id 一般使用自增的方式,但是如果进行水平分表之后,多个表中会生成重复的 id 值。那么如何保证水平分表后的多张表中的 id 是全局唯一性的呢?

如果还是借助数据库主键自增的形式,那么可以让不同表初始化一个不同的初始值,然后按指定的步长进行自增。例如有3张拆分表,初始主键值为1,2,3,自增步长为3。

当然也有人使用 UUID 来作为主键,但是 UUID 生成的是一个无序的字符串,对于 MySQL 推荐使用增长的数值类型值作为主键来说不适合。

也可以使用 Redis 的自增原子性来生成唯一 id,但是这种方式业内比较少用。

当然还有其他解决方案,不同互联网公司也有自己内部的实现方案。雪花算法是其中一个用于解决分布式 id 的高效方案,也是许多互联网公司在推荐使用的。

SnowFlake 雪花算法
SnowFlake 中文意思为雪花,故称为雪花算法。最早是 Twitter 公司在其内部用于分布式环境下生成唯一 ID。在2014年开源 scala 语言版本。

雪花算法的原理就是生成一个的 64 位比特位的 long 类型的唯一 id。

最高 1 位固定值 0,因为生成的 id 是正整数,如果是 1 就是负数了。
接下来 41 位存储毫秒级时间戳,2^41/(1000*60*60*24*365)=69,大概可以使用 69 年。
再接下 10 位存储机器码,包括 5 位 datacenterId 和 5 位 workerId。最多可以部署 2^10=1024 台机器。
最后 12 位存储序列号。同一毫秒时间戳时,通过这个递增的序列号来区分。即对于同一台机器而言,同一毫秒时间戳下,可以生成 2^12=4096 个不重复 id。
可以将雪花算法作为一个单独的服务进行部署,然后需要全局唯一 id 的系统,请求雪花算法服务获取 id 即可。

扫描二维码关注公众号,回复: 16902850 查看本文章

对于每一个雪花算法服务,需要先指定 10 位的机器码,这个根据自身业务进行设定即可。例如机房号+机器号,机器号+服务号,或者是其他可区别标识的 10 位比特位的整数值都行。

代码实现

public class SnowFlakeIdWorker {

	/**
     * 开始时间戳,单位毫秒;这里是2021-06-01
     */
    private static final long TW_EPOCH = 1622476800000L;
    /**
     * 机器 ID 所占的位数
     */
    private static final long WORKER_ID_BITS = 5L;
    /**
     * 数据标识 ID 所占的位数
     */
    private static final long DATA_CENTER_ID_BITS = 5L;
    /**
     * 支持的最大机器ID,最大为31
     *
     * PS. Twitter的源码是 -1L ^ (-1L << workerIdBits);这里最后和-1进行异或运算,由于-1的二进制补码的特殊性,就相当于进行取反。
     */
    private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
    /**
     * 支持的最大机房ID,最大为31
     */
    private static final long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS);
    /**
     * 序列在 ID 中占的位数
     */
    private static final long SEQUENCE_BITS = 12L;
    /**
     * 机器 ID 向左移12位
     */
    private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
    /**
     * 机房 ID 向左移17位
     */
    private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
    /**
     * 时间截向左移22位
     */
    private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;
    /**
     * 生成序列的掩码最大值,最大为4095
     */
    private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
    /**
     * 工作机器 ID(0~31)
     */
    private final long workerId;
    /**
     * 机房 ID(0~31)
     */
    private final long dataCenterId;
    /**
     * 毫秒内序列(0~4095)
     */
    private long sequence = 0L;
    /**
     * 上次生成 ID 的时间戳
     */
    private long lastTimestamp = -1L;
    /**
     * 创建 ID 生成器的方式一: 使用工作机器的序号(也就是将机房的去掉给机器ID使用),范围是 [0, 1023],优点是方便给机器编号
     *
     * @param workerId 工作机器 ID
     */
    public SnowFlakeIdWorker(long workerId) {
        // 计算最大值
        long maxMachineId = (MAX_DATA_CENTER_ID + 1) * (MAX_WORKER_ID + 1) - 1;
        if (workerId < 0 || workerId > maxMachineId) {
        	// 工作进程ID不能大于%d或小于0
            throw new IllegalArgumentException(String.format("Worker ID can't be greater than %d or less than 0", maxMachineId));
        }
        // 取高位部分作为机房ID部分
        this.dataCenterId = (workerId >> WORKER_ID_BITS) & MAX_DATA_CENTER_ID;
        // 取低位部分作为机器ID部分
        this.workerId = workerId & MAX_WORKER_ID;
    }
    /**
     * 创建 ID 生成器的方式二: 使用工作机器 ID 和机房 ID,优点是方便分机房管理
     *
     * @param dataCenterId 机房 ID (0~31)
     * @param workerId     工作机器 ID (0~31)
     */
    public SnowFlakeIdWorker(long dataCenterId, long workerId) {
        if (workerId > MAX_WORKER_ID || workerId < 0) {
        	//工作进程ID不能大于%d或小于0
            throw new IllegalArgumentException(String.format("Worker ID can't be greater than %d or less than 0", MAX_WORKER_ID));
        }
        if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {
        	//数据中心ID不能大于%d或小于0
            throw new IllegalArgumentException(String.format("DataCenter ID can't be greater than %d or less than 0", MAX_DATA_CENTER_ID));
        }
        this.workerId = workerId;
        this.dataCenterId = dataCenterId;
    }
    /**
     * 获得下一个 ID(该方法是线程安全的)
     *
     * @return 返回一个长度位15的 long类型的数字
     */
    public synchronized long nextId() {
        long timestamp = timeGen();
        // 如果当前时间小于上一次 ID 生成的时间戳,说明发生时钟回拨,为保证ID不重复抛出异常。
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(String.format("可能出现服务器时钟回拨问题,请检查服务器时间。当前服务器时间戳:%d,上一次使用时间戳:%d", timestamp,
            		lastTimestamp));
        }
        if (lastTimestamp == timestamp) {
            // 同一时间生成的,则序号+1
            sequence = (sequence + 1) & SEQUENCE_MASK;
            // 毫秒内序列溢出:超过最大值
            if (sequence == 0) {
                // 阻塞到下一个毫秒,获得新的时间戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            // 时间戳改变,毫秒内序列重置
            sequence = 0L;
        }
        // 上次生成 ID 的时间戳
        lastTimestamp = timestamp;
        // 移位并通过或运算拼到一起
        return ((timestamp - TW_EPOCH) << TIMESTAMP_LEFT_SHIFT)
                | (dataCenterId << DATA_CENTER_ID_SHIFT)
                | (workerId << WORKER_ID_SHIFT)
                | sequence;
    }
    
    private long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }
    
    private long timeGen() {
        return System.currentTimeMillis();
    }
	
}

使用方法

public static void main(String[] args) throws InterruptedException {
		SnowFlakeIdWorker sFlakeIdWorker = new SnowFlakeIdWorker(1,2);
		for (int i=0,len=10000; i < len; i++) {
			Thread t1 = new Thread();
			t1.sleep(1000);
			long id = sFlakeIdWorker.nextId();
			System.out.println("id:"+id);
		}
		
	}

算法优缺点


雪花算法有以下几个优点:

*高并发分布式环境下生成不重复 id,每秒可生成百万个不重复 id。
*基于时间戳,以及同一时间戳下序列号自增,基本保证 id 有序递增。
*不依赖第三方库或者中间件。
*算法简单,在内存中进行,效率高。


雪花算法有如下缺点:

* 依赖服务器时间,服务器时钟回拨时可能会生成重复 id。算法中可通过记录最后一个生成 id 时的时间戳来解决,每次生成 id 之前比较当前服务器时钟是否被回拨,避免生成重复 id。

注意事项

其实雪花算法每一部分占用的比特位数量并不是固定死的。例如你的业务可能达不到 69 年之久,那么可用减少时间戳占用的位数,雪花算法服务需要部署的节点超过1024 台,那么可将减少的位数补充给机器码用。

注意,雪花算法中 41 位比特位不是直接用来存储当前服务器毫秒时间戳的,而是需要当前服务器时间戳减去某一个初始时间戳值,一般可以使用服务上线时间作为初始时间戳值。

对于机器码,可根据自身情况做调整,例如机房号,服务器号,业务号,机器 IP 等都是可使用的。对于部署的不同雪花算法服务中,最后计算出来的机器码能区分开来即可。
 

猜你喜欢

转载自blog.csdn.net/qq_19688207/article/details/130358379