Java-111 深入浅出 MySQL 分布式主键策略:UUID、SnowFlake、COMB、Redis、数据库ID表优劣全对比

点一下关注吧!!!非常感谢!!持续更新!!!

🚀 AI篇持续更新中!(长期更新)

AI炼丹日志-31- 千呼万唤始出来 GPT-5 发布!"快的模型 + 深度思考模型 + 实时路由",持续打造实用AI工具指南!📐🤖

💻 Java篇正式开启!(300篇)

目前2025年08月18日更新到:
Java-100 深入浅出 MySQL事务隔离级别:读未提交、已提交、可重复读与串行化

MyBatis 已完结,Spring 已完结,Nginx已完结,Tomcat已完结,分布式服务正在更新!深入浅出助你打牢基础!

📊 大数据板块已完成多项干货更新(300篇):

包括 Hadoop、Hive、Kafka、Flink、ClickHouse、Elasticsearch 等二十余项核心组件,覆盖离线+实时数仓全栈!
大数据-278 Spark MLib - 基础介绍 机器学习算法 梯度提升树 GBDT案例 详解

主键策略

在很多小项目中,我们往往直接使用数据库自增特性来生成主键ID,这样确实比较简单,而在分库分表的环境中,不能再借助数据库自增长特性直接生成,否则会造成不同数据表主键重复。

UUID(通用唯一识别码)

基本概念

UUID(Universally Unique Identifier)是一种128位(16字节)的数字标识符,用于在分布式系统中唯一地标识信息。其标准格式由32个十六进制数字组成,以连字符"-"分隔为5组,呈现为8-4-4-4-12的形式,总长度为36个字符(例如:550e8400-e29b-41d4-a716-446655440000)。

生成机制

UUID的生成通常结合多种系统信息以确保唯一性:

  1. 网络硬件信息(如MAC地址)
  2. 高精度时间戳(纳秒级)
  3. 硬件芯片ID
  4. 随机数生成器
  5. 命名空间(在特定版本中)

常见的版本包括:

  • Version 1:基于时间戳和MAC地址
  • Version 4:基于随机数生成
  • Version 5:基于命名空间和散列值

数据库应用特点

优势
  1. 分布式生成:无需中央服务器协调,各节点可独立生成
  2. 唯一性保证:在理论上有极低的重复概率(约10^38分之一)
  3. 无网络开销:本地生成不需要网络请求
  4. 安全性:不暴露业务信息(相比自增ID)
劣势
  1. 存储开销:占用16字节,是BIGINT的两倍
  2. 索引效率:在InnoDB引擎中,由于:
    • 无序性导致B+树频繁分裂重组
    • 二级索引需要存储完整的主键值
    • 增大了内存占用和IO操作
  3. 可读性差:人类难以直观记忆和识别

InnoDB引擎下的特殊影响

  1. 聚集索引问题:InnoDB使用主键作为聚簇索引,UUID的无序插入会导致:

    • 页分裂频率增加
    • 数据存储碎片化
    • 缓存命中率下降
  2. 二级索引膨胀:每个二级索引都包含主键值,16字节的UUID会导致:

    • 索引文件体积增大
    • 内存缓冲池能缓存的索引数量减少
    • 范围查询时需要加载更多数据页

优化方案

对于必须使用UUID的场景:

  1. 使用有序UUID变种(如COMB UUID)
  2. 将UUID转换为二进制(16)存储
  3. 建立自增ID作为聚簇索引,用UUID作为业务键
  4. 考虑使用UUID的短哈希版本(需评估碰撞概率)

应用场景推荐

适合使用UUID的情况:

  • 需要离线生成的分布式系统
  • 需要提前知道主键值的业务
  • 需要隐藏数据规模的场景
  • 多系统数据合并的场景

COMB(UUID变种)详解

基本概念

COMB(combine)型是数据库领域特有的一种设计思想,它是一种改进型的GUID/UUID实现方式。这种设计通过将传统GUID/UUID与系统时间信息进行组合,显著提升了数据库索引和检索性能。

技术背景

