9种分布式ID生成方式

本文介绍9种分布式ID生成方式,包括UUID、数据库自增ID、数据库集群模式、号段模式、Redis模式、Snowflake算法、uid-generator、Leaf及Tinyid,详细解析各种方法的优缺点及其应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

自增id为什么不是连续的

  1. 唯一键冲突
  2. 事务回滚
  3. 批量插入,分配自增id不连续
对于批量插入数据的语句,MySQL 有一个批量申请自增 id 的策略:
* 语句执行过程中,第一次申请自增 id,会分配 1 个;
* 1 个用完以后,这个语句第二次申请自增 id,会分配 2 个;
* 2 个用完以后,还是这个语句,第三次申请自增 id,会分配 4 个;
* 依此类推,同一个语句去申请自增 id,每次申请到的自增 id 个数都是上一次的两倍。
如果一次性批量插入4组数据,此时会申请7个自增id,多余的3个就浪费了,所以会出现不连续的情况。

int会在多大数据时溢出整数型和字符型不同,长度是固定的,也就是说int(1)和int(11)储存的最大值是一样的。数据的最大值只和类型有关(tinyint, int, bigint)和括号内的数字无关。各类型的最大长度如下

 自增id溢出解决办法:清理数据表的跳跃自增ID,保持ID连贯。

1.   创建评论临时表

create table `hotel_info_comments_tmp` like hotel_info_comments

2.   将线上表的数据插入临时表(自增ID列不转移

insert into hotel_info_comments_tmp(`extra_info`,`create_time`,`update_time`) select `extra_info`,`create_time`,`update_time` from hotel_info_comments;

3.   检验线上表的的数据和临时表是否一致

select count(*) from hotel_info_comments;
select count(*) from hotel_info_comments_tmp;

4.   删除线上表

drop table hotel_info_comments;

5.   重命名临时表为线上表

rename table hotel_info_comments_tmp to hotel_info_comments;

建以下数据表: int 主键自增表, guid主键表

CREATE TABLE `tbl_test_int` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) DEFAULT NULL,
  `comment` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='测试int主键性能';
