回答:不是每次请求都计算,但缓存过期时会有性能瓶颈
补充一下关于协同过滤实际计算时机的深入发现:
实际执行流程
用户请求
↓
APCu L1 缓存? → 命中直接返回(无计算)
↓ 未命中
Redis L2 缓存? → 命中直接返回(无计算)
↓ 未命中 ❗
投递 cache_fill_queue
↓
同步轮询等待(最多 2.5秒)
↓
后台 AsyncSQLiteWriter 执行
↓
RecommendationEngine::buildHybridScores()
↓
结果写回 Redis(TTL: 15分钟)
关键代码验证
RecommendationService 设置 15 分钟缓存:
private const CACHE_TTL = 900; // 15 minutes
DataService 的同步等待逻辑:
while ($elapsedTime < $waitTimeoutMs) { // 最多 2500ms
usleep(50 * 1000); // 睡 50ms 再查
$responseHash = $this->redis->hgetall($responseKey);
if (!empty($responseHash)) {
return $decodedResponse;
}
}
// 超时返回 null
这意味着:缓存过期后的第一个请求会被阻塞最多 2.5 秒。
发现的潜在问题
| 问题 | 影响 |
|---|
| **缓存击穿** | 缓存过期瞬间,多个并发请求可能同时触发重复计算 |
| **同步等待** | 用户感受到明显延迟(2.5s 或超时) |
| **无降级** | 超时后直接返回 null,无兜底内容 |
| **计算资源集中** | 所有计算压力在后台 Worker,无横向扩展 |
优化建议
1. 请求合并(防重复计算)
// 计算前加锁
if ($redis->set("lock:$cacheKey", 1, ['EX' => 60, 'NX' => true])) {
// 只有获得锁的进程触发计算,其他等待
}
2. 异步降级
// 超时不再返回 null,而是返回热门兜底
if ($elapsedTime >= $waitTimeoutMs) {
return getTrendingTopics($limit); // 立即返回
}
3. 主动预热
// 后台定时任务,缓存即将过期时主动更新
if ($redis->ttl($cacheKey) < 300) { // 剩余5分钟
enqueuePrecomputeTask($userId);
}
结论
大部分请求不执行协同过滤计算,但缓存过期后的请求会触发同步等待计算,这是当前架构的潜在性能瓶颈。
对于日活不高的论坛,15分钟缓存 + 后台计算是合理的设计。但如果用户量增长,需要考虑上述优化措施。
补充:协同过滤实际计算在 AsyncSQLiteWriter::handleUserRecommendations() 中,数据范围是最近 500 个话题 + 800 条回复。