banner
半米牙

半米牙的笔记

分享技术、记录生活
email

各种主键生成方法介绍以及生成优缺点对比

一个系统各个表中必须要存在一列来存放唯一主键 ID,并且如果这个系统是分布式的,有多个分布数据库还需要保证每个数据库中的 id 不能重复,这就要求需要唯一 ID 的特性:

  1. 整个系统 ID 唯一
  2. ID 是数字类型,而且是趋势递增的
  3. ID 简短,查询效率快

生成 ID 的方式有多种,大厂肯定用的没这么简单,但是咱们小系统用一下还是绰绰有余的,下面逐一介绍。

UUID#

这个是最大众的方案,直接用工具类方法生成一个 uuid。

优点:

  1. 代码实现简单。
  2. 本机生成,没有性能问题
  3. 因为是全球唯一的 ID,所以迁移数据容易

缺点:

  1. 每次生成的 ID 是无序的,无法保证趋势递增
  2. UUID 的字符串存储,查询效率慢
  3. 存储空间大
  4. ID 本事无业务含义,不可读

应用场景:

  1. 类似生成 token 令牌的场景
  2. 不适用一些要求有趋势递增的 ID 场景

MySQL 主键自增#

这个方法也是很普遍用到的,设置简单,利用了 mysql 的主键自增 auto_increment,默认每次 ID 加 1。

优点:

  1. 数字化,id 递增
  2. 查询效率高
  3. 具有一定的业务可读

缺点:

  1. 存在单点问题,如果 mysql 挂了,就没法生成 iD 了
  2. 数据库压力大,高并发抗不住

MySQL 多实例主键自增#

uuid1

每台的初始值分别为 1,2,3...N,步长为 N(这个案例步长为 4)

优点:解决了单点问题

缺点:一旦把步长定好后,就无法扩容;而且单个数据库的压力大,数据库自身性能无法满足高并发

应用场景:数据不需要扩容的场景

雪花 snowflake 算法#

雪花算法生成 64 位的二进制正整数,然后转换成 10 进制的数。64 位二进制数由如下部分组成:

uuid2

  • 1 位标识符:始终是 0
  • 41 位时间戳:41 位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截 ) 得到的值,这里的的开始时间截,一般是我们的 id 生成器开始使用的时间,由我们程序来指定的
  • 10 位机器标识码:可以部署在 1024 个节点,如果机器分机房(IDC)部署,这 10 位可以由 5 位机房 ID + 5 位机器 ID 组成
  • 12 位序列:毫秒内的计数,12 位的计数顺序号支持每个节点每毫秒 (同一机器,同一时间截) 产生 4096 个 ID 序号

实现方式 Java 版:

/**
 * Twitter_Snowflake<br>
 * SnowFlake的结构如下(每部分用-分开):<br>
 * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
 * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br>
 * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
 * 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
 * 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br>
 * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br>
 * 加起来刚好64位,为一个Long型。<br>
 * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
 */
public class SnowflakeIdWorker {

	// ==============================Fields===========================================
	/** 开始时间截 (2015-01-01) */
	private final long twepoch = 1420041600000L;

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

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

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

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

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

	/** 机器ID向左移12位 */
	private final long workerIdShift = sequenceBits;

	/** 数据标识id向左移17位(12+5) */
	private final long datacenterIdShift = sequenceBits + workerIdBits;

	/** 时间截向左移22位(5+5+12) */
	private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

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

	/** 工作机器ID(0~31) */
	private long workerId;

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

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

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

	//==============================Constructors=====================================
	/**
	 * 构造函数
	 * @param workerId 工作ID (0~31)
	 * @param datacenterId 数据中心ID (0~31)
	 */
	public SnowflakeIdWorker(long workerId, long datacenterId) {
		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;
	}

	// ==============================Methods==========================================
	/**
	 * 获得下一个ID (该方法是线程安全的)
	 * @return SnowflakeId
	 */
	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) {
			sequence = (sequence + 1) & sequenceMask;
			//毫秒内序列溢出
			if (sequence == 0) {
				//阻塞到下一个毫秒,获得新的时间戳
				timestamp = tilNextMillis(lastTimestamp);
			}
		}
		//时间戳改变,毫秒内序列重置
		else {
			sequence = 0L;
		}

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

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

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

	/**
	 * 返回以毫秒为单位的当前时间
	 * @return 当前时间(毫秒)
	 */
	protected long timeGen() {
		return System.currentTimeMillis();
	}

	//==============================Test=============================================
	/** 测试 */
	public static void main(String[] args) {
		SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
		for (int i = 0; i < 1000; i++) {
			long id = idWorker.nextId();
			System.out.println(Long.toBinaryString(id));
			System.out.println(id);
		}
	}
}

优点:

  1. 此方案每秒能够产生 409.6 万个 ID,性能快
  2. 时间戳在高位,自增序列在低位,整个 ID 是趋势递增的,按照时间有序递增
  3. 灵活度高,可以根据业务需求,调整 bit 位的划分,满足不同的需求

缺点:

  1. 依赖机器的时钟,如果服务器时钟回拨,会导致重复 ID 生成

在分布式场景中,服务器时钟回拨会经常遇到,一般存在 10ms 之间的回拨;小伙伴们就说这点 10ms,很短可以不考虑吧。但此算法就是建立在毫秒级别的生成方案,一旦回拨,就很有可能存在重复 ID。

Redis 生成方案#

利用 redis 的 incr 原子性操作自增,一般算法为:年份 + 当天距当年第多少天 + 天数 + 小时 + redis 自增

优点:有序递增,可读性强

缺点:占用带宽,每次要向 redis 进行请求

参考#

  1. 老顾聊技术 - 你想了解一线大厂的分布式唯一 ID 生成方案吗?
  2. 永夜微光 - Twitter 的分布式自增 ID 算法 snowflake (Java 版)
加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。