雪花算法、梨花算法、薄雾算法等在分布式中全局唯一ID的生成和使用及其特性

雪花算法、梨花算法、薄雾算法等在分布式中全局唯一ID的生成和使用及其特性。

互联网快速发展的今天,分布式应用系统已经见怪不怪,在分布式系统中,我们需要各种各样的ID,既然是ID那么必然是要保证全局唯一,除此之外,不同当业务还需要不同的特性,比如像并发巨大的业务要求ID生成效率高,吞吐大;比如某些银行类业务,需要按每日日期制定交易流水号等等。针对每个公司,随着服务化演进,单个服务越来越多,数据库分的越来越细,有的时候一个业务需要分成好几个库,这时候自增主键或者序列之类的主键id生成方式已经不再满足需求,分布式系统中需要的是一个全局唯一的id生成规则。既然号称在全局分布式系统中唯一,那么主键的生成规则必然要复杂一些。

雪花算法(SnowFlake)

SnowFlake 算法,是 Twitter 开源的分布式 id 生成算法。其核心思想就是:使用一个 64 bit 的 long 型的数字作为全局唯一 id。在分布式系统中的应用十分广泛,且ID 引入了时间戳,基本上保持递增的。这 64 个 bit 中,其中 1 个 bit 是不用的,然后用其中的 41 bit 作为毫秒数,用 10 bit 作为工作机器 id,12 bit 作为序列号。举个例子,比如下面那个 64 bit 的 long 型数字:

0|1020000000000000000000000000000000000000000|10001 11001|100000000001

第一个部分,是 1 个 bit:0,这个是无意义的。

第二个部分是 41 个 bit:表示的是时间戳。

第三个部分是 5 个 bit:表示的是机房 id,10001。

第四个部分是 5 个 bit:表示的是机器 id,11001。

第五个部分是 12 个 bit:表示的序号,就是某个机房某台机器上这一毫秒内同时生成的 id 的序号,0000 00000000。

①1 bit:是不用的,为啥呢?

因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。

②41 bit:表示的是时间戳,单位是毫秒。

41 bit 可以表示的数字多达 2^41 - 1,也就是可以标识 2 ^ 41 - 1 个毫秒值,换算成年就是表示 69 年的时间。

③10 bit:记录工作机器 id,代表的是这个服务最多可以部署在 2^10 台机器上,也就是 1024 台机器。

但是 10 bit 里 5 个 bit 代表机房 id,5 个 bit 代表机器 id。意思就是最多代表 2 ^ 5 个机房(32 个机房),每个机房里可以代表 2 ^ 5 个机器(32 台机器),也可以根据自己公司的实际情况确定。

④12 bit:这个是用来记录同一个毫秒内产生的不同 id。

12 bit 可以代表的最大正整数是 2 ^ 12 - 1 = 4096,也就是说可以用这个 12 bit 代表的数字来区分同一个毫秒内的 4096 个不同的 id。

简单来说,你的某个服务假设要生成一个全局唯一 id,那么就可以发送一个请求给部署了 SnowFlake 算法的系统,由这个 SnowFlake 算法系统来生成唯一 id。

这个 SnowFlake 算法系统首先肯定是知道自己所在的机房和机器的,比如机房 id = 17,机器 id = 12。

接着 SnowFlake 算法系统接收到这个请求之后,首先就会用二进制位运算的方式生成一个 64 bit 的 long 型 id,64 个 bit 中的第一个 bit 是无意义的。

接着 41 个 bit,就可以用当前时间戳(单位到毫秒),然后接着 5 个 bit 设置上这个机房 id,还有 5 个 bit 设置上机器 id。

最后再判断一下,当前这台机房的这台机器上这一毫秒内,这是第几个请求,给这次生成 id 的请求累加一个序号,作为最后的 12 个 bit。

最终一个 64 个 bit 的 id 就出来了,这个算法可以保证说,一个机房的一台机器上,在同一毫秒内,生成了一个唯一的 id。可能一个毫秒内会生成多个 id,但是有最后 12 个 bit 的序号来区分开来。

下面我们简单看看这个 SnowFlake 算法的一个代码实现,这就是个示例,总之就是用一个 64 bit 的数字中各个 bit 位来设置不同的标志位,区分每一个 id。

 
public class IdWorker {
 
	//因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。
 
	//机器ID  2进制5位  32位减掉1位 31个
	private long workerId;
	//机房ID 2进制5位  32位减掉1位 31个
	private long datacenterId;
	//代表一毫秒内生成的多个id的最新序号  12位 4096 -1 = 4095 个
	private long sequence;
	//设置一个时间初始值    2^41 - 1   差不多可以用69年
	private long twepoch = 1585644268888L;
	//5位的机器id
	private long workerIdBits = 5L;
	//5位的机房id
	private long datacenterIdBits = 5L;
	//每毫秒内产生的id数 2 的 12次方
	private long sequenceBits = 12L;
	// 这个是二进制运算,就是5 bit最多只能有31个数字,也就是说机器id最多只能是32以内
	private long maxWorkerId = -1L ^ (-1L << workerIdBits);
	// 这个是一个意思,就是5 bit最多只能有31个数字,机房id最多只能是32以内
	private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
 
	private long workerIdShift = sequenceBits;
	private long datacenterIdShift = sequenceBits + workerIdBits;
	private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
	private long sequenceMask = -1L ^ (-1L << sequenceBits);
	//记录产生时间毫秒数,判断是否是同1毫秒
	private long lastTimestamp = -1L;
	public long getWorkerId(){
		return workerId;
	}
	public long getDatacenterId() {
		return datacenterId;
	}
	public long getTimestamp() {
		return System.currentTimeMillis();
	}
 
 
 
	public IdWorker(long workerId, long datacenterId, long sequence) {
 
		// 检查机房id和机器id是否超过31 不能小于0
		if (workerId > maxWorkerId || workerId < 0) {
			throw new IllegalArgumentException(
					String.format("worker Id can't be greater than %d or less than 0",maxWorkerId));
		}
 
		if (datacenterId > maxDatacenterId || datacenterId < 0) {
 
			throw new IllegalArgumentException(
					String.format("datacenter Id can't be greater than %d or less than 0",maxDatacenterId));
		}
		this.workerId = workerId;
		this.datacenterId = datacenterId;
		this.sequence = sequence;
	}
 
