分布式系列之ID生成器

背景

在分布式系统中,当数据库数据量达到一定量级后,需要进行数据拆分、分库分表操作,传统使用方式的数据库自有的自增特性产生的主键ID已不能满足拆分的需求,它只能保证在单个表中唯一,所以需要一个在分布式环境下都能使用的全局唯一ID。

应用场景

  • 用户ID、图片ID等各种业务场景
  • 分库分表情况下的订单号
  • 分布式链路追踪系统中的TraceId

需求分析:

  • 可靠性:全局唯一性,不能生成重复的ID,最基本的要求
  • 安全性:保证数据安全,防止恶意用户分析出ID生成规则,进而获取到系统业务信息
  • 递增性:下一个时间点产生的ID大于前一个时间点的ID
  • 时间有序:以时间为序,或ID里包含时间。表设计时可以不用考虑再添加一个时间字段,也方便冷热数据分离
  • 可读性:生成的ID应该有业务意义
  • 长度适中:不要太长,太长则数据表存储不便,可使用Long类型(String也行)
  • 分片支持:可以控制ShardingId。如某一个用户的文章要放在同一个分片内,这样查询效率高,修改也容易
  • 高可用:不能出现单点故障
  • 高性能:响应速度快,毫秒内生成的ID数量要满足海量用户请求
  • 扩展性:ID生成器服务集群发生节点宕机,加入新节点是否便捷
  • 可维护性:实现方案不能太复杂,方便后期维护

上面列举出11个需求点,已经足够多,当然也可以再增加。安全性和递增性之间存在一定的互斥,需要做取舍。递增性不强求绝对严格递增,即不需要满足+1递增。在做方案设计时,需要具体情况具体分析,前面几个是必须要满足。大体而言,首先确保满足前面几个需求点,即优先级更高,再考虑后面几个。能满足的需求点越多,则方案越复杂,需要加以权衡和取舍,不强求完全实现所有的需求点。

实现

实现方案有很多,不是每种方案都能完美实现上面提到的各个需求点:

  • 数据库
  • UUID
  • Snowflake
  • Redis
  • ZooKeeper
  • Snowflake-like

数据库

基于数据表auto increment规则来生成全局唯一递增ID。

优点:简单,可保证唯一性、递增性,步长固定

缺点:

  • 可用性:不高,数据库常见架构是一主多从+读写分离,生成自增ID是写请求,主库宕机,则服务不可用。
  • 扩展性:较差,性能有限,写入是单点,主库的写性能决定ID生成性能上限,且难以扩展
  • 兼容性:不同数据库语法和实现不同,数据库迁移时或多数据库版本支持时需要特殊处理

改进方法:冗余主库,避免写入单点;数据水平切分,保证各主库生成的ID不重复。

具体来说,比如可将1个写库变成N个写库,每个写库设置不同的auto increment初始值,和相同的步长,以保证每个数据库生成的ID是不同的。

-- db1
set @@auto_increment_offset = 1;     -- 起始值
set @@auto_increment_increment = 2;  -- 步长
-- db2
set @@auto_increment_offset = 2;     -- 起始值
set @@auto_increment_increment = 2;  -- 步长

改进后方案可提高可用性,但拓展性差的问题依旧存在。数据库写压力有所缓解,但写压力依旧存在。

号段模式

基于多个写数据库,从数据库批量的获取自增ID,每次从数据库取出一个号段范围,并加载到内存(或Redis)。

这种分布式ID生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多。

UUID

Universally Unique Identifier,标准型式包含32个16进制字符,以连字号分为五段,其形式为8-4-4-4-12,到目前为止业界一共有5种方式生成UUID,参考IETF发布的UUID规范

  • 版本1 - 根据时间和节点 ID(通常是MAC地址)生成;
  • 版本2 - 根据标识符(通常是组或用户ID)、时间和节点ID生成;
  • 版本3、版本5 - 确定性UUID,通过散列名字空间标识符和名称生成;版本5和3的区别在于使用不同的散列算法;
  • 版本4 - 使用随机性或伪随机性生成。

v1

UUID-v1是通过使用主机MAC地址和当前日期和时间的组合生成的。之外还引入另一个随机组件,以确保其唯一性。但是如果使用同一台机器、同时时间生成UUID,会有很小的几率重复。

UUID-v1存在的问题是:

  • 存在重复几率
  • 根据ID能推算出创建时的相对时间
  • 根据ID能推算出创建的机器唯一标识

v2