在标准数据库中并不存在原生的COMB数据类型,这个概念最早由Jimmy Nilsson在其技术文章《The Cost of GUIDs as Primary Keys》中提出并详细阐述。该文章深入分析了传统GUID作为主键的性能问题及其解决方案。

设计原理

COMB的设计基于以下技术考量:

  1. 传统GUID/UUID是完全随机的128位标识符
  2. 这种随机性导致数据库索引出现严重的碎片化问题
  3. 数据插入时的随机分布导致索引效率低下,影响系统整体性能

具体实现方案

COMB采用分段组合的方式重构GUID:

  1. 保留部分:保持GUID前10个字节(80位)不变,确保唯一性
  2. 时间部分 :使用后6个字节(48位)存储GUID生成的时间戳(DateTime)
    • 精确到毫秒级时间信息
    • 时间戳采用大端序存储

性能优势

这种组合方式带来显著优势:

  1. 保持唯一性:前10个字节仍保证全局唯一
  2. 增加有序性:时间戳使新生成的ID呈现递增趋势
  3. 索引优化
    • 减少索引碎片
    • 提高范围查询效率
    • 优化数据页填充率

典型应用场景

  1. 分布式数据库主键设计
  2. 高并发订单系统
  3. 需要频繁插入的日志系统
  4. 大型电商平台的商品ID生成

与其他方案的对比

特性 标准UUID COMB
唯一性 保证 保证
有序性 时间有序
索引效率 较高
存储空间 16字节 16字节
生成复杂度 简单 中等

实现示例(伪代码)

csharp 复制代码
Guid GenerateCombGuid()
{
    byte[] guidBytes = Guid.NewGuid().ToByteArray();
    DateTime now = DateTime.UtcNow;
    
    // 将时间信息写入后6字节
    byte[] timeBytes = BitConverter.GetBytes(now.Ticks);
    Array.Copy(timeBytes, 0, guidBytes, 10, 6);
    
    return new Guid(guidBytes);
}

注意事项

  1. 时间部分精度需根据业务需求调整
  2. 在极高并发环境下仍需考虑冲突问题
  3. 跨时区系统需要统一使用UTC时间
  4. 6字节时间戳可表示约8925年的时间范围

SnowFlake 分布式ID生成算法

在分布式系统中,我们经常需要一种能够全局唯一且按时间有序的ID生成方案。SnowFlake正是Twitter为解决这一问题而开源的分布式ID生成算法,它生成的ID是一个64位的long型整数。

数据结构解析

SnowFlake的64位ID由以下部分组成:

  1. 符号位(1bit):始终为0,保证生成的ID为正数

  2. 时间戳部分(41bit)

    • 记录生成ID的时间戳(毫秒级)
    • 41位可以表示的时间跨度约为69年(2^41/1000/60/60/24/365)
    • 通常从系统上线时间开始计算,例如2020-01-01 00:00:00
  3. 工作机器ID(10bit)

    • 高5位表示数据中心ID(最大支持32个数据中心)
    • 低5位表示机器ID(每个数据中心最大支持32台机器)
    • 这种设计可以支持最多1024台机器(32*32)
  4. 序列号(12bit)

    • 同一毫秒内产生的不同ID的序列号
    • 12位支持每个节点每毫秒产生4096个ID(2^12)

工作流程

  1. 当收到ID生成请求时,首先获取当前时间戳
  2. 如果当前时间戳小于上次生成ID的时间戳,说明系统时钟回拨,需要抛出异常
  3. 如果是同一毫秒内的请求,则递增序列号
  4. 如果序列号溢出,则等待至下一毫秒
  5. 最后将各部分数值通过位运算拼接成最终的64位ID

应用场景

  1. 分布式系统:作为全局唯一的事务ID
  2. 数据库主键:替代自增ID,避免分库分表时的ID冲突
  3. 消息队列:作为消息的唯一标识
  4. 日志追踪:作为请求链路的追踪ID

优势与限制

优势

  • ID自增趋势,利于数据库索引
  • 不依赖第三方服务,本地生成
  • 高性能,单机每秒可生成数百万ID

限制

  • 依赖系统时钟,时钟回拨会导致ID重复
  • 工作机器ID需要提前配置,不利于动态扩容

