杂记 › 技术笔记 › 自增主键被猜出订单量?换 UUID 前先搞懂代价

自增主键被猜出订单量?换 UUID 前先搞懂代价

去年帮一个做小程序商城的朋友看后台,订单号是自增数字。有客户在客服群里抱怨:上一单 100231,这一单 100237,再翻几个熟人的单号,一天出多少单基本摆出来了。老板拿着截图来问:能不能把对外那串号换成看不出东西的字符。本质不是加密,是不透明——编号只要单调递增,差值就是一个摆在明面上的统计口径。

换成随机串,最常见的是 UUID。我在他那个项目里最后没做全表迁移,只动了一列对外字段,原因在下面这三笔账里。

一、结构:128 位里有 6 位是固定的

UUID 是 128 位的标识,写成 8-4-4-4-12 分组,共 32 个十六进制字符加 4 个连字符:

f3c1a072-8ad4-4b6e-91cf-0d5a2e6b7c10

128 位全用满,总空间是 2 的 128 次方,约 3.4×10 的 38 次方。但 v4 的随机位不是 128 位,是 122 位:从左往右数第 13 个字符(第三个分组的首字符)固定写死为 4,版本位占 4 个二进制位;第 17 个字符(第四个分组的首字符)只能取 8、9、a、b,因为变体位的最高两位固定为 10,占 2 位。128 减 4 减 2 才是随机位。

写成"128 位全随机"是最常见的错误:空间被夸大成 64 倍(2 的 128 次方除以 2 的 122 次方正是 2 的 6 次方),后面的概率估算跟着偏。

二、碰撞概率只能按生日界算

要回答的不是空间大不大,而是生成 n 个之后出现一对重复的概率有多大。口径用生日界。

N = 2^122 = 5316911983139663491615228241121378304
p = 1 - exp( -n*(n-1) / (2*N) )
n 远小于 N 时:p ≈ n*n / (2*N)

这里的 N 请用整数运算取 pow(2,122),浮点会把尾数改掉;上面那串 37 位数就是精确值。反解 p = 0.5,需要的生成量约等于 1.1774 乘以 N 的平方根,也就是 1.1774 乘以 2 的 61 次方(2305843009213693952),得 2714899559048203264 个,量级 2.7×10 的 18 次方。流传的说法把它写成"几十亿",差了十个数量级。

再用极端口径核对:每秒 10 亿个跑满 100 年(一年按 365 天算 31536000 秒)是 3153600000000000000 个,比阈值还多 16%,同一公式代进去概率约 61%。所以"跑很多年也几乎不会重复"方向是反的,它跑到的是抛硬币,不是零。

我实际遇到的量级在另一头:一次灌 1000 万条,n 是 10 的 7 次方,p 约 9.4×10 的负 24 次方,当作不会发生是可以的。但口径要写清:10 的 7 次方这一档安全,10 的 18 次方那一档另说,两档不该塞进同一句话。

三、换成随机主键要付的三笔账

第一笔是索引写入。自增主键永远落在 B+ 树最右边;随机主键的落点由那 122 位决定,页分裂更频繁、缓存命中率下降。共识只能说到这儿:随机主键通常比自增慢、写放大更多。谁给出"慢 3 倍""索引大 4 倍",让他把表结构、数据库版本和压测数据一起拿出来。

第二笔是字节。v4 写成字符串是 36 字节,存成 BINARY(16) 是 16 字节。按他那张 2 亿行的订单表算:字符串主键 7200000000 字节,二进制主键 3200000000 字节,光一列差 4 GB。InnoDB 还把主键值复制进每个二级索引的叶子,4 个二级索引就是主键落 5 份:36 字节约 36 GB,16 字节约 16 GB。列若声明成 utf8mb4 的 CHAR(36),定长按每字符最多 4 字节计,2 亿行是 28.8 GB。

第三笔是人和系统的摩擦。36 个字符没法口头报修,日志里两三条刷满屏幕;随机串不带时间含义,"最近 10 单"要靠 created_at 排,别指望主键顺序可读。