UUID-v2和v1很类似,是根据标识符(通常是组或用户ID)、时间和节点ID生成,区别在于v2将v1中的部分时间信息换成主机名, 存在隐私风险,未大规模使用。

v3

UUID-v3通过MD5散列算法基于命名空间标识符和名称生成UUID。和v1、v2不同,v3不依赖与机器信息和时间信息,但v3要求输入命名空间+名称,命名空间本身也是一个UUID,用来标识应用环境,名称通常是用户账号、用户名之类的内容,通过命名空间+名称+三列算法算出UUID。

UUID-v5和v3类似,区别在于使用sha1散列算法。

v4

基于随机数的算法。用SecureRandom生成16个随机的Byte,用2个long来存储。记得加-Djava.security.egd=file:/dev/./urandom

JDK里UUID.randomUUID静态方法使用加密强度高的伪随机数生成器生成v4伪随机UUID:

public static UUID randomUUID() {
	SecureRandom ng = Holder.numberGenerator;
	byte[] randomBytes = new byte[16];
	ng.nextBytes(randomBytes);
	randomBytes[6]  &= 0x0f;  /* clear version        */
	randomBytes[6]  |= 0x40;  /* set to version 4     */
	randomBytes[8]  &= 0x3f;  /* clear variant        */
	randomBytes[8]  |= (byte) 0x80;  /* set to IETF variant  */
	return new UUID(randomBytes);
}

JDK提供UUID.randomUUID静态方法从字节数组生成基于名称的v3版本的UUID:

public static UUID nameUUIDFromBytes(byte[] name) {
	MessageDigest md;
	try {
	    md = MessageDigest.getInstance("MD5");
	} catch (NoSuchAlgorithmException nsae) {
	    throw new InternalError("MD5 not supported", nsae);
	}
	byte[] md5Bytes = md.digest(name);
	md5Bytes[6]  &= 0x0f;  /* clear version        */
	md5Bytes[6]  |= 0x30;  /* set to version 3     */
	md5Bytes[8]  &= 0x3f;  /* clear variant        */
	md5Bytes[8]  |= (byte) 0x80;  /* set to IETF variant  */
	return new UUID(md5Bytes);
}

v1变种

Hibernate

基于hibernate-core-6.4.4.Final版本的CustomVersionOneStrategy源码如下:

public class CustomVersionOneStrategy implements UUIDGenerationStrategy, UuidGenerator.ValueGenerator {
	private final long mostSignificantBits;
	
	public int getGeneratedVersion() {
		return 1;
	}
	
	public CustomVersionOneStrategy() {
		byte[] hiBits = new byte[8];
		System.arraycopy(Helper.getAddressBytes(), 0, hiBits, 0, 4);
		System.arraycopy(Helper.getJvmIdentifierBytes(), 0, hiBits, 4, 4);
		hiBits[6] = (byte)(hiBits[6] & 15);
		hiBits[6] = (byte)(hiBits[6] | 16);
		this.mostSignificantBits = BytesHelper.asLong(hiBits);
	}
	
	public UUID generateUuid(SharedSessionContractImplementor session) {
		long leastSignificantBits = generateLeastSignificantBits(System.currentTimeMillis());
		return new UUID(this.mostSignificantBits, leastSignificantBits);
	}
	
	public UUID generateUUID(SharedSessionContractImplementor session) {
		return this.generateUuid(session);
	}
	
	public long getMostSignificantBits() {
		return this.mostSignificantBits;
	}
	
	public static long generateLeastSignificantBits(long seed) {
		byte[] loBits = new byte[8];
		short hiTime = (short)((int)(seed >>> 32));
		int loTime = (int)seed;
		System.arraycopy(BytesHelper.fromShort(hiTime), 0, loBits, 0, 2);
		System.arraycopy(BytesHelper.fromInt(loTime), 0, loBits, 2, 4);
		System.arraycopy(Helper.getCountBytes(), 0, loBits, 6, 2);
		loBits[0] = (byte)(loBits[0] & 63);
		loBits[0] = (byte)(loBits[0] | 128);
		return BytesHelper.asLong(loBits);
	}
}

解读:

MongoDB

MongoDB的bson-4.11.1版本下ObjectId的源码:

public final class ObjectId implements Comparable<ObjectId>, Serializable {
    private static final AtomicInteger NEXT_COUNTER = new AtomicInteger((new SecureRandom()).nextInt());

	public ObjectId() {
		this(new Date());
	}
	