实现示例(伪代码)

java 复制代码
public class SnowFlake {
    private long datacenterId;  // 数据中心ID
    private long workerId;      // 机器ID
    private long sequence = 0L; // 序列号
    private long lastTimestamp = -1L; // 上次时间戳
    
    public synchronized long nextId() {
        long timestamp = timeGen();
        if (timestamp < lastTimestamp) {
            throw new RuntimeException("时钟回拨异常");
        }
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }
        lastTimestamp = timestamp;
        return ((timestamp - epoch) << timestampLeftShift)
                | (datacenterId << datacenterIdShift)
                | (workerId << workerIdShift)
                | sequence;
    }
}

如下图所示:

● 符号位:固定位0,二进制表示最高位是符号位,0代表正数,1代表负数

● 时间戳:41个二进制数用来记录时间戳,表示某一个毫秒(毫秒级)

● 机器ID:代表当前算法运行机器的ID

● 序列号:12位,用来记录某个机器同一个毫秒内产生的不同序列号,代表同一个机器同一个毫秒可以产生ID序号

SnowFlake 生成的ID整体上按照时间自增排序,并且整个分布式系统内不会产生ID重复,并且效率较高。经过测试SnowFlake每秒能够产生26万个ID。缺点是强依赖机器时钟,如果多台机器环境时钟没同步,或者时钟回拨,会导致发号重复或者服务会处于不可用的状态,

因此一些互联网公司也基于上述的方案做了封装,例如百度的uidgenerator(基于SnowFlake)和美团leaf(基于数据库和SnowFlake)等。

数据库ID表(分布式ID生成方案)

核心原理

通过独立维护一个专门用于生成全局唯一ID的数据库表,利用MySQL的自增ID特性实现分布式环境下的ID生成。

