题目与范围
服务有大规模 orders 表、子表、公开 URL、事件载荷和副本。PostgreSQL 18 提供原生 uuidv7() 生成器。请在保持 UUIDv4 合约的同时让新记录逐步使用 UUIDv7,避免表重写和长时间阻塞锁。
核心能力是跨 API、存储和异步消费者的在线身份与 Schema 迁移,因此归入 backend。
面试官考察什么
第一,能否区分标识格式与时间真相?UUIDv7 含有时间排序信息,但不是严格全局提交序列,不能替代业务时间戳。
第二,能否保持公开身份?旧 URL、缓存、幂等键、事件和外键在迁移期间都要继续解析。
第三,能否设计双读双写阶段并确定唯一事实来源?
第四,能否在多副本、多地区环境分批回填,同时控制写放大和复制延迟?
第五,能否用实测验证局部性和延迟,而不是假设 UUIDv7 对所有负载都更快?
先澄清的问题
- UUID 是否对外暴露、作为主键,还是两者都是?
- 客户端能否接受新标识,还是旧 UUID 必须保持规范身份?
- 有多少子表、索引、事件和搜索文档引用该键?
- 所有写入器和副本使用哪些 PostgreSQL 版本和扩展?
- 写入速率、复制延迟 SLO 和回滚期限是多少?
- 排序需求用于分页、审计展示,还是只有索引局部性?
30 秒回答框架
“我会保留 UUIDv4 作为稳定公开标识,新增 UUIDv7 映射列,先上线双写再回填。读取同时接受两种键,子表引用和事件保持兼容。回填使用有界主键范围,并监控延迟与锁;完成一致性检查后再切换内部索引与查询路径。UUIDv7 可以改善局部性和时间范围扫描,但业务时间仍需显式保存。全部消费者验证前,用特性开关支持回滚。”
分步作答
第一步:选择兼容形状
不要静默改变现有 UUID 的含义。保留 orderidv4 作为外部合约,新增 orderidv7 与唯一约束,或使用独立内部键和持久映射。明确每个阶段子表和事件使用哪一个键。
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:什么时候删除旧列?
客户端、导出、回放、副本和回滚检查通过约定窗口后,再分独立部署删除约束和索引。