	public ObjectId(Date date) {
		this(dateToTimestampSeconds(date), NEXT_COUNTER.getAndIncrement() & 16777215, false);
	}
	
	private static int dateToTimestampSeconds(Date time) {
		return (int)(time.getTime() / 1000L);
	}
}

解读:

  • 16777215:即LOW_ORDER_THREE_BYTES,一个24位(3字节)的整数,其十六进制表示为0xFFFFFF。机器标识符是一个3字节的值,而16777215是3字节整数的最大值。这意味着机器标识符的范围是0到16777215,确保可以使用一个唯一的标识符来表示每台机器。
  • SecureRandom:和JDK UUID一样使用SecureRandom来生成强随机数。

ObjectId使用12字节的存储空间:

  • 前4个字节表示时间戳,秒级别
  • 随后3个字节是机器标识码
  • 随后2个字节由进程id组成,同一台机器上可能会运行多个mongod实例,因此也需要加入进程标识符PID
  • 最后3个字节是随机数

前9个字节保证同一秒钟不同机器不同进程产生的ObjectId的唯一性。后三个字节是一个自动增加的计数器(一个mongod进程需要一个全局的计数器),保证同一秒的ObjectId是唯一的。同一秒钟最多允许每个进程拥有(256^3=16777216)个不同的ObjectId。

ObjectId用于文档的主键_id字段,_id可在服务端、客户端生成,在客户端生成可以降低服务器端的压力。

总结

缺点:

  • 不易于存储:UUID太长,以36个字符串(加上4个连字符)表示;不适合作为数据表主键,不利于建索引,UUID的无序性可能会引起数据位置频繁变动,严重影响性能
  • 没有排序:无法保证趋势递增
  • 可读性不好
  • 信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置

优点:性能非常高,本地生成,没有远程调用等网络消耗,时延低。

标准的UUID算法使用场景不多,改进版如MongoDB的ObjectId,可用于生产实践中。

Snowflake

参考GitHub。Twitter在把存储系统从MySQL迁移到Cassandra的过程中,由于Cassandra没有顺序ID生成机制,于是自己开发一套全局唯一ID生成服务。

共64位二进制:

  • 第1位固定为0,没有业务含义,sign符号位
  • 第2~42位,共41位,表示时间戳,用于存入精确到毫秒数的时间
  • 第43~52位,共10位,为机器ID位,其中高位5bit是数据中心ID(dataCenterId),低位5bit是工作节点ID(workerId)
  • 第53~64位,共12位,代表1ms内可以产生的序列号,取值区间为[0,4095],也就是说在数据中心ID和机器ID相同的情况下,1ms最多可以生成4096个序列号

如果序列号超过最大值,则会将程序阻塞到下一毫秒,然后序列号归零,继续生成ID。要想Snowflake生成全局唯一的ID,则ID生成器必须也是全局单例。

Snowflake对ZooKeeper的依赖性:集群节点启动时,从一个ZooKeeper集群获取,保证所有节点不会有重复的机器号

代码:

public class SnowflakeIdWorker {
	/**
	 * 机器id所占的位数
	 */
	private final long workerIdBits = 5L;
	/**
	 * 数据标识id所占的位数
	 */
	private final long datacenterIdBits = 5L;
	/**
	 * 工作机器ID(0~31)
	 */
	private final long workerId;
	/**
	 * 数据中心ID(0~31)
	 */
	private final long datacenterId;
	/**
	 * 序列ID位数
	 */
	private static final long sequenceBits = 12L;
	/**
	 * 毫秒内序列(0~4095)
	 */
	private long sequence = 0L;
	
	/**
	 * 上次生成ID的时间截
	 */
	private long lastTimestamp = -1L;
	/**
	 * 构造函数
	 */
	public SnowflakeIdWorker(long workerId, long datacenterId) {
		// 支持的最大机器id,结果是31 (这个移位算法可以很快地计算出几位二进制数所能表示的最大十进制数)
		long maxWorkerId = ~(-1L << workerIdBits);
		if (workerId > maxWorkerId || workerId < 0) {
			throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
		}
		// 支持的最大数据标识id,结果是31
		long maxDatacenterId = ~(-1L << datacenterIdBits);
		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;
	}
	
