当前位置: 首页 > news >正文

面基:为什么不推荐用UUID作为主键

推荐回答结构:

  1. 技术理论层面分析

  2. 实际项目中的教训

  3. 优化改进过程

  4. 总结提炼认知

阐述回答

在阐述回答时,你可以从 UUID 本身特性带来的问题,以及在实际工作中遇到的具体场景和优化过程等方面展开,下面从这一思路,给出参考回复。

在实际项目开发中,我曾大量使用数据库,也碰到过 UUID 作为主键带来的各种问题,因此对 “不推荐使用 UUID 作为主键” 这一观点有深刻体会。

1. 性能层面

在插入数据时,UUID 的无序性会导致 B + 树索引频繁分裂。在一个数据量达千万级别的分库分表项目中,使用 UUID 作为主键,数据插入时产生大量随机 I/O。

        (UUID作为无序字符串会导致聚簇索引频繁的页分裂。因为InnoDB的数据存储是按主键顺序组织的,插入无序数据时,MySQL不得不频繁进行页分裂重组(我们通过SHOW ENGINE INNODB STATUS观察到大量页分裂事件),这直接导致写入性能下降约40%。)

与之对比,自增主键能够顺序写入磁盘,利用磁盘预读特性,大幅提升写入性能。

经测试,使用自增主键的写入速度比 UUID 主键快约 30% 。

在高并发写入场景下,UUID 主键的无序性,会让数据库产生大量锁争用,导致数据库吞吐量降低,响应时间变长。

2. 存储空间层面

UUID 由 36 个字符组成,长度达到 128 位。

以 MySQL 为例,若使用 VARCHAR (36) 类型存储 UUID 主键,相比于 4 字节的 INT 类型自增主键,存储空间增加了近 9 倍。 

        (UUID的36字符长度(实际存储优化后16字节)相比BIGINT的8字节,不仅使主键索引体积膨胀2倍,二级索引中存储的主键引用也会同步膨胀。我们做过测试:当订单表达到2亿数据量时,UUID方案的总索引大小比自增ID多消耗约300GB存储空间,这对SSD存储成本影响显著。)

当数据量达到 PB 级时,这会显著增加存储成本,并且在数据传输过程中,会占用更多的网络带宽,降低系统整体性能。

3. 查询效率层面

由于 UUID 的随机性,数据库无法有效利用索引进行范围查询。

在电商项目的订单查询中,需要根据订单创建时间进行范围查询。若使用自增主键,数据库可以利用索引快速定位数据;而使用 UUID 主键时,数据库不得不进行全表扫描,查询效率极低,导致响应时间从几百毫秒增加到数秒。

4. 业务场景适配层面

在实际业务中,很多场景需要主键具有一定的业务含义,例如订单号。UUID 缺乏业务含义,难以满足业务需求。而且 UUID 不易记忆和识别,对于用户输入操作来说不太友好。

        UUID业务层面的连锁反应:由于主键无序性,当我们需要按时间范围查询近期订单时,即使有created_time索引,执行计划仍经常出现回表查询。最典型的一个场景是用户查询最近三个月订单,响应时间从最初的200ms逐渐恶化到2秒以上。

基于以上这些在实际工作中遇到的问题,我认为除非有特殊需求,否则不推荐使用 UUID 作为主键。在大多数情况下,自增主键或基于业务规则生成的主键,能更好地满足系统性能和业务需求。

5、优化过程分为三个阶段

  1. 先用分布式ID生成器(雪花算法)替换UUID,保持ID时间有序性

  2. 对历史数据做在线迁移时,采用双写方案逐步过渡

  3. 优化后TPS从1200提升到4800,P99延迟下降65%

这个经历让我深刻认识到:主键设计需要平衡唯一性、有序性和存储效率。现在我们团队将UUID的使用场景严格限制在非索引字段或极低频更新的业务字段上,核心业务表的主键必须满足有序性和空间效率。"

---------分界线------------------------------------------------------------------------------------------------

【难点理解】

