问题出现得很突然,日志倒是留得挺全。文档入库时把文本 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 或者实体解码都会让键分叉,命中率会低到像没开缓存。改动前把原配置备份一份,出问题能马上回滚,这比任何技巧都实用。