	/**
	 * 获得下一个ID(线程安全)
	 */
	public synchronized long nextId() {
		long timestamp = timeGen();
		// 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过,抛异常
		if (timestamp < lastTimestamp) {
			throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
		}
		// 如果是同一时间生成的,则进行毫秒内序列
		if (lastTimestamp == timestamp) {
			// 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
			long sequenceMask = ~(-1L << sequenceBits);
			sequence = (sequence + 1) & sequenceMask;
			// 毫秒内序列溢出
			if (sequence == 0) {
				// 阻塞到下一个毫秒,获得新的时间戳
				timestamp = tilNextMillis(lastTimestamp);
			}
		} else {
			// 时间戳改变,毫秒内序列重置
			sequence = 0L;
		}
		// 上次生成ID的时间截
		lastTimestamp = timestamp;
		// 移位并通过或运算拼到一起组成64位的ID
		// 开始时间截(2024-01-01)
		long startEpoch = 1704038400000L;
		// 数据标识id向左移17位(12+5)
		long datacenterIdShift = sequenceBits + workerIdBits;
		// 时间截向左移22位(5+5+12)
		long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
		return ((timestamp - startEpoch) << timestampLeftShift)
	            | (datacenterId << datacenterIdShift)
	            | (workerId << sequenceBits)
	            | sequence;
	}
	
	/**
	 * 阻塞到下一个毫秒,直到获得新的时间戳
	 *
	 * @param lastTimestamp 上次生成ID的时间截
	 * @return 当前时间戳
	 */
	protected long tilNextMillis(long lastTimestamp) {
		long timestamp = timeGen();
		while (timestamp <= lastTimestamp) {
			timestamp = timeGen();
		}
		return timestamp;
	}
	
	/**
	 * 返回以毫秒为单位的当前时间
	 */
	protected long timeGen() {
		return System.currentTimeMillis();
	}
}

如何分配datacenterId和workerId呢?

  • 写死 : 单机部署,然后写死两个值,不可取
  • 读配置文件 : 将值放在配置中心,应用启动时读取
  • 动态分配 :

存在的问题:

  • 趋势递增,而不是绝对递增;
  • 不能在一台服务器上部署多个分布式ID服务;
  • 时钟回拨
  • 时间戳只存在41位二进制,只能使用69年,69年后就可能产生重复ID
  • 如果机器性能足够好,每秒可以产生超过400万个ID,但是对于大部分企业来说,只需要每秒满足数万个ID即可。这种高性能浪费的主要是序号的二进制位,实际上,二进制位达到9位,就可以产生512个序号,如果机器性能足够,就可以每秒产生超过50万的ID,能满足绝大部分企业的需要
  • 从机器位来说,因为去中心化是分布式和微服务的趋势,所以在实现时,并未考虑受理机器编号,这样就会造成机器位数有10位二进制,可以表达区间[0, 1023]的整数。如果数据中心预估总共只有几十台机器,显然也会造成二进制位的浪费。

时钟回拨

指的是系统时间被回调,即系统时间突然被设置为过去的一个时间点。

具体到Snowflake,则表示获取到的当前Timestamp比前一个已生成ID的Timestamp还要小。做法是while循环,继续获取当前机器的时间,直到获取到更大的Timestamp才能继续工作,在这个等待过程中不能分配出新的ID。

解决方案:

  • 关闭系统NTP同步,这样就不会产生时钟调整;禁用后,需要手动管理系统时间,确保不会发生时间漂移。不推荐。
  • 系统做出判断,在时钟回拨这段时间,不生成ID直接返回ERROR_CODE,直到时钟追上恢复服务;不推荐。
if (timestamp < this.lastTimestamp) {
	throw new GenerateIdException("时钟回拨,拒绝生成ID");
}
  • 本地时钟与NTP的渐进同步:使用NTP的drift文件或其他配置来确保时间同步不会大幅度调整系统时钟,NTP可以通过逐步调整时间而非突然回调。
  • 系统做出判断,如果遇到超过容忍限度的回拨,上报报警系统,并把自身从集群节点中摘除
