代表性面试主题

后端面试:如何无停机把 UUIDv4 迁移到 UUIDv7?

后端困难
Offer.cc 编辑团队发布 更新

题干

多地区服务使用随机 UUIDv4 主键,索引局部性和分页成本变差。请设计迁移到 PostgreSQL 18 uuidv7() 的渐进方案,不能阻塞写入或破坏客户端,并解释键形状、双写、回填、外键、排序、复制、验证与回滚。

题目与范围

服务有大规模 orders 表、子表、公开 URL、事件载荷和副本。PostgreSQL 18 提供原生 uuidv7() 生成器。请在保持 UUIDv4 合约的同时让新记录逐步使用 UUIDv7,避免表重写和长时间阻塞锁。

核心能力是跨 API、存储和异步消费者的在线身份与 Schema 迁移,因此归入 backend

面试官考察什么

第一,能否区分标识格式与时间真相?UUIDv7 含有时间排序信息,但不是严格全局提交序列,不能替代业务时间戳。

第二,能否保持公开身份?旧 URL、缓存、幂等键、事件和外键在迁移期间都要继续解析。

第三,能否设计双读双写阶段并确定唯一事实来源?

第四,能否在多副本、多地区环境分批回填,同时控制写放大和复制延迟?

第五,能否用实测验证局部性和延迟,而不是假设 UUIDv7 对所有负载都更快?

先澄清的问题

  • UUID 是否对外暴露、作为主键,还是两者都是?
  • 客户端能否接受新标识,还是旧 UUID 必须保持规范身份?
  • 有多少子表、索引、事件和搜索文档引用该键?
  • 所有写入器和副本使用哪些 PostgreSQL 版本和扩展?
  • 写入速率、复制延迟 SLO 和回滚期限是多少?
  • 排序需求用于分页、审计展示,还是只有索引局部性?

30 秒回答框架

“我会保留 UUIDv4 作为稳定公开标识,新增 UUIDv7 映射列,先上线双写再回填。读取同时接受两种键,子表引用和事件保持兼容。回填使用有界主键范围,并监控延迟与锁;完成一致性检查后再切换内部索引与查询路径。UUIDv7 可以改善局部性和时间范围扫描,但业务时间仍需显式保存。全部消费者验证前,用特性开关支持回滚。”

分步作答

第一步:选择兼容形状

不要静默改变现有 UUID 的含义。保留 order_id_v4 作为外部合约,新增 order_id_v7 与唯一约束,或使用独立内部键和持久映射。明确每个阶段子表和事件使用哪一个键。

sql
ALTER TABLE orders ADD COLUMN order_id_v7 uuid;
CREATE UNIQUE INDEX CONCURRENTLY orders_order_id_v7_uq
  ON orders(order_id_v7) WHERE order_id_v7 IS NOT NULL;

只有目标 PostgreSQL 提供 uuidv7() 时才直接使用;否则先部署一致的生成器。

第二步:先部署写入

新写入在同一事务生成两种标识并记录映射。暂时无法填充新列的旧写入仍有效。让重试幂等,避免重复命令产生两个映射。

第三步:增加双读解析

API 接受任一标识,解析到同一个规范订单,旧响应继续返回 v4。新接口可在版本化合约中暴露 v7。缓存键使用解析后的规范身份,避免两种形式分叉。

第四步:分批回填

使用稳定游标回填,小事务执行;复制延迟、锁等待或写延迟超阈值就暂停。数据足够后再并发建立新索引。父映射持久化前不要更新子表。

第五步:迁移引用和事件

重叠期间保持子表外键和事件 Schema 兼容。v7 字段先设可选,同时发布两个值;所有消费者升级后再设为必填。切换约束前先对账缺失和重复映射。

第六步:切换内部访问路径

一致性检查通过后,在局部性或时间扫描受益的内部连接和分页中使用 UUIDv7。业务排序仍使用显式 created_at 和稳定 tie-breaker。

第七步:验证与观测

在匹配负载下比较索引大小、页分裂、缓存、插入延迟、范围扫描延迟、复制延迟和错误率。确认 v4 与 v7 查询返回同一行,事件消费者仍幂等。

第八步:回滚窗口后再退役

所有客户端、回放工具、导出、复制和副本通过迁移窗口前,保留映射、旧索引和双读。旧约束分独立部署移除,并准备回滚。

示例回答

“我不会原地改写公开 UUID 合约,而是新增 UUIDv7 列和唯一映射,先双写,再让双读解析两种形式。回填使用有界事务,监控锁、复制延迟和写延迟。子表与事件在过渡期同时携带两种 ID,消费者升级后再切换。完成一致性与局部性实测后,切换内部连接和分页,但业务排序保留 created_at。特性开关在旧路径退役前支持反向切换。”

常见错误

  • 立即替换公开 UUID → 链接和事件中断 → 保留规范合约与映射。
  • 把 UUIDv7 当严格时间序 → 分页跳过或重排 → 使用显式时间戳和 tie-breaker。
  • 一笔大事务回填 → 锁与复制延迟尖峰 → 分批执行。
  • 映射未完成就加外键 → 过渡写入失败 → 先父表、再子表、最后约束。
  • 混合不支持版本生成 UUID → 语义不一致 → 固定版本或统一生成器。
  • 忽略重试 → 重复映射 → 让双写幂等。
  • 过早删除 v4 → 旧导出和回放失败 → 等待回滚窗口结束。

追问

追问 1:UUIDv7 能替代 created_at 吗?

不能。它可改善局部性并携带时间位,但业务排序需要显式时间戳和确定性 tie-breaker。

追问 2:v4 和 v7 能共用 UUID 列吗?

UUID 类型可以容纳两种格式,但迁移元数据、公开合约和排序语义仍需明确规划。

追问 3:为什么先双读再切换写入?

这样在回填和消费者升级未完成时,新旧记录都能一致解析。

追问 4:如何限速回填?

采用有界事务,复制延迟、锁等待、CPU 或写延迟超阈值就暂停,从持久游标恢复。

追问 5:事件必须包含什么?

过渡期间发布两个 ID 或稳定映射引用,版本化 Schema,消费者升级后再强制 v7。

追问 6:什么时候删除旧列?

客户端、导出、回放、副本和回滚检查通过约定窗口后,再分独立部署删除约束和索引。

公开来源

同类题目