	// 这个是核心方法,通过调用nextId()方法,让当前这台机器上的snowflake算法程序生成一个全局唯一的id
	public synchronized long nextId() {
		// 这儿就是获取当前时间戳,单位是毫秒
		long timestamp = timeGen();
		if (timestamp < lastTimestamp) {
 
			System.err.printf(
					"clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
			throw new RuntimeException(
					String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
							lastTimestamp - timestamp));
		}
 
		// 下面是说假设在同一个毫秒内,又发送了一个请求生成一个id
		// 这个时候就得把seqence序号给递增1,最多就是4096
		if (lastTimestamp == timestamp) {
 
			// 这个意思是说一个毫秒内最多只能有4096个数字,无论你传递多少进来,
			//这个位运算保证始终就是在4096这个范围内,避免你自己传递个sequence超过了4096这个范围
			sequence = (sequence + 1) & sequenceMask;
			//当某一毫秒的时间,产生的id数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生ID
			if (sequence == 0) {
				timestamp = tilNextMillis(lastTimestamp);
			}
 
		} else {
			sequence = 0;
		}
		// 这儿记录一下最近一次生成id的时间戳,单位是毫秒
		lastTimestamp = timestamp;
		// 这儿就是最核心的二进制位运算操作,生成一个64bit的id
		// 先将当前时间戳左移,放到41 bit那儿;将机房id左移放到5 bit那儿;将机器id左移放到5 bit那儿;将序号放最后12 bit
		// 最后拼接起来成一个64 bit的二进制数字,转换成10进制就是个long型
		return ((timestamp - twepoch) << timestampLeftShift) |
				(datacenterId << datacenterIdShift) |
				(workerId << workerIdShift) | sequence;
	}
 
	/**
	 * 当某一毫秒的时间,产生的id数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生ID
	 * @param lastTimestamp
	 * @return
	 */
	private long tilNextMillis(long lastTimestamp) {
 
		long timestamp = timeGen();
 
		while (timestamp <= lastTimestamp) {
			timestamp = timeGen();
		}
		return timestamp;
	}
	//获取当前时间戳
	private long timeGen(){
		return System.currentTimeMillis();
	}
 
	/**
	 *  main 测试类
	 * @param args
	 */
	public static void main(String[] args) {
		System.out.println(1&4596);
		System.out.println(2&4596);
		System.out.println(6&4596);
		System.out.println(6&4596);
		System.out.println(6&4596);
		System.out.println(6&4596);
		IdWorker worker = new IdWorker(1,1,1);
		for (int i = 0; i < 22; i++) {
			System.out.println(worker.nextId());
		}
	}
}

SnowFlake算法的优点:

(1)高性能高可用:生成时不依赖于数据库,完全在内存中生成。

(2)容量大:每秒中能生成数百万的自增ID。

(3)ID自增:存入数据库中,索引效率高。

SnowFlake算法的缺点:

依赖与系统时间的一致性,如果系统时间被回调,或者改变,可能会造成id冲突或者重复。

实际中我们的机房并没有那么多,我们可以改进改算法,将10bit的机器id优化,成业务表或者和我们系统相关的业务。

public class SnowFlakeGenerator {
 
	public static class Factory {
		/**
		 * 每一部分占用位数的默认值
		 */
		private final static int DEFAULT_MACHINE_BIT_NUM = 5; // 机器标识占用的位数
		private final static int DEFAULT_IDC_BIT_NUM = 5;// 数据中心占用的位数
 
		private int machineBitNum;
		private int idcBitNum;
 
		public Factory() {
			this.idcBitNum = DEFAULT_IDC_BIT_NUM;
			this.machineBitNum = DEFAULT_MACHINE_BIT_NUM;
		}
 
		public Factory(int machineBitNum, int idcBitNum) {
			this.idcBitNum = idcBitNum;
			this.machineBitNum = machineBitNum;
		}
 
		public SnowFlakeGenerator create(long idcId, long machineId) {
			return new SnowFlakeGenerator(this.idcBitNum, this.machineBitNum, idcId, machineId);
		}
	}
 
	/**
	 * 起始的时间戳 作者写代码时的时间戳
	 */
	private final static long START_STAMP = 1553153540626L;
 
	/**
	 * 可分配的位数
	 */
	private final static int REMAIN_BIT_NUM = 12;
 
	/**
	 * idc编号
	 */
	private long idcId;
 
	/**
	 * 机器编号
	 */
	private long machineId;
 
	/**
	 * 当前序列号
	 */
	private long sequence = 0L;
 
	/**
	 * 上次最新时间戳
	 */
	private long lastStamp = -1L;
 
	/**
	 * idc偏移量:一次计算出,避免重复计算
	 */
	private int idcBitLeftOffset;
 
	/**
	 * 机器id偏移量:一次计算出,避免重复计算
	 */
	private int machineBitLeftOffset;
 
	/**
	 * 时间戳偏移量:一次计算出,避免重复计算
	 */
	private int timestampBitLeftOffset;
 
	/**
	 * 最大序列值:一次计算出,避免重复计算
	 */
	private int maxSequenceValue;
 
	private SnowFlakeGenerator(int idcBitNum, int machineBitNum, long idcId, long machineId) {
		int sequenceBitNum = REMAIN_BIT_NUM - idcBitNum - machineBitNum;
 
		if (idcBitNum <= 0 || machineBitNum <= 0 || sequenceBitNum <= 0) {
			throw new IllegalArgumentException("error bit number");
		}
 
		this.maxSequenceValue = ~(-1 << sequenceBitNum);
 
		machineBitLeftOffset = sequenceBitNum;
		idcBitLeftOffset = idcBitNum + sequenceBitNum;
		timestampBitLeftOffset = idcBitNum + machineBitNum + sequenceBitNum;
 
		this.idcId = idcId;
		this.machineId = machineId;
	}
 
	/**
	 * 产生下一个ID
	 */
	public synchronized long nextId() {
		long currentStamp = getTimeMill();
		if (currentStamp < lastStamp) {
			throw new RuntimeException(String.format(
					"Clock moved backwards. Refusing to generate id for %d milliseconds", lastStamp - currentStamp));
		}
 
		// 新的毫秒,序列从0开始,否则序列自增
		if (currentStamp == lastStamp) {
			sequence = (sequence + 1) & this.maxSequenceValue;
			if (sequence == 0L) {
				// Twitter源代码中的逻辑是循环,直到下一个毫秒
				lastStamp = tilNextMillis();
//                throw new IllegalStateException("sequence over flow");
			}
		} else {
			sequence = 0L;
		}
 
		lastStamp = currentStamp;
 
		return (currentStamp - START_STAMP) << timestampBitLeftOffset | idcId << idcBitLeftOffset
				| machineId << machineBitLeftOffset | sequence;
	}
 
	private long getTimeMill() {
		return System.currentTimeMillis();
	}
 
	private long tilNextMillis() {
		long timestamp = getTimeMill();
		while (timestamp <= lastStamp) {
			timestamp = getTimeMill();
		}
		return timestamp;
	}
 
	public static void main(String[] args) {
		SnowFlakeGenerator snowFlakeGenerator = new SnowFlakeGenerator.Factory().create(1, 1);
		
		long start = System.currentTimeMillis();
		for (long i = 0; i < 100000; i++) {
			System.out.println(snowFlakeGenerator.nextId());
		}
		System.out.println(System.currentTimeMillis() - start);
	}
}
/**
 * Twitter的分布式自增ID雪花算法snowflake
 * 
 *
 */
