本期聊聊大多数架构师都会涉及的一个问题:对于一个新的系统,该选择自增 ID 还是 UUID?

我们常常会被这两种 ID 困惑,其实在和架构师交流过程中还有一些其他概念,例如数据库 ID、代理键等词汇。

我们从澄清相关概念开始本篇。

分类法和概念澄清

关于 ID 生成,我把常见的 ID 分为如下几类:

  • 根据业务用途:数据库 ID(相当于小名)、业务编码(相当于大名)。
  • 根据实现方式:数据库自增集中实现(自增 ID)、内存算法实现(UUID等)。
  • 根据主键选择:自然主键、代理主键。

相关概念澄清如下:

  • 数据库 ID:用于数据库数据存储的 ID,一般是唯一的,可以由自增 ID 和 UUID 实现,有时候可以作为代理键。
  • 业务编码:在业务上有一定含义,有时候可能不是唯一的,例如订单编号。这部分内容,我们在前面的文章《业务单号生成》中已经讨论过。
  • 自增 ID:由数据库内部自动实现,在数据提交到数据库后返回。一般来说是一个数字,可以单调递增,唯一。
  • UUID:不依赖被存储数据本身,通过一定规则生成的 ID,可以由应用服务器生成,也可以有数据库生成。
  • 自然主键:选择使用业务含义的键作为数据主键,例如使用有含义订单号、员工号。
  • 代理主键:使用与业务无关的 ID 作为数据库中某条数据作为主键。

选择自增 ID 还是 UUID?

这两种 ID 都有一些问题,一般根据场景选择能容忍其问题的选项。

自增 ID 的问题

  • 数据迁移时,难以唯一标记数据。解决方案是生成一个额外的 UUID key,比如,省市数据,不依赖自增 ID 识别数据。
  • 需要先持久化才能从数据库拿到结果,在 ORM 使用上不方便。
  • 如果有幂等性要求的时候处理不方便,UUID 可以更加方便,可以直接使用 UUID 作为目标业务的主键来实现幂等。
  • 生成自增 ID 在高并发的场景下会有性能问题,依赖中心化节点。
  • 有规律,数据增长可以被猜测出来。
  • 长度优先,对于超大规模的数据自增 ID 会被用光。
  • 在分库分表下,无法做到全局唯一,使用范围受限。

UUID 的问题

  • UUID 依赖时间、主机名作为因子,在配置不良的情况下可能存在重复,这个概率比较小。可以使用雪花算法实现。
  • 存储空间更大,导致数据库在同样的数据行数下,UUID 需要更大的索引空间,对于 B 树索引来说,UUID 占据了更多的索引节点,导致索引树变高。
  • 顺序问题,如果需要实现单调递增,需要额外的处理。
  • 过长,UUID 常用的是 32 位,会比自增 ID 长很多。
  • 丢失自然排序能力,相比自增 ID 需要提供额外的字段排序。
  • 可读性较差。
  • 相比自增 ID,UUID 不符合日常使用习惯。

在技术决策时,除了前面的优缺点之外,还和技术惯性有关。下面是几位架构师的经历和说法:

王喜春:某待知名电商公司的项目,使用自增 ID,因为习惯使用数字的自增 ID,好沟通。选择上没有特别的原因,更多的是技术惯性,没有人特别关注和分析。

邓老师: UUID 的优点是不需要在数据层面创建并取回可以直接在对象构造时就能创建,不可被枚举和猜测。但是安全不应该建立在别人猜不出来上,权衡之下还是选择了自增 ID。

李本波:项目背景为公司内部项目,因为技术惯性使用了 UUID 作为 ID(32 位),没有分库分表需求,技术惯性造成的决策。

使用自然主键还是代理主键?

这也是另外一个常见的讨论点,常常有下面三种方案:

  1. 完全使用自然主键,如果找不到合适的代理主键使用组合键。
  2. 完全使用代理主键,所有操作使用代理主键完成,业务键和数据库无关。
  3. 自然主键优先,如果找不到自然主键则使用代理主键。

方案的优缺点:

  • 方案 1,有些场景下自然主键就是找不到,或者不唯一,而组合键使用起来非常不方便。
  • 方案 2,完全使用代理主键在技术上没有问题,优点是对于数据库设计容易达成共识。缺点是在一些场景下需要继续辨析,什么时候用什么键,比如对外暴露的 ID、前后端交互、API 路径等。
  • 方案 3,相对方案 1 和 方案 2 来说比较均衡,在有自然主键时使用自然主键,必要时生成代理主键,缺点是设计上不统一。

结合各种场景实践,建议使用方案 3,总体上来说性价比更合适。

其它的一些问题

如果已经使用了自增 ID,如何做分库分表?

  • 增加一个 UUID key 作为全局唯一的数据标识,并将其他关联的地方修改为 UUID,完成后可以移除掉 UUID。
  • 使用 Range 的方法,给每个分区分配一个自增的起始数字,例如 TiD 采用了这个方案,不过业界很少实现。

外部系统怎么暴露 ID?

  • 优先暴露业务编码,如果没有业务编码,生成一个 UUID 作为 key 来标识数据。外部系统不应对自己的技术选择造成干扰,内部(一个产品,产品可以独立承接业务)系统(微服务、前后端)可以使用自增 ID。
  • 如果业务编码能保证唯一且不变的前提下,就不再生成自增 ID,这样没有必要且容易产生迷惑。(软删除、草稿需求往往不能保证业务编码不重复,业务编码往往需要复用,所以还需要自增 ID)

新的系统如何选择?用 UUID 还是自增 ID?

  • 如果使用 NoSQL 默认就是 UUID。
  • 看业务规模,如果业务规模不大(没有分库分表的需求时)使用自增 ID,如果活跃数据量达到上千万或亿可以考虑分库分表,这时候需要使用 UUID。
  • 如果业务规模和团队习惯已经使用了 UUID,在充分考虑的情况下,可以使用 UUID。
  • 微服务的背景下为了服务之间的幂等、最终一致性等诉求,UUID 更加方便。

表关联使用业务编码还是数据库 ID(假设这两个值不重合)?

一般使用数据库 ID 关联,比如订单 ID 和订单号,优先使用订单 ID 关联,如果需要优化查询的场景,按需冗余业务编码(非必要不使用)。

URL 上使用业务编码还是数据库 ID?

取决于自然主键和代理主键设计,建议和数据库键保持一致。

为什么很多公司从自增 ID 换到了 UUID?

  • 为了实现在分布式场景下的规模下拓展,例如多个应用服务、跨数据库实例唯一性。
  • 在经验里,创业公司一般使用自增 ID,互联网大厂都一般使用 UUID,由大厂的风气和影响造成的决策。
  • 因为使用了 NoSQL,主流的 NoSQL 是分布式数据库(MongoBD 不是一致性优先,它是可用性和性能优先)

ID 的常用注意事项

  • 使用 UUID 需要去掉中横线(32 位),有中横线会无法排序等一些问题。
  • 在分布式的情况下 UUID 不一定就比自增 ID 的性能差,因为 UUID 生成不依赖数据库主节点。
  • 如果需要有序的 UUID,可以考虑使用 ULID 的方案生成。

参考资料

  • 为什么不建议你使用自增主键? https://time.geekbang.org/column/article/285819?utm_identify=geektime&utm_source=geektime-web&utm_medium=adzone&utm_campaign=qconp&gk_source=qconpgeektime-web&utm_term=0222&screen=full
  • Webinar ID 类型的选择和生成 https://shaogefenhao.com/libs/webinar-notes/java-solution-webinar-2.html
Last Updated:
Contributors: lin