探讨java系统中全局唯一ID实现方案

为什么需要全局唯一ID

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

UUID

UUID (Universally Unique Identifier),通用唯一识别码的缩写。UUID是由一组32位数的16进制数字所构成,所以UUID理论上的总数为 1632=2128,约等于 3.4 x 10^38。也就是说若每纳秒产生1兆个UUID,要花100亿年才会将所有UUID用完。生成的UUID是由 8-4-4-4-12格式的数据组成,其中32个字符和4个连字符’ - ',一般我们使用的时候会将连字符删除 uuid.toString().replaceAll(“-”,“”)。
UUID的产生方式主要包括以下几种:

  1. 基于时间戳和MAC地址(UUID v1):这种方式结合了主机的网络接口卡MAC地址和当前的日期时间来生成UUID。由于包含了MAC地址,因此这种方式生成的UUID在一定程度上是可以被跟踪的,因为它隐含了生成UUID的设备信息。
  2. 基于随机数(UUID v4):这是目前最常用的UUID生成方式。它完全基于随机数或伪随机数,没有任何可以识别的个人或组织信息,因此是完全匿名的。这也是为什么它被广泛应用于需要高度安全性和隐私保护的场景。
  3. 基于命名空间的UUID(UUID v3、v5):这种方式基于MD5和SHA-1散列算法,通过将特定命名空间的标识符和名字进行散列来生成UUID。这种方法允许UUID基于给定的名字和命名空间来生成,确保不同命名空间下的UUID是唯一的。
  4. 基于硬件的UUID:某些系统可能还会根据其他硬件信息来生成UUID,例如硬盘序列号等。
    java实现的uuid为基于随机数,使用代码:
public static void main(String[] args) {

    //获取一个版本4根据随机字节数组的UUID。
    UUID uuid = UUID.randomUUID();
    System.out.println(uuid.toString().replaceAll("-",""));
}

虽然 UUID 生成方便,本地生成没有网络消耗,但是使用起来也有一些缺点:

  1. 不易于存储:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。
  2. 信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,暴露使用者的位置。
  3. 对MySQL索引不利:如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能,可以查阅 Mysql 索引原理 B+树的知识。

使用redis实现

Redis实现分布式唯一ID主要是通过提供像 INCR 和 INCRBY 这样的自增原子命令,由于Redis自身的单线程的特点所以能保证生成的 ID 肯定是唯一有序的。以下代码通过引入jedis来实现:

import redis.clients.jedis.Jedis;

public class UniqueIdGenerator {
    private static final String REDIS_HOST = "localhost";
    private static final int REDIS_PORT = 6379;
    private static final String UNIQUE_ID_KEY = "unique_id";

    public static void main(String[] args) {
        Jedis jedis = new Jedis(REDIS_HOST, REDIS_PORT);
        long uniqueId = generateUniqueId(jedis);
        System.out.println("生成的唯一ID: " + uniqueId);
        jedis.close();
    }

    private static long generateUniqueId(Jedis jedis) {
        return jedis.incr(UNIQUE_ID_KEY);
    }
}

雪花算法-Snowflake

Snowflake,雪花算法是由Twitter开源的分布式ID生成算法,以划分命名空间的方式将 64-bit位分割成多个部分,每个部分代表不同的含义。而 Java中64bit的整数是Long类型,所以在 Java 中 SnowFlake 算法生成的 ID 就是 long 来存储的。
具体实现步骤如下:

  • 第一位为未使用(符号位表示正数),接下来的41位为毫秒级时间(41位的长度可以使用69年)
  • 然后是5位datacenterId和5位workerId(10位的长度最多支持部署1024个节点)
  • 最后12位是毫秒内的计数(12位的计数顺序号支持每个节点每毫秒产生4096个ID序号)
    探讨java系统中全局唯一ID实现方案_第1张图片

