全局唯一id生成器 各种实现记录

全局唯一id生成器

Redis 生成

前提知识~~ 时间戳

时间戳这个东西我们老是听到,却可能不是特别了解
首先,时间戳是从1970年1月1号0点0分开始的秒数,我查了蛮多资料,理论上来说,时间戳是没有上限的,而我们一般用位数来限制这里的上限,比如32位

我们来实际计算一下
32位的二进制, 2的32次方 - 1 = 4294967296 - 1 = 4294967295
因为时间戳表示的是秒数,所以这里就是32位下,最大的秒数

一天的秒数为 86400
365天的秒数为31536000

那么32位的时间戳是 4294967295 / 31536000 = 136

像现在是2024年,已经过了54年了,那么还有82年就要过期了

搞清楚这里的计算,我们后面就不会突然觉得,诶这里会不会超出上限

如何实现Redis全局id

首先我们要搞清楚为什么要全局id,全局id的作用是什么

第一: 唯一性,我们不能一套系统很多种全局id的生成器把,不能都用mysql自动生成id把,那样会混论,尤其是分布式系统
第二: 安全性,为了不让黑客知道我们生成id的规律,我们要加点佐料进去,例如时间戳
第三: 高可用 + 高性能 + 递增性 高可用就是,一个单点故障了,另外的一个服务器可以顶上,高性能就是生成的快,递增性就是为了我们业务的正常递增

所以我们就有redis生成全局id
这上面都符合,特别是高可用,可以用redis集群来保证,但是安全性,就要用不同的方法来实现了

这里是一种设计方法

设计的详解

在这里插入图片描述
时间戳31位,序列号32位

这里的全局id的意思就是,每一秒内的序列号作为全局id

这里的设计就很不错,这样很大程度上解决了问题,你可能会想要是1s内,超出了2的32次方怎么办,好办,就多写几位,压缩时间戳的位数

我们再来讲讲这里的时间戳的上限,如果是31位的化,那么最大就是2的31次方- 1 = 2147483648 - 1 = 2147483647
一年的秒数(365天) = 2147483647

2147483647 / 31536000 = 68 年 约等于68年, 现在是2024年 离1970年已经54年了,所以按道理来说14年后就过期了 也就是2038年

这里的序列号,就用redis的自增来实现

实际代码

/**
 * 全局唯一id生成器 Redis实现
 * @author jjking
 * @date 2024-02-07 20:27
 */
@Component
public class RedisIdWorker {
    
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 生成id
     * @param keyPrefix 业务的前缀key
     * @return
     */
    public long nextId(String keyPrefix) {
        //生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long timestamp = now.toEpochSecond(ZoneOffset.UTC);

        //生成序列号
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        long count = redisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

        return timestamp << 32 | count;
    }
}

这里比较有意思的点是两个点
第一: 是这里的redis生成序列号的点,特别要加入业务代码的前缀,不然全都用一个不就乱了套了,还有就是redis的value是有位数上限的,好像是2的64次方,所以这里还是会超出上限的,那么为了解决这个问题,就用了这里的时间来做区别,这样基本就不会有问题了

第二: ,这里的返回结果的计算也蛮有意思,首先是时间的位数向左边移动了32位,这里的意思就是腾出32位给序列号,然后再用位运算 或,来加上这里的序列号

特别要注意这里的或,很有意思,0 | 0 还是0 0 | 1 那么就是1,所以这里可以直接加上,这个得想一想才能想明白

UUID生成

UUID就比较耳熟能祥了,我这里写一个生成的范例

@Test
public void test1() {
    String uuid = UUID.randomUUID().toString();
    System.out.println(uuid);
}

在这里插入图片描述
可以看出来,他的位数分布是8-4-4-4-12位,一共是32位16进制数

我们来计算一下,总共多少字节,我们先转为为二进制,一位16进制,是4位二进制,那么 总共有32 * 4 = 128位二进制 一个字节是8位二进制

128 / 8 = 16字节

我们上面的redis生成的id是64位的,他的一半8个字节

所以,他的第一个缺点就是太大了,占内存

而且,这个uuid,也不太安全

但是他的优点就是性能还算蛮高的,还没有网络消耗

雪花算法 (重中之重)

先来了解雪花算法生成的id组成

  • 最高位 固定为 0 ,符号位,因为生成的id都为正数,固定为0
  • 41位 时间戳 单位 毫秒 经过计算最多可以使用69年
  • 10 位机器码 = 5位 数据中心id + 5位 工作机器id
  • 12 位序列号

这个样子有点类似于我们redis生成的id,不过序列号少了,并且是毫秒级的,还有一个机器码

我这里摘的是糊涂工具包中的雪花算法id,并且简略了一些无关辅助代码