public class SnowFlake {
 
	/**
	 * 起始的时间戳
	 */
	private final static long START_STMP = 1553153540626L;
 
	/**
	 * 每一部分占用的位数
	 */
	private final static long SEQUENCE_BIT = 2; // 序列号占用的位数
	private final static long MACHINE_BIT = 5; // 机器标识占用的位数
	private final static long DATACENTER_BIT = 5;// 数据中心占用的位数
 
	/**
	 * 每一部分的最大值
	 */
	private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
	private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
	private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
 
	/**
	 * 每一部分向左的位移
	 */
	private final static long MACHINE_LEFT = SEQUENCE_BIT;
	private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
	private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;
 
	private long datacenterId; // 数据中心
	private long machineId; // 机器标识
	private long sequence = 0L; // 序列号
	private long lastStmp = -1L;// 上一次时间戳
 
	public SnowFlake(long datacenterId, long machineId) {
		if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
			throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
		}
		if (machineId > MAX_MACHINE_NUM || machineId < 0) {
			throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
		}
		this.datacenterId = datacenterId;
		this.machineId = machineId;
	}
 
	/**
	 * 产生下一个ID
	 *
	 * @return
	 */
	public synchronized long nextId() {
		long currStmp = getNowMill();
		if (currStmp < lastStmp) {
			throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
		}
 
		if (currStmp == lastStmp) {
			// 相同毫秒内,序列号自增
			sequence = (sequence + 1) & 3;
			// 同一毫秒的序列数已经达到最大
			if (sequence == 0L) {
				currStmp = getNextMill();
			}
		} else {
			// 不同毫秒内,序列号置为0
			sequence = 0L;
		}
		lastStmp = currStmp;
 
		return (currStmp - START_STMP) << TIMESTMP_LEFT // 时间戳部分
				| datacenterId << DATACENTER_LEFT // 数据中心部分
				| machineId << MACHINE_LEFT // 机器标识部分
				| sequence; // 序列号部分
	}
 
	private long getNextMill() {
		long mill = getNowMill();
		while (mill <= lastStmp) {
			mill = getNowMill();
		}
		return mill;
	}
 
	private long getNowMill() {
		return System.currentTimeMillis();
	}
 
	public static void main(String[] args) {
		SnowFlake snowFlake = new SnowFlake(1, 1);
		long start = System.currentTimeMillis();
 
		for (int i = 0; i < 100000; i++) {
			System.out.println(snowFlake.nextId());
		}
 
		System.out.println(System.currentTimeMillis() - start);
 
	}
}

雪花算法中值得考虑的问题
1、雪花算法中的溢出问题

先来看一段代码:sequence = (sequence + 1) & MAX_SEQUENCE;
替换一下参数为:sequence = (sequence + 1) &  (-1L ^ (-1L << SEQUENCE_BIT))
带入常量为:sequence = (sequence + 1) &  (-1L ^ (-1L << 12))
化简得:sequence = (sequence + 1) & 4095;
用控制台打印结果展示这段代码解决的问题:
        //计算12位能耐存储的最大正整数,相当于:2^12-1 = 4095
        long seqMask = -1L ^ (-1L << 12L);
        System.out.println("seqMask: "+seqMask);
        System.out.println(1L & seqMask);
        System.out.println(2L & seqMask);
        System.out.println(3L & seqMask);
        System.out.println(4L & seqMask);
        System.out.println(4095L & seqMask);
        System.out.println(4096L & seqMask);
        System.out.println(4097L & seqMask);
        System.out.println(4098L & seqMask);
 
        
        /**
        seqMask: 4095
        1
        2
        3
        4
        4095
        0
        1
        2
        */

2、雪花算法中的夏令时问题
我们所说的夏令时实际上包括两类:夏令时和冬令时

夏令时(1:00 -> 3:00 AM)
往后拨一个小时,直接从1点变到3点,也就是说我们要比原来提前一个小时和美国人开会。
冬令时(1:00 -> 1:00 -> 2:00 AM)
往前拨一个小时,要过两个1点,这时比平常晚一个小时。

由此可见夏令时从1点跳到3点在雪花算法中没有什么影响,但是在冬令时要经历两个相同的时间段并使用相同的时间戳和算法参数进行运算就要出问题了。


改进的雪花算法——梨花算法(PearFlower)

改进目标:解决雪花算法的时钟回拨问题;部分避免机器id重复时,号码冲突问题。

long型的64位分成以下几个部分组成:

符号位:1位

时间:31位 (精确到秒)够用68年

段号(批次号):3位 每秒可分为8个段

机器号:10位 最多支持1024台机器

序列号:19位 可表示:0–524287

注:根据情况,机器号可以调整到最后部分。

每个机器,每秒可生成4.19w个id(4194304).

经过调整,时间只对秒灵敏,成功回避了服务器间几百毫秒的时间误差引起的时间回拨问题;若第59秒的8个段号没有用完,

则当润秒来临时,还可继续使用。另外具体实现上,可设置一定的秒数(如3秒)内提前消费。比如第10秒的号码,在800毫

秒用完了,可以继续使用第11秒的号码。这样,下1秒用的号码不是很多时,就可以借给上1秒使用。

以上的方案是与时间强相关的。若某一段时间内的号码没用使用,也会浪费掉。当在分布式DB的表主键这种应用场景时,

只需要全局id不重复,且是递增的。类似这种场景,可以设计成时间不相关的。

供分布式DB表主键等类似场景使用,不浪费号码的方案。long型的64位分配还是一样。只不过,取号时,是取上一个号码加1,

而不用管现在的时间是什么时候。当突然down机时,重启又获取当前的时间,重新开始分派号码;这时之前节省下的号码就被浪

费掉了。为解决这个问题,可以在一段时间或分派一定数量的号(如10000),就将当前分派的号码记录到日志,或同步到DB表,

等重启时,可以设置初始值。实现上,还是要控制分派的速度,若每秒几百万的号不够用,可用表名分隔命名空间,每个表单独取

自己的号;即使号码够用,也可以这样做,因为这样得到的号在同一张表里就比较连续,而不只是递增而矣。当各个机器分派的id

速度相差太大时,各机器得到的id大小就比较乱;这种问题,可以设置负载均衡,让每台机器轮流出号。

机器id重复的问题。当两台机器的id一样时,分派的号就会重复。若0-7八个段号(段号3位),每次都是从0-3随机获取一个

开始的段号,比方说获取到2,那重复机器id的服务要是获取到0或1的段号就可以避免号码重复的冲突。当然了,这都是基于每秒用

不完号码的情况下的。可以循环使用段号,如获取到3,那就从3-7,0,1,2这样使用段号,后面0,1,2这几个段号要是分派出去,

号码就不递增了。具体怎么用,还是要根据自己的情况做取舍。

