快思学习空间

首页/ 学习笔记/ 判重策略

文件秒传背后的判重策略

备份类应用第二次运行时的核心问题:怎么判断「这个文件已经传过了」。 这篇笔记整理两级判重的思路 —— 用便宜的手段粗筛,用昂贵的手段确认。

问题本身很简单

同步和备份类应用都绕不开一个问题:怎么知道某个文件在远端已经存在了? 判错的代价是不对称的 —— 误判为「不存在」只是多传一次,浪费点带宽; 误判为「已存在」则可能导致文件永远不会被真正备份,是数据丢失级别的错误。

所以判重策略的目标不是「最快」,而是在几乎不出错的前提下尽量快。

最朴素的做法,以及它为什么不够

最直接的办法是算文件内容的哈希,拿哈希去比对:

hash = SHA-256(读取文件全部字节)

这个办法正确性没问题,但在「判断一万张照片是否需要备份」的场景下代价太高: 为了判断需不需要传,得先把所有文件完整读一遍。 而绝大多数情况下结论都是「不需要传」—— 等于每次同步都白白把整个相册读一遍。

问题的本质是:我们用了最贵的手段去做最常见的判断。

两级策略:粗筛 + 确认

改法是把判断拆成两级,越便宜的越先做:

L1:元数据指纹(粗筛)

只用文件的元数据字段拼出一个指纹,完全不读文件内容:

L1 = SHA1( 拍摄时间_秒 + "|" + 宽 + "x" + 高
         + "|" + 时长毫秒 + "|" + 媒体子类型 )

它的特点:

  • 零 IO。相册的元数据通常已经加载在内存里,十万张照片算一遍是秒级的事
  • 不需要精确。它的职责只是把候选范围缩小,不是给出最终结论
  • 字段必须选跨设备稳定的值 —— 这一点下面单独说

L2:内容哈希(确认)

L2 = SHA-256(文件原始字节)

只在 L1 命中候选时才计算。 这是整个方案性能的关键点:绝大多数文件在 L1 阶段就被判定为「远端没有」, 直接进上传队列,根本不需要读内容。

现代 ARM 处理器普遍带 SHA 扩展指令,主流平台的密码学库 (iOS 的 CryptoKit、HarmonyOS 的 cryptoFramework)都会走硬件加速, 吞吐可以到 100MB/s 以上,单张照片的哈希耗时基本可以忽略。

关于「碰撞」:它不是 bug,是设计的一部分

L1 指纹会有碰撞 —— 最典型的是同秒连拍、动态照片的静帧与封面、连续截图。 这些文件的拍摄时间、尺寸、时长往往完全一致,指纹自然相同。

关键在于:这些恰好正是需要 L2 确认的场景。 粗筛的职责就是把「拿不准的」挑出来交给下一级, 而不是自己下结论。如果 L1 永远不碰撞,那说明它选的字段太细,已经接近在做内容哈希了,反而失去了速度优势。

判重决策表

L1 是否命中L2 比对结果动作
无命中不计算直接上传,省掉一次全文件读取
命中 1 条一致跳过,建立映射关系
命中 1 条不一致上传(元数据巧合,实际是不同文件)
命中 N 条逐条比对一致则跳过;全部不一致则上传
本地已有 L2 且远端确认过不计算直接跳过 —— 最快的路径

注意第一行:L1 没命中就不算 L2。 这条规则让首轮全量同步的额外哈希开销被压到很小 —— 只有 L1 撞车的那一小部分文件才需要读内容。 第二轮开始走本地缓存,开销近乎为零。

哪些字段不能进指纹

这是很容易踩的一个坑。移动端相册会给每个资源一个本地唯一标识 (iOS 的 localIdentifier、HarmonyOS 的 uri), 看起来是天然的完美主键,但它换设备或重装后会变。

所以正确的用法是分开:

  • 进 L1 指纹的:拍摄时间、尺寸、时长、类型 —— 这些跟着文件走,换机不变
  • 不进 L1 的:本地唯一标识 —— 单独存一列,只用于同一台设备上的增量识别

秒传:把判断提前到传输之前

上面的流程仍是「客户端算好再决定传不传」。秒传更进一步: 客户端在发起上传请求时,就把已知的内容哈希一起报上去, 服务端查索引,命中则直接返回「已存在」,零字节传输。

POST /upload/init
{ "name": "IMG_0421.HEIC", "size": 3145728,
  "content_sha256": "9f86d081884c7d65..." }

→ 200 { "skipped": true, "path": "/photo/2026/09/IMG_0421.HEIC" }

这个机制也解释了为什么网盘能做到「几秒传完一个 GB 级文件」—— 传的从来不是文件,只是一个哈希值。

把哈希计算放到常电设备上

一个容易被忽略的优化:手机算哈希要耗电,而 NAS 常年插着电。 所以让服务端在文件落盘时顺手算一次哈希并存进索引, 之后所有客户端都能直接命中这个索引,不需要各自重复计算。

代价是首次落盘时服务端要多做一次全文件读取 —— 但那是它本来就写过的数据, 大概率还在页缓存里,成本远低于让每台客户端各算一遍。

哈希算法怎么选

  • MD5 / SHA-1:已经不适合任何安全用途(碰撞攻击早就有实用化的成果), 但在纯判重场景仍常见,因为快。要用的话需要清楚知道自己在依赖「碰撞极难发生」这个统计假设。
  • SHA-256:当前的稳妥默认值,硬件加速支持好, 在判重这种「碰撞即丢数据」的场景下没有理由省这一点开销。

我的结论很简单:判重场景下哈希碰撞的后果是文件被认为已备份而实际上没有, 这是数据丢失。宁可慢一点,也要用抗碰撞的算法。

小结

两级判重其实是一个很通用的模式: 先用廉价特征把候选集缩小,再用昂贵手段做最终确认。 这个思路在缓存失效判断、去重存储、增量同步里都反复出现。 真正需要想清楚的是两件事:粗筛用的特征在什么条件下会失效,以及 判错的代价由谁来承担。

← 上一篇:JWT 与 JWKS 下一篇:TLS 证书指纹固定 →