Insert into tbl_test_int(name,comment) values('TestMY', '哈哈,呵呵');
CREATE TABLE `tbl_test_measure` (
  `guid` char(36) NOT NULL,
  `deviceID` int(50) DEFAULT NULL,
  `value` int(50) DEFAULT NULL,
  `value2` int(50) DEFAULT NULL,
  PRIMARY KEY (`guid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='测试guid作唯一';
Insert into tbl_test_measure(guid,deviceID,value,value2) values (upper(replace(uuid(),'-','')), 1,2,3)

MySQL实现了 UUID,并且提供 UUID() 函数方便用户生成 UUID。在 MySQL 的 UUID() 函数中,前三组数字从时间戳中生成,第四组数字暂时保持时间戳的唯一性,第五组数字是一个 IEEE 802 节点标点值,保证空间唯一。使用 UUID() 函数,可以生成时间、空间上都独一无二的值。据说只要是使用了 UUID,都不可能看到两个重复的 UUID 值。当然,这个只是在理论情况下。如果是主从即M-S模式,最好是不使用mysql自带函数uuid来生成唯一主键。当字符集设置为utf8mb4时,解决mysql replace uuid() 导致单表重复的问题

select REPLACE(md5(UUID()),"-","")

如果真要使用uuid,可以在程序生成后,直接望DB里存储,这时主从的uuid就是一样的了

使用INT做主键的优点:
1、需要很小的数据存储空间,仅仅需要4 byte 。
2、查询操作时使用INT的性能比GUID好,所以使用int将会提高应用程序的性能。
3、index和Join 操作,int的性能最好。
4、容易记忆。
5、支持通过函数获取最新的值LAST_INSERT_ID()
使用INT做主键的缺点
1、如果经常有合并表的操作,就可能会出现主键重复的情况。
2、使用INT数据范围有限制。如果存在大量的数据,可能会超出INT的取值范围。
3、很难处理分布式存储的数据表。

使用GUID做主键的优点:
1、它是独一无二的。
2、出现重复的机会少。
3、适合大量数据中的插入和更新操作
4、在分表时好处明显,跨服务器数据合并非常方便,数据可移植性强
使用GUID做主键的缺点:
1、存储空间大(16 byte),因此它将会占用更多的磁盘大小。
2、很难记忆。join操作性能比int要低。
3、没有内置的函数获取最新产生的guid主键。
4、GUID做主键将会添加到表上的所以其他索引中,因此会降低性能。
总结:上面列出了GUID和INT两种数据类型做主键优缺点。我觉得,对于大数据量,建议使用guid做主键。而使用int会得到最佳的性能。

分布式ID必要性。

业务量小于500W或数据容量小于2G的时候单独一个mysql即可提供服务,再大点的时候就进行读写分离也可以应付过来。但当主从同步也扛不住的是就需要分表分库了,但分库分表后需要有一个唯一ID来标识一条数据,数据库的自增ID显然不能满足需求;特别一点的如订单、优惠券也都需要有唯一ID做标识。此时一个能够生成全局唯一ID的系统是非常必要的。那么这个全局唯一ID就叫分布式ID。

分布式ID需满足那些条件

  • 全局唯一:基本要求就是必须保证ID是全局性唯一的。
  • 高性能:高可用低延时,ID生成响应要快。
  • 高可用:无限接近于100%的可用性
  • 好接入:遵循拿来主义原则,在系统设计和实现上要尽可能的简单
  • 趋势递增:最好趋势递增,这个要求就得看具体业务场景了,一般不严格要求

1. UUID

UUID 是指Universally Unique Identifier,翻译为中文是通用唯一识别码,UUID 的目的是让分布式系统中的所有元素都能有唯一的识别信息。形式为 8-4-4-4-12,总共有 36个字符。用起来非常简单

2. 基于数据库自增ID

基于数据库的auto_increment自增ID完全可以充当分布式ID,具体实现:需要一个单独的MySQL实例用来生成ID,建表结构如下:

CREATE DATABASE `SoWhat_ID`;
CREATE TABLE SoWhat_ID.SEQUENCE_ID (
    `id` bigint(20) unsigned NOT NULL auto_increment, 
    `value` char(10) NOT NULL default '',
    `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
) ENGINE=MyISAM;
insert into SEQUENCE_ID(value) VALUES ('values');

当我们需要一个ID的时候,向表中插入一条记录返回主键ID,但这种方式有一个比较致命的缺点,访问量激增时MySQL本身就是系统的瓶颈,用它来实现分布式服务风险比较大,不推荐!

优点:实现简单,ID单调自增,数值类型查询速度快

缺点:DB单点存在宕机风险,无法扛住高并发场景

3. 基于数据库集群模式

前边说了单点数据库方式不可取,那对上边的方式做一些高可用优化,换成主从模式集群。害怕一个主节点挂掉没法用,那就做双主模式集群,也就是两个Mysql实例都能单独的生产自增ID。那这样还会有个问题,两个MySQL实例的自增ID都从1开始,会生成重复的ID怎么办?
解决方案设置起始值和自增步长

MySQL_1 配置:

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

MySQL_2 配置:

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

这样两个MySQL实例的自增ID分别就是:

1、3、5、7、9 
2、4、6、8、10

但是如果两个还是无法满足咋办呢?增加第三台MySQL实例需要人工修改一、二两台MySQL实例的起始值和步长,把第三台机器的ID起始生成位置设定在比现有最大自增ID的位置远一些,但必须在一、二两台MySQL实例ID还没有增长到第三台MySQL实例的起始ID值的时候,否则自增ID就要出现重复了,必要时可能还需要停机修改。

优点:解决DB单点问题

缺点:不利于后续扩容,而且实际上单个数据库自身压力还是大,依旧无法满足高并发场景。

4. 基于数据库的号段模式

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

CREATE TABLE id_generator (
  `id` int(10) NOT NULL,
  `max_id` bigint(20) NOT NULL COMMENT '当前最大id',
  `step` int(20) NOT NULL COMMENT '号段的步长',
  `id_lpad` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT 'ID是否左侧补0',
  `biz_type` int(20) NOT NULL COMMENT '业务类型',
  `version` int(20) NOT NULL COMMENT '版本号',
  PRIMARY KEY (`id`)
)
  • max_id :当前最大的可用id
  • step :代表号段的长度
  • biz_type :代表不同业务类型
  • version :是一个乐观锁,每次都更新version,保证并发时数据的正确性
idbiz_typemax_idstepversion
1101100020000

一直到本地自增到1000时,也就是当前号段已经用完了,才去数据库重新获取下一号段。向数据库申请新号段,对max_id字段做一次update操作,update max_id= max_id + step,update成功则说明新号段获取成功,新的号段范围是(max_id ,max_id +step]。

update id_generator set max_id = {max_id+step}, version = version + 1 where version =  {version} and biz_type = XX

由于多业务端可能同时操作,所以采用版本号 version 乐观锁方式更新,这种分布式ID生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多。但是如果遇到了双十一或者秒杀类似的活动还是会对数据库有比较高的访问。

5. 基于Redis模式

Redis 也同样可以实现,原理就是Redis 是单线程的,因此我们可以利用redis的incr命令实现ID的原子性自增

6. 基于雪花算法(Snowflake)模式

SnowFlake 算法,是 Twitter 开源的分布式 id 生成算法。其核心思想就是:使用一个 64 bit 的 long 型的数字作为全局唯一 id。在分布式系统中的应用十分广泛,且ID 引入了时间戳,为什么叫雪花算法呢?私以为众所周知世界上没有一对相同的雪花。雪花算法基本上保持自增的,它将生成的 ID 分为以下几个部分:

  • 符号位 (1位): 通常为0,表示正数
  • 时间戳(41位):用来记录从某个固定时间点(通常是某个纪元时间,比如 2020-01-01 00:00:00 UTC)开始的毫秒数。
  • 工作机器 ID(10位):用来标识不同的服务器或进程。
  • 序列号(12位):用来解决同一毫秒内产生的多个 ID 的唯一性问题。

SnowFlake算法的缺点:

依赖系统时间的一致性,如果系统时间被回调,或者改变,可能会造成id冲突或者重复。

import java.util.concurrent.atomic.AtomicLong;

/**
 * 雪花算法ID生成器 该类用于生成唯一ID,使用雪花算法(Snowflake Algorithm),该算法可以生成唯一的、有序的、可排序的ID
 */
public class SnowflakeIdWorker {

  /**
   * 起始时间戳,用于计算时间差,这里使用2010年1月1日0点0分0秒UTC时间
   */
  private static final long START_TIMESTAMP = 1288834974657L;

  // 序列号位数
  private static final int SEQUENCE_BIT = 12;
  // 机器标识位数
  private static final int MACHINE_ID_BIT = 10;
  // 时间戳位数
  private static final int TIMESTAMP_BIT = 41;

  /**
   * 序列号掩码,用于判断序列号是否溢出
   */
  private static final long SEQUENCE_MASK = -1L ^ (-1L << SEQUENCE_BIT);
  /**
   * 机器标识左移位数,用于将机器标识和时间戳组合成ID
   */
  private static final long MACHINE_ID_LEFT_SHIFT = SEQUENCE_BIT;
  /**
   * 时间戳左移位数,用于将时间戳和序列号组合成ID
   */
  private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BIT + MACHINE_ID_BIT;

  // 机器标识
  private final long machineId;
  // 序列号,使用AtomicLong保证线程安全
  private final AtomicLong sequence = new AtomicLong(0);

  /**
   * 上次生成ID的时间戳,用于保证时间顺序
   */
  private long lastTimestamp = -1L;

  /**
   * 构造函数,初始化SnowflakeIdWorker
   *
   * @param machineId 机器标识,需要保证在不同机器上唯一
   * @throws IllegalArgumentException 如果机器标识不合法
   */
  public SnowflakeIdWorker(long machineId) {
    // 检查机器标识是否合法
    if (machineId < 0 || machineId > (1L << MACHINE_ID_BIT) - 1) {
      throw new IllegalArgumentException(String.format("Invalid machineId: %d", machineId));
    }
    this.machineId = machineId;
  }

  /**
   * 生成下一个ID
   *
   * @return 下一个ID
   */
  public synchronized long nextId() {
    long timestamp = timeGen();

    // 如果当前时间小于上一次生成的时间戳,则表示时间回退了,抛出异常
    if (timestamp < lastTimestamp) {
      throw new RuntimeException(String.format(
          "Clock moved backwards.  Refusing to generate id for %d milliseconds",
          lastTimestamp - timestamp));
    }

    // 如果是同一毫秒内生成的ID,则递增序列号
    if (lastTimestamp == timestamp) {
      long sequence = this.sequence.getAndIncrement();
      // 序列号溢出,等待下一毫秒
      if (sequence > SEQUENCE_MASK) {
        timestamp = tilNextMillis(lastTimestamp);
        lastTimestamp = timestamp;
        sequence = this.sequence.getAndIncrement();
      }
      // 组合时间戳、机器标识和序列号生成ID
      return ((timestamp - START_TIMESTAMP) << TIMESTAMP_LEFT_SHIFT) |
          (machineId << MACHINE_ID_LEFT_SHIFT) |
          sequence;
    } else {
      // 不同毫秒内生成的ID,重置序列号
      this.sequence.set(0);
    }

    lastTimestamp = timestamp;
    // 组合时间戳、机器标识和序列号生成ID
    return ((timestamp - START_TIMESTAMP) << TIMESTAMP_LEFT_SHIFT) |
        (machineId << MACHINE_ID_LEFT_SHIFT) |
        this.sequence.get();
  }

  /**
   * 从Snowflake ID中提取时间戳部分,并转换为毫秒时间戳。
   *
   * @param snowflakeId Snowflake ID
   * @return 毫秒时间戳
   */
  public static long extractTimestampFromId(long snowflakeId) {
    // 提取时间戳部分
    long timestampPart = snowflakeId >> TIMESTAMP_LEFT_SHIFT;
    // 将时间戳部分转换为实际的时间戳
    return timestampPart + START_TIMESTAMP;
  }

  /**
   * 等待下一毫秒,用于处理序列号溢出的情况
   *
   * @param lastTimestamp 上一次生成ID的时间戳
   * @return 下一毫秒的时间戳
   */
  private long tilNextMillis(long lastTimestamp) {
    long timestamp = timeGen();
    // 等待直到时间戳改变
    while (timestamp <= lastTimestamp) {
      timestamp = timeGen();
    }
    return timestamp;
  }

  /**
   * 获取当前时间戳
   *
   * @return 当前时间戳
   */
  private long timeGen() {
    return System.currentTimeMillis();
  }

  /**
   * 主函数,用于测试生成ID的功能
   *
   * @param args 命令行参数
   */
  public static void main(String[] args) {
    SnowflakeIdWorker worker = new SnowflakeIdWorker(1);
    for (int i = 0; i < 10; i++) {
      System.out.println(worker.nextId());
    }

    // 示例Snowflake ID
    long snowflakeId = 1825727719061590016L;
    // 提取时间戳并打印
    long timestamp = extractTimestampFromId(snowflakeId);
    System.out.println("Extracted Timestamp: " + timestamp);
    // 转换为日期格式并打印
    System.out.println("Date: " + new java.util.Date(timestamp));
  }
}

7. 百度uid-generator

项目GitHub地址: GitHub - baidu/uid-generator: UniqueID generator,uid-generator是由百度技术部开发,基于Snowflake算法实现的,与原始的snowflake算法不同在于,uid-generator支持自定义时间戳、工作机器ID和 序列号等各部分的位数,而且uid-generator中采用用户自定义workId的生成策略。

uid-generator需要与数据库配合使用,需要新增一个WORKER_NODE表。当应用启动时会向数据库表中去插入一条数据,插入成功后返回的自增ID就是该机器的workId数据由host,port组成。

8. 美团(Leaf)

Leaf由美团开发,github地址:GitHub - Meituan-Dianping/Leaf: Distributed ID Generate Service,Leaf同时支持号段模式和snowflake算法模式,可以 切换使用

9. 滴滴(Tinyid)

Tinyid 由滴滴开发,Github地址:GitHub - didi/tinyid: ID Generator id生成器 分布式id生成系统,简单易用、高性能、高可用的id生成系统

Tinyid是一个ID生成器服务,它提供了REST API和Java客户端两种获取方式,如果使用Java客户端获取方式的话,官方宣称能单实例能达到1kw QPS(Over10 million QPSper single instance when using the java client.)

Tinyid教程 的原理非常简单,通过数据库表中的数据基本是就能猜出个八九不离十,就是经典的segment模式,和美团的leaf原理几乎一致。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值