// 发生回拨
if (timestamp < this.lastTimestamp) {
	long delay = lastTimestamp - timestamp;
	// 偏差比较小则等待
	if (delay < 10) {
		Thread.sleep(delay);
	}
	timestamp = this.timeGen();
	// 监控判断
	if (timestamp < this.lastTimestamp) {
		timeCallBackProcess(timestamp, this.lastTimestamp);
	} else {
		// 重新分配ID
		long id = nextSeqId();
	}
}
  • 使用逻辑时钟:对于一些极端场景,使用逻辑时钟而不是物理时钟,确保ID的生成只依赖于系统的内部逻辑时间,不依赖于实际时间戳。
  • 系统做兼容处理,由于NTP网络回拨都是几十到几百毫秒,极少数到秒级别。系统中缓存最近几秒内最后的发号序号(具体范围请根据实际需要确定),存储格式为:时间秒-序号。这种回拨会产生以下几种结果:
    1. 当前秒数不变:当前是8:30秒100毫秒,NTP回拨50毫秒,当前时间变成8:30秒50毫秒,这个时候秒数没变,算法的时间戳部分不会产生重复,就不影响系统继续发号
    2. 当前秒数向前:当前是8:30秒800毫秒,NTP向前调整300毫秒,当前时间变成8:31秒100毫秒,由于这个时间还没发过号,不会生成重复ID
    3. 当前秒数向后:当前是8:30秒100毫秒,NTP回拨150毫秒,当前时间变成8:29秒950毫秒,这个时候秒发生回退,就可能产生重复ID。产生重复的原因在于秒回退后,算法的时间戳部分使用已经用过的时间戳,但是算法的序号部分,并没有回退到29秒那个时间对应的序号,依然使用当前的序号,如果序号也同时回退到29秒时间戳所对应的最后序号,就不会重复发号。解决方案如下:
Map<Long, Long> map = new ConcurrentHashMap<>();
// 发生回拨
if (timestampSec < this.lastTimestampSec) {
	// 有缓存
	if (map.get(timestampSec) != null) {
		this.sequence = map.get(timestampSec);
		this.nextId();
		map.put(timestampSec, this.sequence);
	} else {
		throw new GenerateIdException("时钟回拨,拒绝生成ID");
	}
}

NTP

Network Time Protocol,网络时间协议,主要功能是保持系统时间与标准时间服务器同步。NTP一般会通过小幅度调整系统时间来同步时钟(如加快或减慢系统时钟的频率);但在某些极端情况下,NTP可能会直接将时间回拨:

  • 服务器时钟的漂移较大
  • NTP服务器不稳定
  • 管理员手动调整服务器时间

直接关闭NTP服务有不少弊端:

  • 系统时间不同步:系统时间可能会逐渐偏离准确的时间,这种时间漂移会随着时间积累;
  • 影响分布式系统和集群协调
    在分布式系统或集群中,多个服务器之间需要同步时间来确保一致性。如果不同服务器的时间不一致,可能导致:
    • 事务失效:分布式事务依赖时间戳,时间不同步可能导致事务不一致,或者事务超时被错误处理;
    • 日志不一致:系统或应用程序日志记录时间不一致,难以排查问题,尤其在跨多个服务器的情况下;
    • 心跳机制异常:有些系统使用心跳机制监控节点状态,时间不同步可能导致误报或忽略实际的节点故障。
  • 安全问题
    许多安全协议依赖于系统时间的准确性,比如:
    • 证书验证:证书有有效期,系统时间错误可能导致证书验证失败,阻止系统与外部服务的安全连接;
    • Kerberos认证失败:Kerberos等基于时间的安全协议要求客户端和服务器时间一致,如果时间偏差过大,可能导致认证失败。
  • 影响时间敏感的应用
    某些应用依赖准确的时间来执行任务或调度,比如:
    • 金融系统:需要精确的时间戳来记录交易、订单等操作,时间偏差可能导致记录不准确,影响合规性和操作一致性;
    • 调度任务:定时任务调度依赖系统时间,如果时间不准确,任务可能会提前或延后执行,影响系统的可靠性。
  • 跨系统同步困难
    多个系统之间的时间同步非常重要。例如数据库、文件同步等系统如果时间不同步,可能会造成:
    • 数据冲突:例如文件系统或数据库中的时间戳用于确定版本或变更顺序,时间不同步可能导致版本冲突或数据丢失;
    • 无法正确处理跨系统的操作:时间偏差会影响不同系统之间的交互,例如备份、同步操作等。
  • 故障排查困难
    系统日志中的时间戳用于追踪事件发生的顺序,如果时间不准确,排查问题时很难精确地确定问题发生的时间点。

chrony

chrony有更好的时间管理机制,可以让时间同步时逐渐调整,而不是强制性回拨。

闰秒

