2025-01-22  阅读(296)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://www.skjava.com/mianshi/baodian/detail/1357140117

回答

目前生成全局唯一 ID 比较常用的方式有 5 种:

  • UUID

UUID 是一种长度为 128 位的标识符,通常以 32 个字符串的十六进制字符串表示,它的生成依赖于当前的时间戳、随机数、MAC 地址等信息。它保证了不同时间、不同机器生成的 ID 不会重复。

它的优点就是实现过程简单,本地生成,无中心化需求。但是缺点也很明显,UUID 是随机生成的,它不具备递增性,在某些场景下可能不是很适合,而且生成的 ID 是以 32 位十六进制字符串表示的,会给存储带来一定的压力。

  • 数据库自增 id

数据库自增 ID 是利用数据库管理系统的内部机制为每条新增记录自动生成唯一且递增的 ID。它由于是利用数据库自身的功能,所以实现比较简单,性能也高效,但是由于它依赖数据库,且是单表,所以它无法跨表、跨库,存在单点瓶颈,所以它无法实现全局唯一 ID

  • 基于数据库的号段模式

通过在数据库中维护一个号段表,应用系统每次获取一个号段后,能够从该号段中连续生成多个 ID,当号段用完后,则继续想数据库申请下一个号段即可。

相比数据库自增 id,它减少了数据库的访问频率,提高了生成 ID 的效率,同时也保证了全局唯一性,所以它适用于高并发和分布式系统。但是,它可能会存在 ID 空间的浪费,同时,需要应用自己实现号段的管理和 ID 的生成,也需要高并发场景下 ID 生成的效率和唯一性,实现较为复杂。

  • Redis 生成分布式 ID

采用 Redis 原则命令 INCRINCRBY 命令来实现,由于这两个命令是原子操作,它保证了并发情况下也能够安全地递增,通过这一特性,我们可以利用 Redis 实现高效的全局唯一 ID 生成器。

高性能、高并发、简单易用,支持分布式环境,但是它依赖 Redis 环境。

  • 雪花算法

雪花算法(Snowflake Algorithm)是分布式环境下非常经典的一种分布式唯一 ID 生成算法,由 Twitter 的工程师设计出来的。它的核心目标是高效生成全局唯一的、趋势递增的 ID,而且生成速度快、并发性能好,完全可以满足大规模分布式系统的需求。

雪花算法生成的 ID 是一个 64 位的长整型数字,里面的每一部分都编码了特定的信息,比如时间戳、机器 ID 和序列号,这让它既能保证全局唯一性,又不会出现重复。而且,ID 是按照时间递增的,能在数据写入时优化索引性能。

详解

UUID

UUID 全称 Universally Unique Identifier,它是一种用于标识信息的标准。它是 128 位长的标识符,通常以 32 个字符串的十六进制字符串表示。

UUID 的设计目标是保证在全球范围内生成的每一个 UUID 都是唯一的。为了实现这一点,UUID 的生成不仅依赖于当前的时间戳,还可能依赖于随机数、节点(如机器的 MAC 地址)等信息。不同版本的 UUID 生成方式略有不同,但都保证了全局唯一性。

目前 UUID 主要有如下几个版本:

  • V1- 基于时间的 UUID:使用当前时间戳和其他信息(如机器的 MAC 地址、进程 ID 等)来生成 UUID。此种方式生成的 UUID 是基于时间的,可以保证全球唯一性。
  • V2 - 分布式安全的 UUID:将 V1 的时间戳前四位替换为
  • V3- 基于随机数的 UUID(Md5 版):生成时通过随机数来确保唯一性。在生成时,会用 16byte 的伪随机数来填充,所以,虽然时间上不一定唯一,但由于随机数的加入,在绝大多数情况下是可以保证唯一的。当然,重复率在很大程度上依赖随机数生成的质量。
  • V4- 基于名称的 UUID:基于指定的名字空间/名字生成 MD5 散列值得到,标准不推荐。
  • V5 - 基于名字空间的 UUID(SHA1版):将版本 3 的散列算法改为 SHA1。

Java 的UUID.randomUUID()是基于 v4 版本,UUID.nameUUIDFromBytes()是基于 v3 版本。

实现

Java 的 jdk 中有现成的 UUID 生成方式:

public static void main(String[] args) {
    // 基于 V3 生成一个 UUID
    UUID uuid = UUID.randomUUID();
    System.out.println(uuid.toString().replaceAll("-",""));

    // 基于 V4 生成一个 UUID
    byte[] nbyte = {10, 20, 30};
    UUID uuidFromBytes = UUID.nameUUIDFromBytes(nbyte);
    System.out.println(uuidFromBytes.toString().replaceAll("-",""));
}

优点

  • 实现简单:生成 UUID 的过程非常简单,本地生成,不需要依赖其他系统或服务。
  • 无中心化需求:由于 UUID 的生成是基于算法的,不依赖于中心化的服务或数据库

缺点

  • 不利于存储:UUID 长度为 128 位,通常以 32 个字符的十六进制字符串表示,这个对存储和索引来说是一个负担,所以对于有存储需求的场景来说,它并不是那么适合。
  • 不连续:UUID 是随机生成的,因此生成的 ID 是不具备递增性,在这某些场景(例如 MySQL 主键)

适用场景

  • 无需排序的唯一 ID

当生成的 ID 不需要有序时,UUID 则是一个不错的选择,例如文件标识、日志标识等场景。

数据库自增 ID

数据库自增 ID是利用数据库管理系统中的自增字段生成唯一标识符的方法。每当一条新记录插入数据库时,数据库会自动为该记录分配一个唯一且递增的 ID,这个 ID 会在同一表中保持唯一。自增字段通常在定义数据表时就会设置,并且数据库管理系统(DBMS)会自动管理该字段的生成。