org.teasoft.honey.distribution.SerialUniqueId 在一个workerid内连续唯一的ID生成方法(绝对连续单调递增,全局唯一).Serial Unique Id in one workerid. 优点:连续唯一;不依赖时钟. 在DB内实现,可达到分布式全局唯一ID在DB内自增长;在同一个workerid内,获取的ID号,可以满足连续单调递增唯一. Advantages:continuous and unique;less clock dependent.Implemented in dB, it can achieve auto increment of distributed global unique ID in dB. The ID number get in the same workerid can satisfy continuous monotonic increasing uniqueness. 缺点/Shortcoming:worker1’s ID。

org.teasoft.honey.distribution.OneTimeSnowflakeId OneTimeSnowflakeId,进一步改进了梨花算法。 不依赖时间的梨花算法,Workerid应放在序号sequence的上一段,且应用SerialUniqueId算法,使ID不依赖于时间自动递增。 使用不依赖时间的梨花算法OneTimeSnowflakeId,应保证各节点大概均衡轮流出号,这样入库的ID会比较有序,因此每个段号内的序列号不能太多。 支持批获取ID号。可以一次取一批ID(即一个范围内的ID一次就可以获取了)。可以代替依赖DB的号段模式。 应用订单号等有安全要求的场景,可随机不定时获取一些批的号码不用即可。 考虑到2019年双11的峰值不超过55万笔/秒, 因此419w/s这个值已可以满足此苛刻要求;采用testSpeedLimit()检测平均值不超过419w/s这个值即可,而且在空闲时 段省下的ID号,还可以在高峰时使用。


package org.teasoft.bee.distribution;

/**
 * 
 * 分布式环境下生成全局唯一数字id.Generate global unique id number in distributed environment.
 * GUID : Global Unique ID.
 * 
 * 分段模式与雪花到底有什么区别?
 * 一个是依赖DB,一个是依赖时间的.
 * 一个是取的号码可以一直连续递增的;一个是趋势递增,会因workerid的原因产生的ID号是会跳很大一段的.
 * 依赖于DB的号段模式,当多个节点一起拿号时,最终落库的ID还是不能连续的。
 * 雪花ID适合做分布式数据库表主键吗?它只保证递增,没保证连续。
 * 
 * 改进目标:
 * 能不能找到一种,既不依赖DB,也不依赖时间的ID生成算法呢?答案是,肯定有的,这是我们努力的方向!
 * 
<p> ---------------------------------------------
<p> org.teasoft.honey.distribution.PearFlowerId
<p>改进的雪花算法——姑且称为梨花算法(PearFlowerId)吧  (忽如一夜春风来,千树万树梨花开)。
<p>改进目标:解决雪花算法的时钟回拨问题;部分避免机器id重复时,号码冲突问题。

<p>long型的64位分成以下几个部分组成:
<p>符号位:1位
<p>时间:31位   (精确到秒)够用68年
<p>段号(批次号):3位    每秒可分为8个段
<p>机器号:10位   最多支持1024台机器
<p>序列号:19位    可表示:0--524287

<p>注:根据情况,机器号可以调整到最后部分。
<p>每个机器,每秒可生成4.19w个id(4194304).

<p>   经过调整,时间只对秒灵敏,成功回避了服务器间几百毫秒的时间误差引起的时间回拨问题;若第59秒的8个段号没有用完,
<p>则当润秒来临时,还可继续使用。另外具体实现上,可设置一定的秒数(如3秒)内提前消费。比如第10秒的号码,在800毫
<p>秒用完了,可以继续使用第11秒的号码。这样,下1秒用的号码不是很多时,就可以借给上1秒使用。
<p>   以上的方案是与时间强相关的。若某一段时间内的号码没用使用,也会浪费掉。当在分布式DB的表主键这种应用场景时,
<p>   只需要全局id不重复,且是递增的。类似这种场景,可以设计成时间不相关的。
<p>   供分布式DB表主键等类似场景使用,不浪费号码的方案。long型的64位分配还是一样。只不过,取号时,是取上一个号码加1,
<p>而不用管现在的时间是什么时候。当突然down机时,重启又获取当前的时间,重新开始分派号码;这时之前节省下的号码就被浪
<p>费掉了。为解决这个问题,可以在一段时间或分派一定数量的号(如10000),就将当前分派的号码记录到日志,或同步到DB表,
<p>等重启时,可以设置初始值。实现上,还是要控制分派的速度,若每秒几百万的号不够用,可用表名分隔命名空间,每个表单独取
<p>自己的号;即使号码够用,也可以这样做,因为这样得到的号在同一张表里就比较连续,而不只是递增而矣。当各个机器分派的id
<p>速度相差太大时,各机器得到的id大小就比较乱;这种问题,可以设置负载均衡,让每台机器轮流出号。

<p>   机器id重复的问题。当两台机器的id一样时,分派的号就会重复。若0-7八个段号(段号3位),每次都是从0-3随机获取一个
<p>开始的段号,比方说获取到2,那重复机器id的服务要是获取到0或1的段号就可以避免号码重复的冲突。当然了,这都是基于每秒用
<p>不完号码的情况下的。可以循环使用段号,如获取到3,那就从3-7,0,1,2这样使用段号,后面0,1,2这几个段号要是分派出去,
<p>号码就不递增了。具体怎么用,还是要根据自己的情况做取舍。

<p> ---------------------------------------------
<p> org.teasoft.honey.distribution.SerialUniqueId
 * 在一个workerid内连续唯一的ID生成方法(绝对连续单调递增,全局唯一).Serial Unique Id in one workerid.
 * 优点:连续唯一;不依赖时钟. 在DB内实现,可达到分布式全局唯一ID在DB内自增长;在同一个workerid内,获取的ID号,可以满足连续单调递增唯一.
 * Advantages:continuous and unique;less clock dependent.Implemented in dB, it can achieve auto increment 
 * of distributed global unique ID in dB. The ID number get in the same workerid can satisfy continuous
 * monotonic increasing uniqueness.
 * 缺点/Shortcoming:worker1's ID<worker2's ID...<worker1023's ID.
 * 
 * SerialUniqueId:绝对连续单调递增,全局唯一.
 * 分布式环境下生成连续单调递增(在一个workerid内),且全局唯一数字id.
 * 连续单调递增ID生成算法SerialUniqueId:不依赖于时间,也不依赖于任何第三方组件,只是启动时,用一个时间作为第一个ID设置的种子,
 * 设置了初值ID后,就可获取并递增ID。在一台DB内与传统的一样,连续单调递增(而不只是趋势递增),而代表DB的workerid作为DB的区别放在高位,
 * 从所有DB节点看,则满足分布式DB生成全局唯一ID。本地(C8 I7 16g)1981ms可生成1亿个ID号,利用上批获取,分隔业务,每秒生成过亿ID号
 * 不成问题。可用作分布式DB内置生成64位long型ID自增主键。只要按本算法设置了记录的ID初值,然后默认让数据库表id主键自增就可以(如MYSQL)。
 * 绝对连续单调递增,全局唯一的方案(可用于DB表主键),如下:
 * 只能是在新增一个库时,就分配一个库的workerid. 然后在初始化表时,设置初始ID开始用的值,以后由DB自动增长。Workerid的分配可统一放在一个
 * 配置文件,由工具检测到某个表是空表,且使用的主键对应的是Java的long型时,设置初始ID开始用的值。
 * 
 * 考虑到2019年双11的峰值不超过55万笔/秒, 因此419w/s这个值已可以满足此苛刻要求;采用testSpeedLimit()检测平均值不超过419w/s这个
 * 值即可,而且在空闲时段省下的ID号,还可以在高峰期时使用。
 
 * 这个都被大家忽略了:
 * DB表自增ID,也是可以改为具有分布式特性的,SerialUniqueId就是!


<p> ---------------------------------------------
<p> org.teasoft.honey.distribution.OneTimeSnowflakeId
 * OneTimeSnowflakeId,进一步改进了梨花算法。
 * 不依赖时间的梨花算法,Workerid应放在序号sequence的上一段,且应用SerialUniqueId算法,使ID不依赖于时间自动递增。
 * 使用不依赖时间的梨花算法OneTimeSnowflakeId,应保证各节点大概均衡轮流出号,这样入库的ID会比较有序,因此每个段号内的序列号不能太多。
 * 支持批获取ID号。可以一次取一批ID(即一个范围内的ID一次就可以获取了)。可以代替依赖DB的号段模式。
 * 应用订单号等有安全要求的场景,可随机不定时获取一些批的号码不用即可。
 * 考虑到2019年双11的峰值不超过55万笔/秒, 因此419w/s这个值已可以满足此苛刻要求;采用testSpeedLimit()检测平均值不超过419w/s这个值即可,而且在空闲时
 * 段省下的ID号,还可以在高峰时使用。
 * 
 */