实现方案详解

  1. 独立ID库建设

    • 单独创建一个MySQL数据库实例(如命名为id_generator_db

    • 在该库中创建专门用于ID生成的表(如global_id_table

    • 表结构设计示例:

      sql 复制代码
      CREATE TABLE global_id_table (
        id bigint NOT NULL AUTO_INCREMENT,
        stub char(1) NOT NULL DEFAULT '',
        PRIMARY KEY (id),
        UNIQUE KEY stub (stub)
      ) ENGINE=InnoDB;
  2. ID生成流程

    • 业务系统需要ID时,执行以下SQL:

      sql 复制代码
      REPLACE INTO global_id_table (stub) VALUES ('a');
      SELECT LAST_INSERT_ID();
    • 获取到ID后即可用于业务表的插入操作

  3. 分表场景应用

    • 以A表分表为例:
      • 先向global_id_table获取全局ID

      • 根据分表规则(如ID取模)决定插入A1还是A2表

      • 示例代码:

        java 复制代码
        // 获取全局ID
        long id = getIdFromGlobalTable();
        
        // 确定分表
        String tableName = "A" + (id % 2 + 1); // A1或A2
        
        // 插入业务表
        insertIntoTable(tableName, id, ...);

优化与注意事项

  1. 性能优化

    • 可使用连接池管理ID库连接
    • 批量获取ID:通过设置auto_increment_increment参数批量分配ID段
  2. 高可用方案

    • 部署主从架构,避免单点故障
    • 可考虑多机房部署ID生成服务
  3. 使用限制

    • 单库吞吐量有限(约1-2万QPS)
    • 跨机房调用可能产生网络延迟
    • 需注意自增ID的溢出问题(使用bigint类型)

替代方案对比

当单库性能不足时,可考虑:

  1. 分库分表:将ID表水平拆分到多个库
  2. 号段模式:每次获取一个ID范围段
  3. Snowflake算法:分布式ID生成算法

典型应用场景

  1. 电商订单系统
  2. 社交网络中的动态ID生成
  3. 物流系统的运单编号
  4. 金融交易流水号生成

通过这种方案,可以在分布式系统中保证ID的全局唯一性,同时维持较好的顺序性,便于分库分表场景下的数据路由。

例如,下面 DISTRIBUTE_ID就我们创建要负责ID生成的表,结构如下:

sql 复制代码
CREATE TABLE DISTRIBUTE_ID (
id bigint(32) NOT NULL AUTO_INCREMENT COMMENT '主键',
createtime datetime DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

当分布式集群环境中哪个应用需要获取一个全局唯一的分布式ID的时候,就可以使用代码连接这个数据库实例,执行如下SQL语句即可。

sql 复制代码
INSERT INTO DISTRIBUE_ID(createtime) VALUES(NOW());
SELECT LAST_INSERT_ID();

这里需要注意以下几点:

● createtime字段的设计考量:

  • 该字段本身没有实际的业务含义
  • 主要目的是为了满足数据库表结构的完整性要求
  • 通过插入任意数据来触发数据库的自增ID机制
  • 这种设计虽然简单,但可能会造成数据冗余

● 使用独立MySQL实例生成分布式ID的局限性:

  1. 性能问题:
  • 每次获取ID都需要建立数据库连接
  • 高并发场景下会成为系统瓶颈
  • 网络延迟会影响ID获取速度
  • 无法满足毫秒级的ID生成需求
  1. 可靠性问题:
  • 存在单点故障风险
  • MySQL服务宕机会导致整个系统无法获取ID
  • 数据库维护期间无法提供服务
  • 网络分区时可能无法连接
  1. 扩展性问题:
  • 难以应对业务量快速增长
  • 垂直扩展有上限
  • 水平扩展实现复杂
  • 无法实现无缝扩容
  1. 其他问题:
  • 增加了系统复杂度
  • 需要维护额外的数据库连接池
  • 跨机房部署困难
  • ID生成效率受限于数据库性能

建议考虑更专业的分布式ID生成方案,如雪花算法、UUID等,这些方案在性能和可靠性方面都有更好的表现。

Redis生成ID

背景与需求

在分布式系统中,生成全局唯一ID是一个常见的需求。传统的数据库自增ID在并发量大的场景下可能面临性能瓶颈,因为:

  1. 数据库生成ID需要磁盘I/O操作
  2. 高并发时容易产生锁竞争
  3. 扩展性较差

Redis解决方案的优势

Redis作为内存数据库,具有以下特点使其适合生成ID:

  1. 单线程模型保证原子性操作
  2. 高性能(10万+ QPS)
  3. 支持持久化,保证数据安全

实现方式

1. 基础INCR命令
redis 复制代码
INCR id_counter
  • 每次执行自动将键值加1
  • 返回新的整数值作为ID
  • 示例:第一次调用返回1,第二次返回2
2. 批量生成ID(INCRBY)
redis 复制代码
INCRBY id_counter 1000
  • 一次性获取一段ID范围
  • 适合批量操作的场景
  • 服务端缓存这部分ID本地分配
3. 时间戳组合模式
redis 复制代码
INCR daily_counter
  • 生成格式:年月日(8位) + 自增序列(6位)
  • 例如:20230515-000001
  • 每天自动重置计数器

应用场景

  1. 订单系统:生成唯一订单号
  2. 日志系统:为每条日志标记唯一ID
  3. 分布式锁:基于ID实现锁机制
  4. 消息队列:消息的唯一标识

注意事项

  1. 需要设置适当的持久化策略(AOF或RDB)
  2. 集群环境下建议使用固定节点生成ID
  3. 可配合Lua脚本实现更复杂的ID生成逻辑
  4. 初始化时需要设置合适的初始值

性能对比

方案 QPS 优点 缺点
数据库自增ID 1k-5k 简单可靠 性能受限
Redis INCR 50k+ 高性能,原子性 需要维护Redis服务
UUID 100k+ 无需中心节点 ID较长,无序

通过合理使用Redis生成ID,可以在分布式系统中获得高性能的唯一ID生成方案。

也可以使用Redis集群来获取更高的吞吐量,假设一个集群中有5台Redis,可以初始化每台Redis的值分别是1,2,3,4,5,然后步长都是5,那么:

shell 复制代码
A:1,6,11,16,21
B:2,7,12,17,22
C:3,8,13,18,23
D:4,9,14,19,24
E:5,10,15,20,25