聊聊常见的分布式ID解决方案

07-17 395阅读


highlight: xcode

theme: vuepress

为什么要使用分布式ID?

随着 Web 开发技术的不断发展,单体的系统逐步走向分布式系统。在分布式系统中,使用分布式 ID(Distributed IDs)主要是为了在没有单点故障的情况下生成唯一标识符。这些唯一标识符在很多场景中非常重要,例如数据库记录的主键、消息队列中的消息ID、日志系统中的唯一事件ID等。使用分布式 ID 有以下几个主要原因:

  1. 唯一性:分布式系统中,各个节点可能同时生成 ID,如果不加以控制,可能会出现 ID 冲突。分布式ID 生成方案确保在整个系统中生成的 ID 都是唯一的。
  2. 可扩展性:分布式 ID 生成方案能够在分布式系统的不同节点上生成 ID,不会成为系统扩展的瓶颈。相比于集中式的 ID 生成方案(如数据库自增 ID),分布式 ID 方案能够更好地适应系统的扩展需求。
  3. 高可用性:分布式 ID 生成方案通常设计为无单点故障的结构,即使某些节点故障,其他节点仍然可以正常生成 ID,保证系统的高可用性。
  4. 性能:分布式 ID 生成方案通常具有低延迟、高并发的特点,能够满足分布式系统中高频次的ID生成需求。相比于依赖集中式数据库生成 ID,分布式 ID 生成在性能上有显著优势。
  5. 时间排序:有些分布式 ID 生成方案(如 Twitter 的 Snowflake)生成的 ID 具有时间排序的特性,即 ID 的大致顺序反映了生成的时间。这对于某些需要按时间顺序处理的数据非常有用。

常见的分布式ID解决方案

下面介绍一下常见的分布式 ID 解决方案,有一些只是作为了解,在工作中基本上用不到。

UUID

UUID(Universally Unique Identifier),通用唯一识别码。UUID 是基于当前时间、计数器(counter)和硬件标识(通常为无线网卡的 MAC 地址)等数据计算生成的。

