跳到正文
Konlue的博客
返回

列表接口 50ms → 12ms:一次完整的查询优化复盘

场景

dam-hub 的列表页是整个系统用得最重的接口:按空间、素材类型、标签、时间范围筛选,再分页展示。压测口径下,优化前这个接口 P95 大约 50ms。50ms 不算慢,但它是首页级接口,放大到缓存穿透、深分页和并发场景,就会成为最先倒下的一环。这篇复盘三刀下去的过程:索引、分页方式、缓存,压测结果 50ms → 12ms。

先说方法论。三刀的顺序不是随便排的:先让查询本身走对路(索引),再让走的路变短(分页),最后让重复的路不用重走(缓存)。顺序反了,等于给一个走错路的查询套上昂贵的缓存。

第一刀:联合索引

列表查询的 WHERE 条件组合很多,不可能每种组合都建索引。做法是找出高频的筛选列,按“等值条件在前、范围条件在后、排序列收尾”的顺序建一个联合索引:

ALTER TABLE asset
  ADD INDEX idx_space_type_time (space_id, asset_type, created_at);

space_id 和 asset_type 是等值匹配,放在最前面吃满最左前缀;created_at 既是筛选条件也是排序字段,放最后可以在索引内直接完成排序,避免 filesort。

建完 explain 一看,大部分查询从全表扫描变成了 range 扫描,这一刀拿走了最大头的收益。但也立刻遇到一个尴尬:有一个索引建了却没用上。排查发现查询里对时间列包了一层函数(DATE(created_at) = ...),函数包裹让 MySQL 放弃走索引。改成范围条件(created_at >= ? AND created_at < ? + 1 day)后索引恢复工作。教训:索引失效不一定是索引设计错,先检查查询写法有没有破坏最左前缀或对列做运算。

第二刀:深分页改游标

索引解决“找得快”,但 LIMIT 100000, 20 这种深分页还是慢——MySQL 要先扫过前 100000 行再丢掉。素材列表恰好有这种访问模式:用户翻页翻得很深,或者爬虫按页遍历。

改法是游标分页(seek method):不用“第几页”定位,用“上一页最后一条的 id”定位:

-- 之前:OFFSET 越深越慢
SELECT * FROM asset WHERE space_id = ? ORDER BY id DESC LIMIT 100000, 20;

-- 之后:从游标继续,永远只扫 20 行
SELECT * FROM asset
WHERE space_id = ? AND id < ?   -- 上一页最后一条的 id
ORDER BY id DESC LIMIT 20;

代价要说清楚:页码跳转没有了,用户只能“下一页”。对素材库这种信息流式浏览的场景,这个代价可以接受;如果产品需要跳页,可以用混合方案——常规页码走缓存,深分页自动切换到游标。dam-hub 的前端交互本来就不是翻页式的,所以直接切换没有阻力。

第三刀:两级缓存

前两刀之后单次查询已经不慢,但热点读还是反复打 DB。第三刀的目标是让热点读全链路不落库。

缓存结构是本地缓存(Caffeine)+ Redis 两级:本地缓存扛最热的元数据(空间信息、高频筛选的计数),毫秒级返回且不吃网络往返;Redis 存查询结果集——具体说是“查询条件组合 → 素材 id 列表”的映射,命中后按 id 批量回表取详情,避免把整行数据重复存两份。

这里有个容易做错的地方:缓存 key 的设计。把所有筛选参数原样拼进 key,会导致组合爆炸和低命中率。我们先把参数规范化(默认值归一、排序固定),只缓存出现过的高频条件组合,长尾组合直接走 DB——不是所有查询都值得缓存。

TTL 加随机值:防雪崩

同一批 key 如果设置了相同 TTL,会在同一秒集体过期,下一秒所有请求砸向 DB,这就是缓存雪崩。解法简单到不像话:TTL 加随机偏移。

long ttl = BASE_TTL + ThreadLocalRandom.current().nextLong(0, JITTER);

基础 TTL 之上叠一个随机抖动,key 的过期时间被摊开,DB 的压力曲线从尖峰变成平坡。

抖动带的取值要和业务的访问节奏匹配:太小摊不开,太大让相邻批次的缓存有效期差异过大,命中率反而下降。我们取基础 TTL 的 10% 左右作为抖动带,没有精确推导,压测里这个量级表现稳定。

效果

压测口径下,列表接口 P95 从约 50ms 降到 12ms。拆开看三刀各自的贡献:索引和分页方式解决单次查询的成本,是绝对大头;两级缓存吃掉重复访问,把热点读从 DB 挪进内存;深分页改游标后,最深的翻页路径成本和第一页一样。

回头看,这次优化里最值钱的不是某个具体技巧,而是顺序:先确认查询走对路,再缩短路径,最后才谈缓存。缓存是最容易被滥用的优化——它把索引没建好的问题藏起来,等 key 失效那天一起爆发。



上一篇
跨系统回调的安全设计:鉴权链、防重放与幂等
下一篇
逾期预警与工单:给长流程加上时间维度