B + 树索引与数据插入

  • B + 树索引:在数据库中,B + 树是一种常见的索引结构,它用于快速定位和查找数据。B + 树的节点按照一定的顺序存储数据,通常是按照主键的值进行排序。这种有序的结构使得数据库在查询数据时能够快速地通过索引定位到所需的记录。
  • 数据插入与索引分裂:当向数据库中插入新数据时,数据库需要根据主键的值将新数据插入到 B + 树的合适位置。如果主键是有序的,例如自增的整数,新数据会依次插入到 B + 树的末尾或接近末尾的位置,这样只会导致 B + 树在原有基础上进行少量的扩展。然而,如果主键是无序的,如 UUID,新数据可能会被插入到 B + 树的任何位置。当插入的位置已满时,就会导致 B + 树的节点分裂,以容纳新的数据。节点分裂是一个相对复杂且耗时的操作,它需要重新调整 B + 树的结构,并且可能会导致数据库的 I/O 操作增加。

UUID 无序性导致的随机 I/O

  • UUID 的特性:UUID 是通用唯一识别码,它是一种由数字和字母组成的 128 位标识符,具有全球唯一性和随机性。这意味着 UUID 的值是随机分布的,没有任何规律可循。
  • 随机 I/O 的产生:在分库分表的项目中,数据通常会被分散存储在多个数据库节点或表中。当使用 UUID 作为主键插入数据时,由于其无序性,新数据可能会被随机地插入到不同的数据库节点或表中。这就导致了数据插入时的 I/O 操作也是随机的,而不是像有序主键那样可以进行顺序的 I/O 操作。在数据量达到千万级别时,这种随机 I/O 的影响会更加明显,因为大量的随机插入操作会使得数据库的存储引擎需要频繁地在不同的磁盘位置之间进行切换,以写入新的数据,从而产生大量的随机 I/O。

这种大量的随机 I/O 会严重影响数据库的性能,因为随机 I/O 的速度通常比顺序 I/O 慢很多。它会导致磁盘寻道时间增加,降低数据库的写入吞吐量,增加系统的响应时间,甚至可能导致数据库出现性能瓶颈。

雪花算法 Snowflake

        基本结构:雪花算法生成的ID是64位的长整数,由时间戳、工作机器ID、序列号组成。这三个部分组合起来保证在分布式系统中生成的ID唯一且有序。

       尽管存在时间回拨等限制,但通过合理调整参数和引入优化策略,仍可适应大多数分布式场景需求。

        雪花算法就像给每个事件打一个「时间戳+地点+流水号」的三合一标签,让计算机世界里的所有事情都有序排队。

具体参考URL: 简聊雪花算法Snowflake快乐认知-CSDN博客

---------分界线------------------------------------------------------------------------------------------------

问:UUID(防爬优势)这么好,那么数据库中的主键是用UUID还是自增主键呢?

 分析UUID和自增主键各自的优缺点。

        UUID的优点包括全局唯一性、分布式生成无需协调、防爬;

                缺点则是存储空间大、索引碎片化、查询性能差。

        自增主键的优点是存储紧凑、查询高效、有序性好;

                缺点是易预测、单点故障、分布式环境下生成困难。

这个问题其实触及了系统设计的核心权衡(trade-off),我们可以用「选餐厅」的比喻来理解这个选择困境:


1.场景类比:

假设你要开一家餐厅,纠结菜单用**流水号(自增主键)还是随机暗号(UUID)**来标记订单:

  • 流水号A001, A002, A003...(服务员快速传菜,后厨按顺序处理)

  • 随机暗号X9K4, 3LPQ, R2D2...(顾客看不出订单量,防竞争对手分析)

你发现:

  • 想要传菜快就选流水号,但竞争对手站在门口数A001→A100就知道今天卖了100单

  • 想要保密性好就选随机暗号,但后厨找订单像玩捉迷藏,高峰期容易堵车


2.技术本质对比:

自增主键 (如MySQL Auto Increment)UUID
优点存储小(8字节),索引紧凑,范围查询快全局唯一,无需中心化协调,天然防爬虫
致命缺点易暴露业务规模,单机瓶颈,分布式难扩展存储大(16字节),索引碎片化,查询性能差
适用场景单机/主从架构,内部管理系统分布式系统初期,安全敏感且低QPS场景

