餐饮平台缓存优化,SQL从502次降至5次的实战

本文通过一个餐饮外卖平台的案例,详细记录了如何通过引入Redis缓存解决N+1问题,将数据库查询次数从502次降低至个位数。文章分析了问题根源、优化方案,并探讨了防雪崩和防穿透的实现细节,为类似系统提...

互联网/IT

在开发一个餐饮外卖平台时,我们遇到了一个典型的性能瓶颈问题。用户端有一个「按分类查菜品」的接口,每次调用都会返回该分类下的菜品列表及口味信息。起初未做任何缓存优化,导致每次请求都需要直接查询数据库。

通过本地JMeter压测(模拟100并发,每个线程循环100次),我们发现虽然平均响应时间只有12ms,但P99达到了33ms。进一步查看MyBatis SQL日志后,惊讶地发现100个请求竟然执行了502次SQL语句。经过拆解分析,问题出在典型的N+1查询模式上:

  • 每个请求先执行一次主查询获取菜品列表(100次)
  • 然后对每个菜品单独执行一次子查询获取口味信息(100 × 4 = 400次)

这种设计在数据量较小时可能表现尚可,但随着用户增长和数据量扩大,性能会迅速恶化。考虑到菜品数据读多写少的特点,最直接的解决方案是引入缓存机制。

第一版:基础Redis缓存实现

这个版本确实显著降低了数据库查询次数,SQL从502次降到了个位数。然而,经过深入思考,我们意识到这个方案存在两个潜在风险:缓存永久有效可能导致脏数据累积,以及大量缓存同时失效引发的缓存雪崩问题。

文章配图

第二版:增强型缓存策略

为了解决上述问题,我们在第一版基础上进行了改进,主要增加了两个关键特性:TTL随机化和空值缓存。

TTL随机化防雪崩

第一个改进是在设置缓存过期时间时加入随机因子。我们将基础过期时间设为30分钟,并允许在25-35分钟之间随机波动,这样可以避免大量缓存同时失效:

第二个改进是处理不存在的分类ID查询。如果直接返回空结果而不写入缓存,恶意攻击者可能会利用这一点发起缓存穿透攻击。我们通过给空结果添加特殊标记来解决这个问题:

在查询时,我们通过判断这个特殊标记来区分缓存命中和实际空结果:

完成这些改进后,我们再次使用JMeter进行压测。结果显示,SQL查询次数稳定在个位数,平均响应时间保持在1ms左右,P99也控制在2ms以内。通过对比优化前后的性能指标,我们可以看到显著的提升效果:

此外,通过监控MySQL的慢查询日志,我们发现原本频繁出现的N+1查询模式已经完全消失。整个系统的数据库负载显著下降,服务器CPU利用率从原来的30%降低到10%以下。

行业启示与最佳实践

这次缓存优化不仅解决了当前的性能问题,也为类似系统提供了重要的设计参考。以下是几个值得借鉴的最佳实践:

  • 对于读多写少的数据,优先考虑缓存方案而非复杂的SQL优化
  • 缓存粒度应根据业务特点合理设计,既不能过于粗放也不能过于细碎
  • 必须为缓存设置合理的过期时间,并采用随机化策略防止雪崩
  • 针对可能的异常情况(如不存在的数据),需要设计专门的防御机制
  • 在高并发场景下,缓存穿透是一个不可忽视的风险点
  • 通过这次优化,我们深刻认识到,在系统设计初期就考虑缓存策略的重要性。对于类似的电商平台或内容管理系统,都可以参考这种基于分类维度的整体缓存方案,从而显著提升系统性能和稳定性。