UUID 由以下几部分的组合:

  • 当前日期和时间,UUID 的第一个部分与时间有关,如果你在生成一个 UUID 之后,过几秒又生成一个 UUID,则第一个部分不同,其余相同。
  • 时钟序列。
  • 全局唯一的 IEEE 机器识别号,如果有网卡,从网卡 MAC 地址获得,没有网卡以其他方式获得。

    UUID 是由一组 32 位数的 16 进制数字所构成,以连字号分隔的五组来显示,形式为 8-4-4-4-12,总共有 36 个字符(即 32 个英数字母和 4 个连字号)。例如:

    text aefbbd3a-9cc5-4655-8363-a2a43e6e6c80 xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx

    在 Java 中,可以使用 UUID 类来生成 UUID:

    java @Test public void testUuid() { UUID uuid = UUID.randomUUID(); System.out.println(uuid); } // 4996554d-e196-446a-80e4-330037825695

    如果需求是只保证唯一性,那么 UUID 也是可以使用的,但是按照上面的分布式 ID 的要求, UUID 其实是不能做成分布式 ID 的,原因如下:

    1. 首先分布式 ID 一般都会作为主键,但是安装 MySQL 官方推荐主键要尽量越短越好,UUID 每一个都很长,所以不是很推荐。
    2. 既然分布式 ID 是主键,然后主键是包含索引的,然后 MySQL 的索引是通过 B+ 树来实现的,每一次新的UUID 数据的插入,为了查询的优化,都会对索引底层的 B+ 树进行修改,因为 UUID 数据是无序的,所以每一次 UUID 数据的插入都会对主键生成的 B+ 树进行很大的修改,这一点很不好。
    3. 信息不安全:基于 MAC 地址生成 UUID 的算法可能会造成 MAC 地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。

    自增ID

    针对表结构的主键,我们常规的操作是在创建表结构的时候给对应的 ID 设置 auto_increment,也就是勾选自增选项。

    但是这种方式我们清楚在单个数据库的场景中我们是可以这样做的,但如果是在分库分表的环境下,直接利用单个数据库的自增肯定会出现问题。因为 ID 要唯一,但是分表分库后只能保证一个表中的 ID 的唯一,而不能保证整体的 ID 唯一。

    聊聊常见的分布式ID解决方案

    上面的情况我们可以通过单独创建主键维护表来处理。

    聊聊常见的分布式ID解决方案

    简单举一个例子。创建一个表:

    sql CREATE TABLE `t_order_id` ( `id` bigint NOT NULL AUTO_INCREMENT, `title` char(1) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `title` (`title`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;

    然后我们通过更新 ID 操作来获取 ID 信息:

    ```sql BEGIN;

    REPLACE INTO torderid (title) values ('p') ; SELECT LASTINSERTID();

    COMMIT; ```

    但是这种方式还是很麻烦,不推荐。

    数据库多主模式

    单点数据库方式存在明显的性能问题,可以对数据库进行高可以优化,担心一个主节点挂掉没法使用,可以选择做双主模式集群,也就是两个 MySQL 实例都能单独生产自增的 ID。

    查看主键自增的属性:

    sql show variables like '%increment%'

    聊聊常见的分布式ID解决方案

    我们可以设置主键自增的步长从 2 开始。

    聊聊常见的分布式ID解决方案

    但是这种在并发量比较高的情况下,如何保证扩展性其实会是一个问题。在高并发情况下性能堪忧。

    号段模式

    号段模式是当下分布式 ID 生成器的主流实现方式之一,号段模式可以理解为从数据库批量的获取自增 ID,每次从数据库取出一个号段范围,例如 (1, 1000] 代表 1000 个ID,具体的业务服务将本号段,生成 1~1000 的自增 ID 并加载到内存。表结构如下:

    sql CREATE TABLE id_generator ( id int(10) NOT NULL, max_id bigint(20) NOT NULL COMMENT '当前最大id', step int(20) NOT NULL COMMENT '号段的布长', biz_type int(20) NOT NULL COMMENT '业务类型', version int(20) NOT NULL COMMENT '版本号', PRIMARY KEY (`id`) )

    • max_id:当前最大的可用 id。
    • step:代表号段的长度。
    • biz_type:代表不同业务类型。
    • version:是一个乐观锁,每次都更新 version,保证并发时数据的正确性。

      等这批号段 ID 用完,再次向数据库申请新号段,对 max_id 字段做一次 update 操作: update max_id = max_id + step,update 成功则说明新号段获取成功,新的号段范围是(max_id, max_id + step]。

      聊聊常见的分布式ID解决方案

      聊聊常见的分布式ID解决方案

      由于多业务端可能同时操作,所以采用版本号 version 乐观锁方式更新,这种分布式 ID 生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多。但同样也会存在一些缺点比如:服务器重启,单点故障会造成 ID 不连续。

      使用Redis生成

      基于全局唯一 ID 的特性,我们可以通过 Redis 的 INCR 命令来生成全局唯一 ID。

      同样使用 Redis 也有对应的缺点:

      • ID 生成的持久化问题,如果 Redis 宕机了怎么进行恢复。
      • 单个节点宕机问题。

        当然针对故障问题我们可以通过 Redis 集群来处理,比如我们有三个 Redis 的 Master 节点。可以初始化每台 Redis 的值分别是 1,2,3。然后分别把分布式 ID 的 KEY 用 Hash Tags 固定每一个 Master 节点,步长就是 Master 节点的个数。各个 Redis 生成的 ID 为:

        text A: 1, 4, 7 B: 2, 5, 8 C: 3, 6, 9

        优点:

        • 不依赖于数据库,灵活方便,且性能优于数据库
        • 数字 ID 有序,对分页处理和排序都很友好
        • 防止了 Redis 的单机故障。

          缺点:

          • 如果没有 Redis 中间件,需要安装配置,增加复杂度。
          • 集群节点确定是 3 个后,后面调整不是很方便。

            Redis 分布式 ID 生成器实例代码,结合 Spring Boot 实现:

            ```java /** * Redis 分布式ID生成器 */ @Component public class RedisDistributedId {

            @Autowired
            private StringRedisTemplate redisTemplate;
            private static final long BEGIN_TIMESTAMP = 1659312000l;
            /**
             * 生成分布式ID
             * 符号位    时间戳[31位]  自增序号【32位】
             * @param item
             * @return
             */
            public long nextId(String item) {
                // 1. 生成时间戳
                LocalDateTime now = LocalDateTime.now();
                // 格林威治时间差
                long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
                // 我们需要获取的 时间戳 信息
                long timestamp = nowSecond - BEGIN_TIMESTAMP;
                // 2. 生成序号 从Redis中获取
                // 当前当日期
                String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
                // 获取对应的自增的序号
                Long increment = redisTemplate.opsForValue().increment("id:" + item + ":" + date);
                return timestamp 
VPS购买请点击我

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

目录[+]