不同的数据库,自增字段的实现方式不同,常见的数据库有:

  • MySQL:在 MySQL 中,使用AUTO_INCREMENT属性为某一字段设置自动增长。
  • PostgreSQL:PostgreSQL 使用SERIAL类型来创建自增字段,或者使用BIGSERIAL来生成更大的自增值。
  • Oracle:Oracle 不直接支持自增字段,而是使用序列(SEQUENCE)来生成自增值。

优点

  • 简单易用:由于直接依赖数据库,所以一个配置就可以解决,实现非常简单。
  • 性能高效:由于自增 ID 通常是整数类型,它的比较和索引效率很高,非常适用于数据库内部索引。

缺点

  • 依赖于数据库:自增 ID 是数据库内部的生成方式,它对数据库的状态产生强依赖。
  • 无法跨数据库生成唯一 ID:在分布式系统中,多个数据库之间的自增 ID 会出现冲突。如果不同节点使用相同的自增序列,可能会导致重复 ID。当然,这种也是有解决方案的,采用不同的起点 + 步长来规避。

假如我们有三台机器,那么 DB1 起始值为 1,DB2 起始值为 2,DB3 起始值为 3,他们自增的步长都为 3。所以:DB1 的 id 为 1、4、7...,DB2 的 id 为 2、5、8...,DB3 的 id 为 3、6、9...

适用场景

这种方式比较适用于单机系统或者小型的分布式系统。

基于数据库的号段模式

号段模式旨在解决数据库自增 ID 在分布式环境下可能出现的性能瓶颈和冲突问题。其基本思想是将 ID 生成的过程分为多个“段”,每个段包含多个连续的 ID 值。每次需要生成新的 ID 时,应用程序会向数据库申请一个新的号段,并从该号段中获取一个 ID,避免了每次都需要访问数据库来获取唯一的 ID。

核心原理包括:

  • 段的定义:首先,在数据库中定义一个用于存储号段信息的表,表中存储每个段的起始值、结束值和当前值。
  • 获取号段:当需要生成新的 ID 时,应用程序会从数据库获取一个新的号段(例如,号段为 1000),然后生成该段中的连续 ID。生成的 ID 会根据号段递增。
  • 段的更新:一旦一个段的 ID 被用完,系统就会向数据库请求一个新的号段,并开始使用新段的 ID。

每个段通常包含较大的 ID 范围,如 1000、10000 或 100000 等,可以根据需要进行调整。这种方法有利于减少数据库的访问频率,提高性能,同时避免了并发冲突。

实现

首先我们需要先设计一张表,该表用于存储号段,如下:

CREATE TABLE id_segment (
    segment_name VARCHAR(50) PRIMARY KEY,    -- 段名称,用于区分不同类型的 ID
    start_value BIGINT,                      -- 段号起始值
    end_value BIGINT,                        -- 段号结束值
    current_value BIGINT,                    -- 当前使用的值
    step_size INT DEFAULT 1000               -- 每次段号的增量,默认每次申请 1000 个 ID
);

在初始时,我们向这张表中插入一条记录来定义一个 ID 段:

INSERT INTO id_segment (segment_name, start_value, end_value, current_value, step_size)
VALUES ('user_id', 1, 1000, 0, 1000);

此时,user_id段的起始值是 1,结束值是 1000,每次申请一个新的段时会分配 1000 个 ID。

然后就是利用应用程序来实现获取了。这里有两个点需要好注意,由于是分布式场景,我们需要避免同一个应用的不同服务器可能会获取到同一个号段,所以我们需要考虑并发情况,实现方式多种,比如分布式锁,或者数据库的乐观锁。

同时,有些场景需要每天都要从 1 开始,我们只需要在这个表中增加一个 current_date 字段就可以了。

优点

  • 减少数据库压力:号段模式避免了每次生成 ID 都需要访问数据库,只需向数据库申请一次段号即可。这样大大减少了数据库的访问次数,减轻了数据库的负担。
  • 高效生成 ID:一旦获得一个段号,系统可以在本地快速生成连续的 ID,无需再访问数据库,性能优越。
  • 支持分布式:号段模式适用于分布式环境,可以通过不同的服务获取不同的段号,并生成唯一的 ID。

缺点

  • 空间浪费:每次申请一个段号时,系统会预分配一段连续的 ID,这可能导致 ID 在某些情况下存在空间浪费。例如,如果段大小为 1000,而实际上只用了 100 个 ID,那么剩余的 900 个 ID 就会被浪费掉。
  • 实现有点复杂:虽然号段模式减少了数据库的访问次数,但是我们需要在应用程序合理地号段的分配和 ID 的生成,确保生成的 ID 全局唯一。
  • 号段用尽:如果系统长时间运行,可能会存在号段用尽的情况。所以,我们可以采取每天初始化一次,然后生成的 ID 利用 yyyyMMdd 来拼接。

使用场景

号段模式非常适用于分布式系统,同时性能也比较好,适用于高并发场景。

利用 Redis 实现分布式 ID

Redis 实现分布式 ID ,主要是通过 Redis 提供的原子性命令 INCRINCRBY 来实现。由于这两个命令是原子操作的,所以它保证了在并发情况下也能安全地实现递增,并返回递增后的结果。通过这 Redis 的这个特性,我们可以利用它实现高性能的全局唯一 ID 生成器。

优点多多。高性能、全局唯一、简单易用、分布式支持。唯一的缺点可能就是需要依赖 Redis 环境。

雪花算法

雪花算法就做介绍了,参考这两篇:


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文