代码

package com.hmdp.utils;

import cn.hutool.core.date.SystemClock;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;

import java.io.Serializable;
import java.util.Date;

/**
 * Twitter的Snowflake 算法<br>
 * 分布式系统中,有一些需要使用全局唯一ID的场景,有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。
 *
 * <p>
 * snowflake的结构如下(每部分用-分开):<br>
 *
 * <pre>
 * 符号位(1bit)- 时间戳相对值(41bit)- 数据中心标志(5bit)- 机器标志(5bit)- 递增序号(12bit)
 * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
 * </pre>
 * <p>
 * 第一位为未使用(符号位表示正数),接下来的41位为毫秒级时间(41位的长度可以使用69年)<br>
 * 然后是5位datacenterId和5位workerId(10位的长度最多支持部署1024个节点)<br>
 * 最后12位是毫秒内的计数(12位的计数顺序号支持每个节点每毫秒产生4096个ID序号)
 * <p>
 * 并且可以通过生成的id反推出生成时间,datacenterId和workerId
 * <p>
 * 参考:http://www.cnblogs.com/relucent/p/4955340.html<br>
 * 关于长度是18还是19的问题见:https://blog.csdn.net/unifirst/article/details/80408050
 *
 * @author Looly
 * @since 3.0.1
 */
public class Snowflake implements Serializable {
	private static final long serialVersionUID = 1L;

	/**
	 * 默认的起始时间,为Thu, 04 Nov 2010 01:42:54 GMT
	 */
	public static long DEFAULT_TWEPOCH = 1288834974657L;
	/**
	 * 默认回拨时间,2S
	 */
	public static long DEFAULT_TIME_OFFSET = 2000L;

	private static final long WORKER_ID_BITS = 5L;