public interface GenId {
	
	/**
	 * 返回id号码.return the id number.
	 * @return long id号码.long id number.
	 */
	public long get();
	
	/**
	 * 一次获取一段号码,返回一个批次可用号码的最小值和最大值,eg: [100000,101000].return the min and max long in this batch.eg: [100000,101000]
	 * @param sizeOfIds 返回的一批id的数量.the size Of ids in one batch.
	 * @return return the array of long. array[0]=min,array[1]=max.
	 */
	public long[] getRangeId(int sizeOfIds);

}

package org.teasoft.bee.distribution;

/**
 * 用于唯一标识在一个运行的ID生成程序.uniquely identify the ID generator running in an application.
 */
public interface Worker {
	
	/**
	 * 获取唯一标识ID生成程序的ID编号.get long number of unique worker ID.
	 * @return long number of unique worker ID.
	 */
	long getWorkerId();

}

薄雾算法(Mist)

薄雾算法是不同于 snowflake 的全局唯一 ID 生成算法,相比 snowflake,薄雾算法具有更高的数值上限(百万亿级)和更长的使用期限,并且薄雾算法不受时间回拨影响。

考量了什么业务场景和要求呢?
用到全局唯一 ID 的场景不少,这里引用美团 Leaf 的场景介绍:

在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识。如在美团点评的金融、支付、餐饮、酒店、猫眼电影等产品的系统中,数据日渐增长,对数据分库分表后需要有一个唯一 ID 来标识一条数据或消息,数据库的自增 ID 显然不能满足需求;特别一点的如订单、骑手、优惠券也都需要有唯一 ID 做标识。此时一个能够生成全局唯一ID 的系统是非常必要的。

引用微信 seqsvr 的场景介绍:

微信在立项之初,就已确立了利用数据版本号实现终端与后台的数据增量同步机制,确保发消息时消息可靠送达对方手机。

爬虫数据服务的场景介绍:

数据来源各不相同,且并发极大的情况下难以生成统一的数据编号,同时数据编号又将作为爬虫下游整个链路的溯源依据,在爬虫业务链路中十分重要。

这里参考美团 Leaf 的要求:

1、全局唯一性:不能出现重复的 ID 号,既然是唯一标识,这是最基本的要求;

2、趋势递增:在 MySQL InnoDB 引擎中使用的是聚集索引,由于多数 RDBMS 使用 B-tree 的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能;

3、单调递增:保证下一个 ID 一定大于上一个 ID,例如事务版本号、IM 增量消息、排序等特殊需求;

4、信息安全:如果 ID 是连续的,恶意用户的爬取工作就非常容易做了,直接按照顺序下载指定 URL 即可;如果是订单号就更危险了,竞对可以直接知道我们一天的单量。所以在一些应用场景下,会需要 ID 无规则、不规则;

可以用“全局不重复,不可猜测且呈递增态势”这句话来概括描述要求。

薄雾算法的设计思路是怎么样的?
薄雾算法采用了与 snowflake 相同的位数——64,在考量业务场景和要求后并没有沿用 1-41-10-12 的占位,而是采用了 1-47-8-8 的占位。即:

* 1      2                                                     48         56       64
* +------+-----------------------------------------------------+----------+----------+
* retain | increas                                             | salt     | salt |
* +------+-----------------------------------------------------+----------+----------+
* 0      | 0000000000 0000000000 0000000000 0000000000 0000000 | 00000000 | 00000000 |
* +------+-----------------------------------------------------+------------+--------+

第一段为最高位,占 1 位,保持为 0,使得值永远为正数;
第二段放置自增数,占 47 位,自增数在高位能保证结果值呈递增态势,遂低位可以为所欲为;
第三段放置随机因子一,占 8 位,上限数值 255,使结果值不可预测;
第四段放置随机因子二,占 8 位,上限数值 255,使结果值不可预测;
薄雾算法生成的数值是什么样的?
薄雾自增数为 1~10 的运行结果类似如下:

171671
250611
263582
355598
427749
482010
581550
644278
698636
762474

根据运行结果可知,薄雾算法能够满足“全局不重复,不可猜测且呈递增态势”的场景要求。

薄雾算法 mist 和雪花算法 snowflake 有何区别?
snowflake 是由 Twitter 公司提出的一种全局唯一 ID 生成算法,它具有“递增态势、不依赖数据库、高性能”等特点,自 snowflake 推出以来备受欢迎,算法被应用于大大小小公司的服务中。snowflake 高位为时间戳的二进制,遂完全受到时间戳的影响,倘若时间回拨(当前服务器时间回到之前的某一时刻),那么 snowflake 有极大概率生成与之前同一时刻的重复 ID,这直接影响整个业务。

snowflake 受时间戳影响,使用上限不超过 70 年。

薄雾算法 Mist 由书籍《Python3 反爬虫原理与绕过实战》的作者韦世东综合 百度 UidGenerator、 美团 Leaf 和 微信序列号生成器 seqsvr 中介绍的技术点,同时考虑高性能分布式序列号生成器架构后设计的一款“递增态势、不依赖数据库、高性能且不受时间回拨影响”的全局唯一序列号生成算法。

