最稳妥的动态Key设计是用fmt.Sprintf将确定性业务维度(如userID、标准化日期、pageNum)按固定格式拼接,统一用:分隔、%d/%s格式化、避免JSON/URL编码、过滤无关参数,并控制Key长度不超过256字节。

用 fmt.Sprintf 拼接多层业务参数最稳妥
动态 Key 的核心是把变化的业务维度(如用户 ID、时间范围、分页页码)稳定地转成字符串,且保证相同语义一定生成相同 Key。直接字符串拼接容易漏分隔符或类型转换出错,fmt.Sprintf 能强制类型对齐、显式控制格式。
常见错误现象:user:123:report:2024-05 和 user:123:report:2024-5 被当成两个 Key;或整数 ID 传了指针导致 %v 输出地址。
- 固定用
:作分隔符,避免和业务数据冲突(比如用户名含下划线) - 时间统一转为
2006-01-02格式,不用time.Now().Format()直接拼,防止时区/精度干扰 - ID 类字段一律用
%d(整型)或%s(已校验过的字符串),不混用%v - 示例:
fmt.Sprintf("user:%d:report:%s:page:%d", userID, dateStr, pageNum)
别在 Key 里塞 JSON 或 URL 编码后的值
有人想“保险起见”把整个查询参数 map 序列化成 JSON 再当 Key 一部分,结果发现缓存命中率暴跌——JSON 字段顺序不固定、空格缩进不一致、浮点数精度差异都会让相同逻辑产生不同 Key。
URL 编码更危险:%20 和空格等价,但编码与否全看调用方,下游服务可能未解码就拼 Key。
- 只取确定性高的原始字段:ID、枚举值、标准化日期字符串
- 过滤掉非关键参数(如
trace_id、source这类不影响结果的) - 如果必须带模糊条件(如搜索关键词),先做归一化:
strings.TrimSpace(strings.ToLower(keyword))
redis-cli --scan 查不到动态 Key?检查游标和匹配模式
用 SCAN 查动态 Key 时经常返回空,不是 Key 不存在,而是匹配模式写错了或游标没重置。Redis 的 SCAN 不支持通配符递归,* 只匹配单层,** 无效。
典型错误:SCAN 0 MATCH user:*:report:*:page:* 看似合理,但如果某次 Key 是 user:123:report:2024-05:page:1:extra,它就不会被匹配到。
- 匹配模式用
user:*:report:*:page:*即可,末尾不加多余冒号或通配符 - 命令行执行时加
--scan会自动处理游标,但首次必须从0开始,别手输错成1 - 线上环境慎用
KEYS *,会阻塞 Redis;调试时优先用SCAN+COUNT 1000
Key 太长或嵌套太深会影响性能和运维
见过最长的 Key 达到 512 字节:包含完整 URL、Base64 用户信息、毫秒级时间戳。这不仅拖慢网络传输,还让 redis-cli monitor 日志难以阅读,排查缓存穿透时根本没法快速定位模式。
Redis 对 Key 长度无硬限制,但实际中超过 256 字节就该警惕——序列化开销、内存碎片、集群 slot 计算都受影响。
- 去掉冗余前缀,比如
cache_user_report_v2_压缩为u:r: - 时间戳精确到天即可,除非业务真需要区分小时级缓存
- 用哈希摘要压缩长字段:
md5.Sum([]byte(userID + "-" + dateStr))[0:8](注意:仅用于 Key,勿用于安全场景) - 示例优化后:
u:r:7a3f9c1d:p:1(比user:123456789:report:2024-05-20:page:1短 62%)
Key 设计没有银弹,但所有复杂性都来自没想清楚“哪些变量真正影响缓存结果”。一旦开始往 Key 里塞 trace_id、request_id、随机 salt,基本就说明缓存策略本身需要重构了。