闰秒,是指为保持协调世界时接近于世界时时刻,由国际计量局统一规定在年底或年中(也可能在季末)对协调世界时增加或减少1秒的调整。由于地球自转的不均匀性和长期变慢性(主要由潮汐摩擦引起的),会使世界时(民用时)和原子时之间相差超过到±0.9秒时,就把协调世界时向前拨1秒(负闰秒,最后一分钟为59秒)或向后拨1秒(正闰秒,最后一分钟为61秒),闰秒一般加在公历年末或公历六月末。

在闰秒产生时,系统会出现秒级时间调整,闰秒对发号器的影响:

  • 负闰秒:当前23:59:58的下一秒就是第二天00:00:0000:00:00这个时间还没产生过ID,不会产生重复,对发号器没影响;
  • 正闰秒:当天23:59:59的下一秒记为23:59:60,然后才是第二天的00:00:00。由于系统时间戳部分取的从某个时间点(1970年1月1日)到现在的秒数,是一个数字,只要这个数字不重复,就不会产生重复ID。如果在闰秒发生一段时间后NTP时间同步(为了规避闰秒风险,很多公司闰秒前关闭NTP同步,闰秒后打开NTP同步),这个时候系统时钟回拨,可以使用解决时钟回拨的方案进行处理。

Redis

基于Redis的原子操作INCR和INCRBY来实现ID的原子性自增,与数据库改进版类似,Redis采用集群化部署方案,每个节点设置一个不同的初始值,步长保持一致。且可以预生成一批ID,提高性能。

考虑到Redis持久化,RDB会定时生成快照文件,假如连续自增但Redis没及时持久化,而这会Redis挂掉,重启Redis后会出现ID重复的情况。

AOF会对每条写命令进行持久化,即使Redis挂掉也不会出现ID重复的情况,但由于incr命令的特殊性,会导致Redis重启恢复的数据时间过长。

ZooKeeper

Snowflake改进

业界最常用的解决方案是基于Snowflake的改进版。

Boundary flake

GitHub

Flake: A decentralized, k-ordered id generation service in Erlang

几点变化:

  • ID长度扩展到128位
  • 最高64位时间戳
  • 48位的Worker ID,和Mac地址一样长,启动时无需和ZooKeeper通讯获取Worker ID,做到完全去中心化
  • 16位的Seq Number
  • 基于Erlang

目的是用更多的位实现更小的冲突概率,且能支持更多的Worker同时工作,每毫秒能分配出更多的ID。

Instagram

Instagram的分布式存储方案:

  • 先把每个Table划分为多个逻辑分片(Logic Shard,简称LS)
  • 制定规则,每个LS被存储到哪个数据库实例上,数据库实例不需要很多。例如有2个PostgreSQL实例的系统,可将奇数逻辑分片存放到第一个数据库实例,偶数放到第二个
  • 每个Table指定一个字段作为分片字段,如用户表可指定uid作为分片字段
  • 插入一个新的数据时,先根据分片字段的值,决定数据被分配到哪个逻辑分片
  • 然后再根据逻辑分片和PostgreSQL实例的对应关系,确定这条数据应该被存放到哪台PostgreSQL实例上

Instagram unique ID组成:

  • 41位: 精确到毫秒的Timestamp,和Snowflake类似
  • 13位: 每个Logic Shard的代号,最大支持 2 13 2^{13} 213个LS
  • 10位: Sequence Number,简称SN,每个Shard每毫秒最多可以生成1024个ID

SN利用PostgreSQL表的自增序列(sequence)来生成:如果当前表上已经有5000条记录,则这个表的下一个自增序列就是5001(直接调用PG提供的方法可以获取到),然后把这个5001对1024取模就得到10位的SN。

优势在于:

  • 利用LS号来替换Snowflake使用的Worker号,就不需要到中心节点获取Worker号,做到完全去中心化
  • 通过ID可直接知道这条记录被存放在哪个LS上;数据迁移时,也是按LS为单位做数据迁移

开源

Leaf

美团-点评开源的Leaf

UidGenerator

UidGenerator是百度开源的,使用Java语言实现,基于Snowflake算法的分布式唯一ID生成器。

UidGenerator通过消费未来时间克服雪花算法的并发限制,提前生成一批ID并缓存在环形队列RingBuffer中。

TDDL

Taobao Distributed Data Layer,即淘宝分布式数据层,

Flyway

Flyway是一款开源的数据库版本管理工具,其源码里有对Snowflake的支持:
在这里插入图片描述
有待进一步研究。

其他

https://github.com/zhuzhong/idleaf

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

johnny233

晚饭能不能加鸡腿就靠你了

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

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

打赏作者

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

抵扣说明:

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

余额充值