	// 最大支持机器节点数0~31,一共32个
	@SuppressWarnings({"PointlessBitwiseExpression", "FieldCanBeLocal"})
	//-1L 为 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 (1L的补码)
	//左移5为  1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1110 0000
	//-1L为   1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111
	// ^ 异或是不同为1,相同为0
	// 结果为  0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 1111
	private static final long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS);


	private static final long DATA_CENTER_ID_BITS = 5L;


	// 最大支持数据中心节点数0~31,一共32个
	@SuppressWarnings({"PointlessBitwiseExpression", "FieldCanBeLocal"})
	//和上面的最大工作id一样的道理
	private static final long MAX_DATA_CENTER_ID = -1L ^ (-1L << DATA_CENTER_ID_BITS);



	// 序列号12位(表示只允许workId的范围为:0-4095)
	private static final long SEQUENCE_BITS = 12L;
	// 机器节点左移12位
	private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
	// 数据中心节点左移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
	//计算机的负数是用补码表示的
	//1L     0000000000000000000000000000000000000000000000000000000000000001
	//1L 反码 1111111111111111111111111111111111111111111111111111111111111110
	//1L 补码 1111111111111111111111111111111111111111111111111111111111111111      补码 = 反码 + 1

	//这里是 -1L(64位1) 往左移动12位 111111111111111111111111111111111111111111111111 0000 0000 0000 0000
	// ~ 取反 					  000000000000000000000000000000000000000000000000 1111 1111 1111 1111
	//结果为                       2的12次方 - 1 = 4095
	@SuppressWarnings("FieldCanBeLocal")
	private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);// 4095

	//起始时间
	private final long twepoch;


	private final long workerId;
	private final long dataCenterId;
	private final boolean useSystemClock;
	// 允许的时钟回拨数
	private final long timeOffset;

	private long sequence = 0L;
	private long lastTimestamp = -1L;


	/**
	 * @param epochDate        初始化时间起点(null表示默认起始日期),后期修改会导致id重复,如果要修改连workerId dataCenterId,慎用
	 * @param workerId         工作机器节点id
	 * @param dataCenterId     数据中心id
	 * @param isUseSystemClock 是否使用{@link SystemClock} 获取当前时间戳
	 * @param timeOffset 允许时间回拨的毫秒数
	 * @since 5.7.3
	 */
	public Snowflake(Date epochDate, long workerId, long dataCenterId, boolean isUseSystemClock, long timeOffset) {

		//如果没有给起始的时间就用默认的起始时间
		if (null != epochDate) {
			this.twepoch = epochDate.getTime();
		} else{
			// Thu, 04 Nov 2010 01:42:54 GMT
			this.twepoch = DEFAULT_TWEPOCH;
		}

		//工作机器id <= 31
		if (workerId > MAX_WORKER_ID || workerId < 0) {
			throw new IllegalArgumentException(StrUtil.format("worker Id can't be greater than {} or less than 0", MAX_WORKER_ID));
		}
		if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {
			throw new IllegalArgumentException(StrUtil.format("datacenter Id can't be greater than {} or less than 0", MAX_DATA_CENTER_ID));
		}


		this.workerId = workerId;
		this.dataCenterId = dataCenterId;
		this.useSystemClock = isUseSystemClock;
		this.timeOffset = timeOffset;
	}

	/**
	 * 根据Snowflake的ID,获取机器id
	 *
	 * @param id snowflake算法生成的id
	 * @return 所属机器的id
	 */
	public long getWorkerId(long id) {
		return id >> WORKER_ID_SHIFT & ~(-1L << WORKER_ID_BITS);
	}

	/**
	 * 根据Snowflake的ID,获取数据中心id
	 *
	 * @param id snowflake算法生成的id
	 * @return 所属数据中心
	 */
	public long getDataCenterId(long id) {
		return id >> DATA_CENTER_ID_SHIFT & ~(-1L << DATA_CENTER_ID_BITS);
	}

	/**
	 * 根据Snowflake的ID,获取生成时间
	 *
	 * @param id snowflake算法生成的id
	 * @return 生成的时间
	 */
	public long getGenerateDateTime(long id) {
		return (id >> TIMESTAMP_LEFT_SHIFT & ~(-1L << 41L)) + twepoch;
	}

	/**
	 * 下一个ID
	 *
	 * @return ID
	 */
	public synchronized long nextId() {
		//获取当前时间戳
		long timestamp = genTime();

		//如果小于上次的时间,这里有问题,时间回拨!
		if (timestamp < this.lastTimestamp) {
			if(this.lastTimestamp - timestamp < timeOffset){
				// 容忍指定的回拨,避免NTP校时造成的异常
				timestamp = lastTimestamp;
			} else{
				// 如果服务器时间有问题(时钟后退) 报错。
				throw new IllegalStateException(StrUtil.format("Clock moved backwards. Refusing to generate id for {}ms", lastTimestamp - timestamp));
			}
		}


		//如果等于上次的时间,说明,此时是同一毫秒,递增序列号
		if (timestamp == this.lastTimestamp) {

			//SEQUENCE_MASK为4095,这里的运算看上面的解释,这个相当于最大值
			//SEQUENCE_MASK 为                             00000000000000000000000000000000000000000000 0000 1111 1111 1111 1111
			//假设此时的序列号为4095(sequence) 那么前面是4096   00000000000000000000000000000000000000000000 0001 0000 0000 0000 0000
			//这样子与,0 & 0 = 0 ----- 0 & 1 = 0 ----- 1 & 1 = 1
			//所以最后结果为 								   00000000000000000000000000000000000000000000 0000 0000 0000 0000 0000
			final long sequence = (this.sequence + 1) & SEQUENCE_MASK;


			//如果此时为0说明,已经到了4095了,到达上限,应该等待下一个毫秒
			if (sequence == 0) {
				timestamp = tilNextMillis(lastTimestamp);
			}

			this.sequence = sequence;
		} else {
			sequence = 0L;
		}

		//赋值此时的上一次时间戳(毫秒)
		lastTimestamp = timestamp;



		return ((timestamp - twepoch) << TIMESTAMP_LEFT_SHIFT)
				| (dataCenterId << DATA_CENTER_ID_SHIFT)
				| (workerId << WORKER_ID_SHIFT)
				| sequence;
	}

	/**
	 * 循环等待下一个时间
	 *
	 * @param lastTimestamp 上次记录的时间
	 * @return 下一个时间
	 */
	private long tilNextMillis(long lastTimestamp) {
		long timestamp = genTime();
		// 循环直到操作系统时间戳变化
		while (timestamp == lastTimestamp) {
			timestamp = genTime();
		}
		if (timestamp < lastTimestamp) {
			// 如果发现新的时间戳比上次记录的时间戳数值小,说明操作系统时间发生了倒退,报错
			throw new IllegalStateException(
					StrUtil.format("Clock moved backwards. Refusing to generate id for {}ms", lastTimestamp - timestamp));
		}
		return timestamp;
	}

	/**
	 * 生成时间戳
	 *
	 * @return 时间戳
	 */
	private long genTime() {
		return this.useSystemClock ? SystemClock.now() : System.currentTimeMillis();
	}
	// ------------------------------------------------------------------------------------------------------------------------------------ Private method end
}

会有点长,但是核心的东西就一段