以下是java实现版本:

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"})
	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"})
	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
	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;
	/**
	 * 当在低频模式下时,序号始终为0,导致生成ID始终为偶数
* 此属性用于限定一个随机上限,在不同毫秒下生成序号时,给定一个随机数,避免偶数问题。
* 注意次数必须小于{@link #SEQUENCE_MASK},{@code 0}表示不使用随机数。
* 这个上限不包括值本身。 */
private final long randomSequenceLimit; /** * 自增序号,当高频模式下时,同一毫秒内生成N个ID,则这个序号在同一毫秒下,自增以避免ID重复。 */ private long sequence = 0L; private long lastTimestamp = -1L; /** * 构造,使用自动生成的工作节点ID和数据中心ID */ public Snowflake() { this(IdUtil.getWorkerId(IdUtil.getDataCenterId(MAX_DATA_CENTER_ID), MAX_WORKER_ID)); } /** * 构造 * * @param workerId 终端ID */ public Snowflake(long workerId) { this(workerId, IdUtil.getDataCenterId(MAX_DATA_CENTER_ID)); } /** * 构造 * * @param workerId 终端ID * @param dataCenterId 数据中心ID */ public Snowflake(long workerId, long dataCenterId) { this(workerId, dataCenterId, false); } /** * 构造 * * @param workerId 终端ID * @param dataCenterId 数据中心ID * @param isUseSystemClock 是否使用{@link SystemClock} 获取当前时间戳 */ public Snowflake(long workerId, long dataCenterId, boolean isUseSystemClock) { this(null, workerId, dataCenterId, isUseSystemClock); } /** * @param epochDate 初始化时间起点(null表示默认起始日期),后期修改会导致id重复,如果要修改连workerId dataCenterId,慎用 * @param workerId 工作机器节点id * @param dataCenterId 数据中心id * @param isUseSystemClock 是否使用{@link SystemClock} 获取当前时间戳 * @since 5.1.3 */ public Snowflake(Date epochDate, long workerId, long dataCenterId, boolean isUseSystemClock) { this(epochDate, workerId, dataCenterId, isUseSystemClock, DEFAULT_TIME_OFFSET); } /** * @param epochDate 初始化时间起点(null表示默认起始日期),后期修改会导致id重复,如果要修改连workerId dataCenterId,慎用 * @param workerId 工作机器节点id * @param dataCenterId 数据中心id * @param isUseSystemClock 是否使用{@link SystemClock} 获取当前时间戳 * @param timeOffset 允许时间回拨的毫秒数 * @since 5.8.0 */ public Snowflake(Date epochDate, long workerId, long dataCenterId, boolean isUseSystemClock, long timeOffset) { this(epochDate, workerId, dataCenterId, isUseSystemClock, timeOffset, 0); } /** * @param epochDate 初始化时间起点(null表示默认起始日期),后期修改会导致id重复,如果要修改连workerId dataCenterId,慎用 * @param workerId 工作机器节点id * @param dataCenterId 数据中心id * @param isUseSystemClock 是否使用{@link SystemClock} 获取当前时间戳 * @param timeOffset 允许时间回拨的毫秒数 * @param randomSequenceLimit 限定一个随机上限,在不同毫秒下生成序号时,给定一个随机数,避免偶数问题,0表示无随机,上限不包括值本身。 * @since 5.8.0 */ public Snowflake(Date epochDate, long workerId, long dataCenterId, boolean isUseSystemClock, long timeOffset, long randomSequenceLimit) { this.twepoch = (null != epochDate) ? epochDate.getTime() : DEFAULT_TWEPOCH; this.workerId = Assert.checkBetween(workerId, 0, MAX_WORKER_ID); this.dataCenterId = Assert.checkBetween(dataCenterId, 0, MAX_DATA_CENTER_ID); this.useSystemClock = isUseSystemClock; this.timeOffset = timeOffset; this.randomSequenceLimit = Assert.checkBetween(randomSequenceLimit, 0, SEQUENCE_MASK); } /** * 根据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) { final long sequence = (this.sequence + 1) & SEQUENCE_MASK; if (sequence == 0) { timestamp = tilNextMillis(lastTimestamp); } this.sequence = sequence; } else { // issue#I51EJY if (randomSequenceLimit > 1) { sequence = RandomUtil.randomLong(randomSequenceLimit); } else { sequence = 0L; } } lastTimestamp = timestamp; return ((timestamp - twepoch) << TIMESTAMP_LEFT_SHIFT) | (dataCenterId << DATA_CENTER_ID_SHIFT) | (workerId << WORKER_ID_SHIFT) | sequence; } /** * 下一个ID(字符串形式) * * @return ID 字符串形式 */ public String nextIdStr() { return Long.toString(nextId()); } // ------------------------------------------------------------------------------------------------------------------------------------ Private method start /** * 循环等待下一个时间 * * @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(); } }

百度-UidGenerator

百度的 UidGenerator 是百度开源基于Java语言实现的唯一ID生成器,是在雪花算法 snowflake 的基础上做了一些改进。UidGenerator以组件形式工作在应用项目中, 支持自定义workerId位数和初始化策略,适用于docker等虚拟化环境下实例自动重启、漂移等场景。
在实现上,UidGenerator 提供了两种生成唯一ID方式,分别是 DefaultUidGenerator 和 CachedUidGenerator,官方建议如果有性能考虑的话使用 CachedUidGenerator 方式实现。UidGenerator 依然是以划分命名空间的方式将 64-bit位分割成多个部分,只不过它的默认划分方式有别于雪花算法 snowflake。它默认是由 1-28-22-13 的格式进行划分。可根据你的业务的情况和特点,自己调整各个字段占用的位数。

  • 第1位仍然占用1bit,其值始终是0。
  • 第2位开始的28位是时间戳,28-bit位可表示2^28个数,这里不再是以毫秒而是以秒为单位,每个数代表秒则可用(1L<<28)/ (360024365) ≈ 8.51 年的时间。
  • 中间的 workId (数据中心+工作机器,可以其他组成方式)则由 22-bit位组成,可表示 2^22 = 4194304个工作ID。
  • 最后由13-bit位构成自增序列,可表示2^13 = 8192个数。

Medis 薄雾算法工程实践

https://github.com/asyncins/medis

薄雾算法 Mist 由书籍《Python3 反爬虫原理与绕过实战》的作者韦世东综合各大厂的技术点且考虑高性能分布式序列号生成器架构后设计的一款“递增态势、不依赖数据库、高性能且不受时间回拨影响”的全局唯一序列号生成算法。
Medis 是薄雾算法 Mist 的工程实践,其名取自 Mist 和 Redis。薄雾算法是一款性能强到令我惊喜的全局唯一 ID 算法。有了 Mist 和 Medis,你就拥有了和美团 Leaf、微信 Seqsvr、百度 UIDGenerator 性能相当(甚至超过)的全局唯一 ID 服务了。相比复杂的 UIDGenerator 双 Buffer 优化和 Leaf-Snowflake,薄雾算法 Mist 简单太多了。

你可能感兴趣的:(java)