3.真实案例决策树:

假设你在开发一个外卖平台:

  1. 如果订单系统是单体架构 → 自增主键 + 业务层加密(例如对外暴露A001转码为Zx3h7T

    • 实际案例:早期美团外卖用自增ID,对外接口通过HMAC算法生成Token隐藏真实ID

  2. 如果订单系统是分布式架构 → 雪花算法/Snowflake(时间有序的分布式ID)

    • 实际案例:饿了么使用改良版雪花ID(64位包含机房、服务节点等信息)

  3. 如果涉及跨国多中心 → UUID v7(新版时间有序UUID)或 ULID

    • 实际案例:Airbnb的预订ID采用ULID,既保证分布式唯一性又保留时间排序


4.防爬虫的深层真相:

不要迷信UUID的防爬能力! 举个反例:

  • 某社交App用UUID作为帖子ID,但爬虫通过时间排序接口+批量拉取照样能抓取数据

  • 更有效的防爬方案:

    // 自增主键 + 加密混淆(Spring Boot示例)
    public String getOrderPublicId(Long dbId) {
        // 1. 用密钥将数字ID转换为无规律字符串
        String cipher = AES.encrypt(dbId.toString(), SECRET_KEY); 
        
        // 2. 转换为URL安全的Base64
        return Base64.encodeBase64URLSafeString(cipher.getBytes());
    }
    • 实际效果:数据库内用自增ID保证性能,对外暴露3q2-7XgtyuJKlw式乱码,兼顾安全与效率


5.终极决策原则:

  1. 先保性能:优先让数据库吃得好(索引效率),再考虑防爬这类业务需求

  2. 分层防御:主键层用适合数据库的机制(自增/雪花ID),安全层用加密/鉴权/限流

  3. 特殊场景特供

    • 金融交易流水 → 自增ID + 前缀(TXN202403011000001 含日期和机构码)

    • 匿名投票系统 → UUID + 哈希加盐(既隐藏投票顺序又保证不可篡改)


6.一句话总结:

选主键就像选工装裤——优先保证干活利索(性能),兜里的暗袋(防爬)只是加分项。真正要防贼,得靠门禁系统(安全架构),而不是指望裤子的花纹迷惑小偷。

(望各位潘安、各位子健/各位彦祖、于晏不吝赐教!多多指正!🙏)

相关文章:

  • 探索多种方案下 LLM 的预训练性能
  • Spring Boot 七种事务传播行为只有 REQUIRES_NEW 和 NESTED 支持部分回滚的分析
  • C++26新特性解读: 结构化绑定作为条件
  • 在linux中GCC、Yum 与 Apt - get 的区别
  • OCRmyPDF 开源核弹
  • PyCharm 下载与安装教程:从零开始搭建你的 Python 开发环境
  • 【江协科技STM32】PWR电源控制(学习笔记)
  • 排序算法-插入排序
  • Tomcat深度解析:Java Web服务的核心引擎
  • Java 线程池与 Kotlin 协程 高阶学习
  • 子网划分2
  • OSPF五种数据包详解
  • FPGA实现LED流水灯
  • Leetcode 3500. Minimum Cost to Divide Array Into Subarrays
  • Spring IOC:容器管理与依赖注入秘籍
  • RK3568 pinctrl内容讲解
  • Python----机器学习(距离计算方式:欧式距离,曼哈顿距离,切比雪夫距离,余弦相似度,汉明距离,闵可夫斯基距离,Jaccard指数,半正矢距离)
  • 探索PHP的未来发展与应用趋势
  • Java面试黄金宝典27
  • transformer架构与其它架构对比
  • 魔都眼|咖啡节上小孩儿忍不住尝了咖啡香,母亲乐了
  • 韩代总统李周浩履职
  • 武汉大学新闻与传播学院已由“80后”副院长吴世文主持工作
  • 三家“券商系”公募同日变更掌门人,新董事长均为公司股东方老将
  • 金砖国家外长会晤主席声明(摘要)
  • 马上评丨准入壁垒越少,市场活力越足