我们直接来看这一段

	/**
	 * 下一个ID
	 *
	 * @return ID
	 */
	public synchronized long nextId() {
		//获取当前时间戳
		long timestamp = genTime();

		//如果小于上次的时间,这里有问题,时间回拨!
		if (timestamp < this.lastTimestamp) {
			if(this.lastTimestamp - timestamp < timeOffset){
				// 容忍指定的回拨,避免NTP校时造成的异常
				timestamp = lastTimestamp;
			} else{
				// 如果服务器时间有问题(时钟后退) 报错。
				throw new IllegalStateException(StrUtil.format("Clock moved backwards. Refusing to generate id for {}ms", lastTimestamp - timestamp));
			}
		}


		//如果等于上次的时间,说明,此时是同一毫秒,递增序列号
		if (timestamp == this.lastTimestamp) {

			//SEQUENCE_MASK为4095,这里的运算看上面的解释,这个相当于最大值
			//SEQUENCE_MASK 为                             00000000000000000000000000000000000000000000 0000 1111 1111 1111 1111
			//假设此时的序列号为4095(sequence) 那么前面是4096   00000000000000000000000000000000000000000000 0001 0000 0000 0000 0000
			//这样子与,0 & 0 = 0 ----- 0 & 1 = 0 ----- 1 & 1 = 1
			//所以最后结果为 								   00000000000000000000000000000000000000000000 0000 0000 0000 0000 0000
			final long sequence = (this.sequence + 1) & SEQUENCE_MASK;


			//如果此时为0说明,已经到了4095了,到达上限,应该等待下一个毫秒
			if (sequence == 0) {
				timestamp = tilNextMillis(lastTimestamp);
			}

			this.sequence = sequence;
		} else {
			sequence = 0L;
		}

		//赋值此时的上一次时间戳(毫秒)
		lastTimestamp = timestamp;



		return ((timestamp - twepoch) << TIMESTAMP_LEFT_SHIFT)
				| (dataCenterId << DATA_CENTER_ID_SHIFT)
				| (workerId << WORKER_ID_SHIFT)
				| sequence;
	}

我们来总结一下,这个核心代码的代码逻辑

我们要生成id的化,需要几部分 时间戳 + 机器码 + 序列号
机器码也就是我们服务器的标识,一般是我们字节写的,所以不用考虑这个

重点在于时间戳 + 序列号


时间戳的生成: 当前时间戳,并且是毫秒级的
时间戳的生成,代码很简单,所以也不要终点考虑


序列号的生成(重点): 第一: 我们需要校验这里的时间戳,是否有问题,也就是当前时间比上一次的时间还早,出现时间回拨问题

第二: 我们得校验此时的序列号是否超过上限,如果超过上限,那么置此时的序列号为0,并且等待下一毫秒,将此时的时间戳更新

最重要的问题就是这两,相比较,比较简单的问题是这里的超过上限问题,这里也很简单,就是循环等待下一毫秒,到达下一毫秒更新此时的时间戳,序列号也已经设置好了为0

最难也是最重要的问题,时间回拨问题,这里的位运算问题,还是很好理解的,只要会位运算,都能解决

但是我这里特别不能搞懂,为啥这里要用位运算

类似于如下代码

	// 序列掩码,用于限定序列最大值不能超过4095
	//计算机的负数是用补码表示的
	//1L     0000000000000000000000000000000000000000000000000000000000000001
	//1L 反码 1111111111111111111111111111111111111111111111111111111111111110
	//1L 补码 1111111111111111111111111111111111111111111111111111111111111111      补码 = 反码 + 1

	//这里是 -1L(64位1) 往左移动12位 111111111111111111111111111111111111111111111111 0000 0000 0000 0000
	// ~ 取反 					  000000000000000000000000000000000000000000000000 1111 1111 1111 1111
	//结果为                       2的12次方 - 1 = 4095
	@SuppressWarnings("FieldCanBeLocal")
	private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);// 4095

这里的mask就是,相当于最大值,我不能明白的是,为什么不直接写4095L 或者写2的12次方 - 1,这里的12次方的12 一样也可以写成这里的 SEQUENCE_BITS 为啥要搞这个位运算???,我查了一下,都没有这方面的问题,如果你懂的化,可以私信我,谢谢了

时钟回拨问题

我也是看别人说,会有这个时钟回拨问题,问题的出现在于,有可能运维人员手动的更改了服务器的时间,或者两个服务器时间不同,需要同步时间,就会导致这里的时钟回拨问题

解决方案:

第一种方案: 是如果是时间回拨只是一两次,并且时间跨度不是很大的化,例如1 到 3秒,那么就直接等,那么几秒,这样子相当于有冗余,但是影响也不是很大,但这种操作,不能再并发量很高的时候操作,不然肯定出问题

第二种方案: 就是美团 和百度的方案
这两的方案我就先不研究了,到时候我真的懂了,就来更新这里的博客,我估计我也看不懂

  • 22
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

憨憨小江

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值