mistSturct

薄雾算法不受时间戳影响,受到数值大小影响。薄雾算法高位数值上限计算方式为int64(1<<47 - 1),上限数值140737488355327 百万亿级,假设每天消耗 10 亿,薄雾算法能使用 385+ 年。

为什么薄雾算法不受时间回拨影响?
snowflake 受时间回拨影响的根本原因是高位采用时间戳的二进制值,而薄雾算法的高位是按序递增的数值。结果值的大小由高位决定,遂薄雾算法不受时间回拨影响。

为什么说薄雾算法的结果值不可预测?
考虑到“不可预测”的要求,薄雾算法的中间位是 8 位随机值,且末 8 位是也是随机值,两组随机值大大增加了预测难度,因此称为结果值不可预测。

中间位和末位随机值的开闭区间都是 [0, 255],理论上随机值可以出现 256 * 256 种组合。

当程序重启,薄雾算法的值会重复吗?
snowflake 受时间回拨影响,一旦时间回拨就有极大概率生成重复的 ID。薄雾算法中的高位是按序递增的数值,程序重启会造成按序递增数值回到初始值,但由于中间位和末尾随机值的影响,因此不是必定生成(有大概率生成)重复 ID,但递增态势必定受到影响。

薄雾算法的值会重复,那我要它干嘛?
1、无论是什么样的全局唯一 ID 生成算法,都会有优点和缺点。在实际的应用当中,没有人会将全局唯一 ID 生成算法完全托付给程序,而是会用数据库存储关键值或者所有生成的值。全局唯一 ID 生成算法大多都采用分布式架构或者主备架构提供发号服务,这时候就不用担心它的重复问题;

2、生成性能比雪花算法高太多倍;

3、代码少且简单,在大型应用中,单功能越简单越好;

是否提供薄雾算法的工程实践或者架构实践?
是的,作者的另一个项目 Medis 是薄雾算法与 Redis 的结合,实现了“全局不重复”,你再也不用担心程序重启带来的问题。

薄雾算法的分布式架构,推荐 CP 还是 AP?
CAP 是分布式架构中最重要的理论,C 指的是一致性、A 指的是可用性、P 指的是分区容错性。CAP 当中,C 和 A 是互相冲突的,且 P 一定存在,遂我们必须在 CP 和 AP 中选择。实际上这跟具体的业务需求有关,但是对于全局唯一 ID 发号服务来说,大多数时候可用性比一致性更重要,也就是选择 AP 会多过选择 CP。至于你怎么选,还是得结合具体的业务场景考虑。

薄雾算法的性能测试
采用 Golnag(1.14) 自带的 Benchmark 进行测试,测试机硬件环境如下:

内存 16 GB 2133 MHz LPDDR3
处理器 2.3 GHz 双核Intel Core i5
操作系统 macOS Catalina
机器 MacBook Pro (13-inch, 2017, Two Thunderbolt 3 ports)

进行了多轮测试,随机取 3 轮测试结果。以此计算平均值,得 单次执行时间 346 ns/op。以下是随机 3 轮测试的结果:

goos: darwin
goarch: amd64
pkg: mist
BenchmarkMain-4          3507442               339 ns/op
PASS
ok      mist    1.345s
goos: darwin
goarch: amd64
pkg: mist
BenchmarkMain-4          3488708               338 ns/op
PASS
ok      mist    1.382s
goos: darwin
goarch: amd64
pkg: mist
BenchmarkMain-4          3434936               360 ns/op
PASS
ok      mist    1.394s

全局唯一 ID 是分布式系统和订单类业务系统中重要的基础设施。这里引用美团的描述:

在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识。如在美团点评的金融、支付、餐饮、酒店、猫眼电影等产品的系统中,数据日渐增长,对数据分库分表后需要有一个唯一
ID 来标识一条数据或消息,数据库的自增 ID 显然不能满足需求;特别一点的如订单、骑手、优惠券也都需要有唯一 ID 做标识。

为什么一定要全局唯一 ID?

举一个场景,在 MySQL 分库分表的条件下,MySQL 无法做到依次、顺序、交替地生成 ID,这时候要保证数据的顺序,全局唯一 ID 就是一个很好的选择。

在爬虫场景中,这条数据在进入数据库之前会进行数据清洗、校验、矫正、分析等多个流程,这期间有一定概率发生重试或设为异常等操作,也就是说在进入数据库之前它就需要有一个 ID 来标识它。

全局唯一 ID 应当具备什么样的属性,才能够满足上述的场景呢?
美团技术团队列出的 4 点属性我觉得很准确,它们是:

全局唯一性:不能出现重复的 ID 号,既然是唯一标识,这是最基本的要求; 趋势递增:在 MySQL InnoDB
引擎中使用的是聚集索引,由于多数 RDBMS 使用 B-tree
的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能; 单调递增:保证下一个 ID 一定大于上一个
ID,例如事务版本号、IM 增量消息、排序等特殊需求; 信息安全:如果 ID
是连续的,恶意用户的爬取工作就非常容易做了,直接按照顺序下载指定 URL
即可;如果是订单号就更危险了,竞争对手可以直接知道我们一天的单量。所以在一些应用场景下,会需要 ID 无规则、不规则。

看上去第 3 点和第 4 点似乎还存在些许冲突,这个后面再说。除了以上列举的 ID 属性外,基于这个生成算法构建的服务还需要买足高 QPS、高可用性和低延迟的几个要求。

业内常见的 ID 生成方式有哪些?
都学过 UUID 和 GUID,它们生成的值看上去像这样:

6F9619FF-8B86-D011-B42D-00C04FC964FF

由于不是纯数字组成,这就无法满足趋势递增和单调递增这两个属性,同时在写入时也会降低写入性能。上面提到了数据库自增 ID 无法满足入库前使用和分布式场景下的需求,遂排除。

有人提出了借助 Redis 来实现,例如订单号=日期+当日自增长号,自增长通过 INCR 实现。但这样操作的话又无法满足编号不可猜测需求。

这时候有人提出了 MongoDB 的 ObjectID,不要忘了它生成的 ID 是这样的: 5b6b3171599d6215a8007se0,和 UUID 一样无法满足递增属性,且和 MySQL 一样要入库后才能生成。

难道就没有能打的了吗?

大名鼎鼎的 Snowflake
Twitter 于 2010 年开源了内部团队在用的一款全局唯一 ID 生成算法 Snowflake,翻译过来叫做雪花算法。Snowflake 不借助数据库,可直接由编程语言生成,它通过巧妙的位设计使得 ID 能够满足递增属性,且生成的 ID 并不是依次连续的,能够满足上面提到的全局唯一 ID 的 4 个属性。它连续生成的 3 个 ID 看起来像这样:

563583455628754944
563583466173235200
563583552944996352

Snowflake 以 64 bit 来存储组成 ID 的4 个部分:

1、最高位占1 bit,值固定为 0,以保证生成的 ID 为正数;

2、中位占 41 bit,值为毫秒级时间戳;

3、中下位占 10 bit,值为工作机器的 ID,值的上限为 1024;

4、末位占 12 bit,值为当前毫秒内生成的不同 ID,值的上限为 4096;

Snowflake 的代码实现网上有很多款,基本上各大语言都能找到实现参考。通过 Golang 代码实现:

package main

import (
	"errors"
	"fmt"
	"sync"
	"time"
)

const (
	twepoch        = int64(1483228800000)             //开始时间截 (2017-01-01)
	workeridBits   = uint(10)                         //机器id所占的位数
	sequenceBits   = uint(12)                         //序列所占的位数
	workeridMax    = int64(-1 ^ (-1 << workeridBits)) //支持的最大机器id数量
	sequenceMask   = int64(-1 ^ (-1 << sequenceBits)) //
	workeridShift  = sequenceBits                     //机器id左移位数
	timestampShift = sequenceBits + workeridBits      //时间戳左移位数
)

// A Snowflake struct holds the basic information needed for a snowflake generator worker
type Snowflake struct {
	sync.Mutex
	timestamp int64
	workerid  int64
	sequence  int64
}

// NewNode returns a new snowflake worker that can be used to generate snowflake IDs
func NewSnowflake(workerid int64) (*Snowflake, error) {

	if workerid < 0 || workerid > workeridMax {
		return nil, errors.New("workerid must be between 0 and 1023")
	}

	return &Snowflake{
		timestamp: 0,
		workerid:  workerid,
		sequence:  0,
	}, nil
}

// Generate creates and returns a unique snowflake ID
func (s *Snowflake) Generate() int64 {

	s.Lock()

	now := time.Now().UnixNano() / 1000000

	if s.timestamp == now {
		s.sequence = (s.sequence + 1) & sequenceMask

		if s.sequence == 0 {
			for now <= s.timestamp {
				now = time.Now().UnixNano() / 1000000
			}
		}
	} else {
		s.sequence = 0
	}

	s.timestamp = now

	r := int64((now-twepoch)<<timestampShift | (s.workerid << workeridShift) | (s.sequence))

	s.Unlock()
	return r
}

func main() {
	snow := Snowflake{}
	for i := 0; i < 3; i++ {
		id := snow.Generate()
		fmt.Println(id)
	}
}


Snowflake 存在的问题
snowflake 不依赖数据库,也不依赖内存存储,随时可生成 ID,这也是它如此受欢迎的原因。但因为它在设计时通过时间戳来避免对内存和数据库的依赖,所以它依赖于服务器的时间。上面我们提到了 Snowflake 的 4 段结构,实际上影响 ID 大小的是较高位的值,由于最高位固定为 0,遂影响 ID 大小的是中位的值,也就是时间戳。

试想,服务器的时间发生了错乱或者回拨,这就直接影响到生成的 ID,有很大概率生成重复的 ID 且一定会打破递增属性。这是一个致命缺点,你想想,支付订单和购买订单的编号重复,这是多么严重的问题!

另外,由于它的中下位和末位 bit 数限制,它每毫秒生成 ID 的上限严重受到限制。由于中位是 41 bit 的毫秒级时间戳,所以从当前起始到 41 bit 耗尽,也只能坚持 70 年。

再有,程序获取操作系统时间会耗费较多时间,相比于随机数和常数来说,性能相差太远,这是制约它生成性能的最大因素。

一线企业如何解决全局唯一 ID 问题,看看百度、美团、腾讯(微信)是如何做的。

百度团队开源了 UIDGenerator 算法.

它通过借用未来时间和双 Buffer 来解决时间回拨与生成性能等问题,同时结合 MySQL 进行 ID 分配。这是一种基于 Snowflake 的优化操作,是一个好的选择,你认为这是不是优选呢?

美团团队根据业务场景提出了基于号段思想的 Leaf-Segment 方案和基于 Snowflake 的 Leaf-Snowflake 方案.

出现两种方案的原因是 Leaf-Segment 并没有满足安全属性要求,容易被猜测,无法用在对外开放的场景(如订单)。Leaf-Snowflake 通过文件系统缓存降低了对 ZooKeeper 的依赖,同时通过对时间的比对和警报来应对 Snowflake 的时间回拨问题。这两种都是一个好的选择,你认为这是不是优选呢?

微信团队业务特殊,它有一个用 ID 来标记消息的顺序的场景,用来确保我们收到的消息就是有序的。在这里不是全局唯一 ID,而是单个用户全局唯一 ID,只需要保证这个用户发送的消息的 ID 是递增即可。

这个项目叫做 Seqsvr,它并没有依赖时间,而是通过自增数和号段来解决生成问题的。这是一个好的选择,你认为这是不是优选呢?

性能高出 Snowflake 587 倍的算法是如何设计的?
在了解 Snowflake 的优缺点、阅读了百度 UIDGenertor、美团 Leaf 和腾讯微信 Seqsvr 的设计后,我希望设计出一款能够满足全局唯一 ID 4 个属性且性能更高、使用期限更长、不受单位时间限制、不依赖时间的全局唯一 ID 生成算法。

这看起来很简单,但吸收所学知识、设计、实践和性能优化占用了我 4 个周末的时间。在我看来,这个算法的设计过程就像是液态的水转换为气状的雾一样,遂我给这个算法取名为薄雾(Mist)算法。接下来我们来看看薄雾算法是如何设计和实现的。

位数是影响 ID 数值上限的主要因素,Snowflake 中下位和末位的 bit 数限制了单位时间内生成 ID 的上限,要解决这个两个问题,就必须重新设计 ID 的组成。

抛开中位,我们先看看中下位和末位的设计。中下位的 10 bit 的值其实是机器编号,末位 12 bit 的值其实是单位时间(同一毫秒)内生成的 ID 序列号,表达的是这毫秒生成的第 5 个或第 150 个 数值,同时二者的组合使得 ID 的值变幻莫测,满足了安全属性。实际上并不需要记录机器编号,也可以不用管它到底是单位时间内生成的第几个数值,安全属性我们可以通过多组随机数组合的方式实现,随着数字的递增和随机数的变幻,通过 ID 猜顺序的难度是很高的。

最高位固定是 0,不需要对它进行改动。我们来看看至关重要的中位,Snowflake 的中位是毫秒级时间戳,既然不打算依赖时间,那么肯定也不会用时间戳,用什么呢?我选择自增数 1,2,3,4,5,…。中位决定了生成 ID 的上限和使用期限,如果沿用 41 bit,那么上限跟用时间戳的上限相差无几,经过计算后我选择采用与 Snowflake 的不同的分段:

缩减中下位和末位的 bit 数,增加中位的 bit 数,这样就可以拥有更高的上限和使用年限,那上限和年限现在是多久呢?中位数值的上限计算公式为 int64(1<<47 - 1),计算结果为 140737488355327 。百万亿级的数值,假设每天消耗 10 亿 ID,薄雾算法能用 385+ 年,几辈子都用不完。

中下位和末位都是 8 bit,数值上限是 255,即开闭区间是 [0, 255]。这两段如果用随机数进行填充,对应的组合方式有 256 * 256 种,且每次都会变化,猜测难度相当高。由于不像 Snowflake 那样需要计算末位的序列号,遂薄雾算法的代码并不长,具体如下:

/*
* 薄雾算法
*
* 1      2                                                     48         56       64
* +------+-----------------------------------------------------+----------+----------+
* retain | increas                                             | salt     | sequence |
* +------+-----------------------------------------------------+----------+----------+
* 0      | 0000000000 0000000000 0000000000 0000000000 0000000 | 00000000 | 00000000 |
* +------+-----------------------------------------------------+------------+--------+
*
* 0. 最高位,占 1 位,保持为 0,使得值永远为正数;
* 1. 自增数,占 47 位,自增数在高位能保证结果值呈递增态势,遂低位可以为所欲为;
* 2. 随机因子一,占 8 位,上限数值 255,使结果值不可预测;
* 3. 随机因子二,占 8 位,上限数值 255,使结果值不可预测;
*
* 编号上限为百万亿级,上限值计算为 140737488355327 即 int64(1 << 47 - 1),假设每天取值 10 亿,能使用 385+ 年
 */

package main

import (
	"crypto/rand"
	"math/big"
	"sync"
)

const saltBit = uint(8)                  // 随机因子二进制位数
const saltShift = uint(8)                // 随机因子移位数
const increasShift = saltBit + saltShift // 自增数移位数

type Mist struct {
	sync.Mutex       // 互斥锁
	increas    int64 // 自增数
	saltA      int64 // 随机因子一
	saltB      int64 // 随机因子二
}

/* 初始化 Mist 结构体*/
func NewMist() *Mist {
	mist := Mist{increas: 1}
	return &mist
}

/* 生成唯一编号 */
func (c *Mist) Generate() int64 {
	c.Lock()
	c.increas++
	// 获取随机因子数值 | 使用真随机函数提高性能
	randA, _ := rand.Int(rand.Reader, big.NewInt(255))
	c.saltA = randA.Int64()
	randB, _ := rand.Int(rand.Reader, big.NewInt(255))
	c.saltB = randB.Int64()
	// 通过位运算实现自动占位
	mist := int64((c.increas << increasShift) | (c.saltA << saltShift) | c.saltB)
	c.Unlock()
	return mist
}

 // 使用方法
  func main() {
 	
 	mist := NewMist()
 	for i := 0; i < 10; i++ {
 		fmt.Println(mist.Generate())
 	}
 }

性能问题,获取时间戳是比较耗费性能的,不获取时间戳速度当然快了,那 500+ 倍是如何得来的呢?以 Golang 为例,Golang 随机数有三种生成方式:

基于固定数值种子的随机数;
将会变换的时间戳作为种子的随机数;
大数真随机;
基于固定数值种子的随机数每次生成的值都是一样的,是伪随机,不可用在此处。将时间戳作为种子以生成随机数是目前 Golang 开发者的主流做法,实测性能约为 8800 ns/op。大数真随机知道的人比较少,实测性能 335ns/op,由此可见性能相差近 30 倍。

大数真随机也有一定的损耗,如果想要将性能提升到顶点,只需要将中下位和末位的随机数换成常数即可,常数实测性能 15ns/op,是时间戳种子随机数的 587 倍。

要注意的是,将常数放到中下位和末位的性能是很高,但是猜测难度也相应下降。

薄雾算法的依赖问题
薄雾算法为了避开时间依赖,不得不依赖存储,中位自增的数值只能在内存中存活,遂需要依赖存储将自增数值存储起来,避免因为宕机或程序异常造成重复 ID 的事故。

看起来是这样,但它真的是依赖存储吗?

你想想,这么重要的服务必定要求高可用,无论你用 Twitter 还是百度或者美团、腾讯微信的解决方案,在架构上一定都是高可用的,高可用一定需要存储。在这样的背景下,薄雾算法的依赖其实并不是额外的依赖,而是可以与架构完全融合到一起的设计。

薄雾算法和 Redis 的结合
既然提出了薄雾算法,怎么能不提供真实可用的工程实践呢?在编写完薄雾算法之后,我就开始了工程实践的工作,将薄雾算法与 KV 存储结合到一起,提供全局唯一 ID 生成服务。这里我选择了较为熟悉的 Redis,Mist 与 Redis 的结合,我为这个项目取的名字为 Medis。

在大基数条件下的性能约为 2.5w/sec。这么高的性能除了薄雾算法本身高性能之外,Medis 的设计也作出了很大贡献:

使用 Channel 作为数据缓存,这个操作使得发号服务性能提升了 7 倍; 采用预存预取的策略保证 Channel
在大多数情况下都有值,从而能够迅速响应客户端发来的请求; 用 Gorouting 去执行耗费时间的预存预取操作,不会影响对客户端请求的响应;
采用 Lrange Ltrim 组合从 Redis 中批量取值,这比循环单次读取或者管道批量读取的效率更高; 写入 Redis
时采用管道批量写入,效率比循环单次写入更高; Seqence
值的计算在预存前进行,这样就不会耽误对客户端请求的响应,虽然薄雾算法的性能是纳秒级别,但并发高的时候也造成一些性能损耗,放在预存时计算显然更香;
得益于 Golang Echo 框架和 Golang 本身的高性能,整套流程下来我很满意,如果要追求极致性能,试试 Rust。

高可用架构和分布式性能
分布式 CAP (一致性、可用性、分区容错性)已成定局,这类服务通常追求的是可用性架构(AP)。由于设计中采用了预存预取,且要保持整体顺序递增,遂单机提供访问是优选,即分布式架构下的性能上限就是提供服务的那台主机的单机性能。

想要实现分布式多机提供服务?

这样的需求要改动 Medis 的逻辑,同时也需要改动各应用之间的组合关系。如果要实现分布式多机同时提供服务,那么就要废弃 Redis 和 Channel 预存预取机制,接着放弃 Channel 而改用即时生成,这样便可以同时使用多个 Server,但性能的瓶颈就转移到了 KV 存储(这里是 Redis),性能等同于单机 Redis 的性能。你可以采用 ETCD 或者 Zookeeper 来实现多 KV,但这不是又回到了 CAP 原点了吗?

至于怎么选择,可根据实际业务场景和需求与架构进行讨论,选择一个适合的方案进行部署即可。


其他

看大厂的设计和权衡。

百度 UIDGenertor:点击查看

美团 Leaf:点击查看

腾讯 Seqsvr: 点击查看

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

代码讲故事

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

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

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

打赏作者

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

抵扣说明:

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

余额充值