对象 key 设计与元数据一致性

对象 key 不要用原始文件名

对象存储的 key(路径)是文件的「物理地址」。不要直接使用原始文件名,推荐包含业务、日期和随机 ID:

uploads/{tenant_id}/{yyyy}/{mm}/{dd}/{file_id}.{ext}

这样做的好处:

  • 避免同名文件覆盖。
  • 支持按租户和日期做清理、统计、迁移。
  • 不暴露用户上传的原始文件名。
  • 方便接入冷热分层和生命周期策略。

原始文件名只保存在元数据表里,用于展示和下载时的 Content-Disposition——用户看到的是「合同.pdf」,存储里是「uploads/…/x9f3.pdf」。

对象存储不知道业务

对象存储只知道「有一个对象」,不知道它属于哪个用户、是否通过审核、能不能下载。因此数据库仍然是业务事实来源:

created -> uploading -> uploaded -> scanning -> available
                                      -> rejected
                                      -> deleted

一致性问题:谁先谁后

迁移到对象存储后,存在两类一致性问题:

  • 对象上传成功,但数据库更新失败 → 对象存在、记录缺失(孤儿对象)。
  • 数据库记录存在,但对象不存在 → 记录存在、内容缺失(悬挂记录)。

不要强行做分布式事务,而是用状态机和补偿任务修复:

  • uploading 状态超时未完成 → 清理临时对象。
  • uploaded 状态迟迟未进入扫描 → 重新投递扫描任务。
  • 定期比对数据库与对象存储 → 发现孤儿文件 / 悬挂记录并清理。

幂等与重试

对象存储上传可能重试,业务侧也要幂等:

  • 重复的完成通知,不产生重复记录。
  • 回调重试要有幂等键。
  • 状态流转只允许「前进」,防止回退造成混乱。

小结

  • 对象 key 用「业务 / 日期 / 随机 ID」,避免同名覆盖、便于治理。
  • 数据库是业务事实来源,对象存储只管内容。
  • 一致性问题靠状态机 + 补偿任务修复,不靠分布式事务。

下一节看对象存储最容易被忽视的一面:生命周期与成本。

发现内容有问题?

告诉我们哪里需要改进。