问题出现得很突然,日志倒是留得挺全。文档入库时把文本 md5 当缓存键,查询侧用同一套函数生成键,理论上应该全部命中,实际命中率只有 3%。排查发现入库侧在 html 清洗后留了尾部换行,查询侧多调了一次 strip,两边的 md5 完全不同。把缓存 TTL 关掉仍然不命中。

刚开始盯着「缓存键里混进了批次时间戳,每次都不同」,折腾了半个多小时,排掉了。

排查方向其实有限,先把可能性排一排。入库与查询两侧的文本规范化步骤不一致。少数情况下是HTML 实体解码只在入库侧做了一次。

另外,多副本各有一份本地缓存,读写不在同一个实例上。

上来先把两侧的键都打出来比对

python -c "import hashlib;t='示例 文本';print(hashlib.md5(t.encode()).hexdigest(), hashlib.md5(t.strip().encode()).hexdigest())"

再往下统一成一个 normalize 函数再算键

python -c "import re,hashlib;n=lambda s: re.sub(r'\s+',' ',s).strip();print(hashlib.md5(n(' 示例 文本 ').encode()).hexdigest())"

接着把缓存改成共享实例并统计命中

python -c "print('redis hget/hset with same key')"

剩下的就是对命中率加一条按小时的分组统计

python -c "print('hit/miss by hour')"

改完先别撤,确认这几项。同一段文本在入库与查询两侧算出的 md5 完全一致,缓存命中率升到 95% 以上,重复入库的条数明显减少。

提醒一点。缓存键的生成逻辑必须收敛到一个函数里,任何一侧多做一次 strip 或者实体解码都会让键分叉,命中率会低到像没开缓存。改动前把原配置备份一份,出问题能马上回滚,这比任何技巧都实用。

作者 9823189