用户端最凶的流量,经常是商品/服务列表:商城 SKU、外卖菜品、上门家政服务项,按平台、店铺、分类、排序一刷就是一屏。
如果每次都打库,高峰时数据库连接和慢查询会先告警。
本文只拆 DSMall 商品列表这一条链路,看缓存具体卡在哪一步。
1. 场景:同一筛选会被问很多遍
列表请求通常带着一串条件,例如:
platform(mall 商城 / food 外卖 / house 上门家政 / kms)store_id、分类、排序字段limit条数
同一运营位、同一店铺首页,短时间内条件几乎不变——典型读多写少。
2. 第一步:用「整包参数」生成 Key
不是手写 goods_list_mall_store_12_...(易漏参数),而是:
$cacheKey = sprintf(
CacheKeyManager::GOODS_LIST_KEY, // goods_list_%s
md5(serialize($data))
);
$result = KvManager::cache()->get($cacheKey);
要点:
- 模板前缀固定在
CacheKeyManager,方便全局搜索 serialize($data)把当次查询条件整体纳入指纹- 换排序、换分类 = 换 Key,不会串数据
命中则直接返回,后面的 DAO 不再跑。
3. 第二步:未命中才查库,且条件收紧
只有 $result == null 时才组装查询,例如坚持:
- 已上架
- 系统审核通过
- 未软删
- 库存
> 0
再叠加请求里的 platform / store / 分类等。
排序也白名单化(销量、评分、时间、点击、最低价等),避免任意字段注入排序。
也就是说:缓存里存的是「已经适合给 C 端看的结果」,不是随便一张宽表 dump。
4. 第三步:回写时带 TTL 和 Tag
KvManager::cache()->set(
$cacheKey,
$result,
3600, // 1 小时
CacheKeyManager::GOODS_TAG // tag = goods
);
两层含义:
- TTL:即使忘记手动失效,最迟一小时也会过期重建
- Tag:后台改商品分类、运营调整商品域时,可
clear(GOODS_TAG)整组丢掉相关列表/分页缓存,而不用知道有哪些 md5 Key
分页接口同理,用 GOODS_PAGES_KEY + 同一 GOODS_TAG,和列表共用失效域。
5. 性能上到底赚在哪
| 无缓存 | 有列表缓存 |
|---|---|
| 每次筛选打库 + 关联查询 | 热点条件几乎只读缓存 |
| 连接数随 QPS 涨 | 回源次数被 TTL / 失效节奏压住 |
| 排序、分类重复计算 | 同指纹条件复用结果集 |
这不是「缓存万能」,而是:把最热、可变维度可枚举的读请求,从 DB 挪到 KV。
店铺列表、技师列表也是同一套路:STORE_LIST_KEY / TECHNICIAN_LIST_KEY + md5(serialize($data)) + 3600 + 对应 Tag。学会商品列表,其它列表几乎同构。
6. 使用时要注意的三点
条件必须进入 Key
漏掉platform会导致商城列表串到外卖数据。用整包serialize就是为了降低漏拼概率。写商品后要能失效
只 set 不 clear,用户会在最长 TTL 内看到旧列表。分类、积分商品等后台写操作应对应clear(*_TAG)。开关
CACHE_ENABLED=false时 get/set 相当于旁路,便于本地调试;压测和生产再打开。
收束
商品列表抗刷,靠的不是单行「加 Redis」,而是:
条件指纹 Key → 收紧条件查库 → TTL + 业务 Tag 回写。
把这一条链路跑顺,本地生活 C 端(商城、外卖、上门家政)大半读流量就有了抓手。