项目 UUID v4(随机) UUID v1(时间戳) 自增 ID
位从哪来 122 位随机,6 位固定 时间戳加节点标识 表内计数器
主键写入形态 落点随机,页分裂多 前段随时间推进 顺序追加
单值字节 36 字符 / 二进制 16 同上 4 或 8 字节
陌生人能读出什么 读不出量级与时间 可能读出生成时间 做减法就知道出单量
顺序可用吗 不可读 跨机不保证单调 严格递增
本站工具 直接出 前端模拟,无真实 MAC 不适用

四、工具只是这条链上的一环

决定用 v4 之后先要灌测试数据。我日常用 UUID生成器:出 v4(随机)或 v1(时间戳),一次批量 1 到 100 个,格式选项是大写或小写、带不带连字符、要不要花括号,单个复制或全部复制都行;要 5000 行就取 50 次。对端只认花括号大写的 GUID 写法时,格式那三项比版本更有用。

100 行 ID 粘进 文本行操作,给每行统一加前后缀,拼成 INSERT 语句的每一行。麻烦从来不在生成,在生成之后那几十次字符串处理。

也拿它做过幂等键:客户端为同一次提交复用同一个键,服务端记下见过的键,重复提交就不再执行第二次。它防的是重复提交,不解决并发正确性——两个请求真同时改同一行,还得靠锁或版本字段。

关于 v1 有一句必须交代:本站工具的 v1 是纯前端模拟,不读取真实 MAC 地址,因此不具备真实 MAC 派生的可追溯性,和库里拿网卡地址当节点标识的 v1 不是一回事。要追到产生它的那台机器,请用库实现。

五、这几个问题我被问过最多

Q:随机位到底是 122 还是 128?

122。版本位 4 位加变体位 2 位固定。记成 128 会把空间夸大约 64 倍,50% 阈值跟着错一档。

Q:想按时间排序,换成 v1 就够了吗?

前段确实随时间推进,但本站 v1 是前端模拟、不含真实 MAC,也不保证跨进程单调。真要有序,常见做法是在 ID 前面自己拼时间分段,别把期望放在随机串的顺序上。

Q:能直接拿 UUID 当口令、邀请码或密钥吗?

不建议。那 122 位随机是为标识唯一服务的,格式写死、生成源不承诺密码学强度,还常整条进日志。要口令就用 随机密码生成,长度和字符集(大小写、数字、符号)由你定,熵能明确算出来。生成源用途不同:一个给人鉴权,一个给机器对账。

Q:已经上线的自增主键要不要迁成 UUID?

先看这个 ID 会不会被陌生人看到。只在内部关联用的表我不动;出现在接口、URL、导出文件里的,加一列对外不透明编号、内部仍留自增,索引重建和外键连锁就只压在一列上。

Q:客户端生成的 UUID 会不会被伪造?

会,而且它本来就不证明任何事:它只保证这一串大概率独一份,不保证请求是谁发的、内容改没改。猜不到不等于可信,鉴权要靠签名。

回到开头那笔单子:朋友的表没改主键,内部 id 仍自增,对外多一列 order_no 存 v4,接口和导出只露它。不透明只需要在暴露面上成立,而索引、字节、摩擦这三笔账在一列上的代价远小于在 2 亿行主键上。要不要换,看你的 ID 会不会被陌生人看到;要换,就先接受随机性的写代价,再从二进制列、索引份数、排序字段这三处把它省回来。

📎 相关文章

PNG 压了三遍只减 426 字节:无损压缩的真实边界 2026-10-22 WebP 转 PNG 后体积涨了几倍:什么时候必须转,怎么把涨幅压住 2026-10-22 截图缩到四分之一,小字糊成一团的三种原因 2026-10-22 同样裁 300×300,头像为什么总偏右下角半格 2026-10-22 按每月 1.7 倍预估 18 个月,脚本和表格为什么给出两个数 2026-10-22