Skip to content

怎样提高服务器本地缓存的命中率

多级缓存(Multi-Level Cache)架构

我们将构建一个两级缓存架构:

  • L1 缓存 (一级缓存): 服务器本地缓存。直接在应用服务器的内存中,速度最快(无网络IO),但容量有限,且多台服务器之间不共享。

  • L2 缓存 (二级缓存): 外部集中式缓存 (如 Redis)。速度快(内网IO),容量大,所有服务器实例共享。

  • 数据源: 数据库 (DB)。

我们的目标是让绝大多数的读请求在 L1 缓存中就得到满足。

设计方案

1. 缓存读取策略(Read-Through)

这是整个体系的基础,请求的查询顺序是固定的:

  1. 查询 L1 缓存:应用接收到请求后,首先查询自己的本地缓存。

  2. 命中 (Hit):如果命中,直接返回数据。这是最理想的情况。

  3. 未命中 (Miss):如果 L1 未命中,则继续查询 L2 缓存 (Redis)。

  4. L2 命中:如果 Redis 中有数据,则将数据:

    • 写入 L1 本地缓存(为了下一次请求能命中L1)。

    • 返回给用户。

  5. L2 未命中:如果 Redis 中也没有数据,则查询最终的数据源(数据库)。

  6. DB 命中:从数据库查到数据后,依次:

    • 将数据写入 L2 缓存 (Redis)。

    • 将数据写入 L1 本地缓存。

    • 返回给用户。

  7. DB 未命中:说明数据不存在,为了防止缓存穿透,可以考虑在缓存中存入一个短时效的空对象。

2. 缓存更新与数据一致性策略

这是提高本地缓存命中率并保证数据相对一致的核心。当商品信息发生变更时(例如,后台修改了价格或库存),我们必须有一种机制来通知所有应用服务器实例,“你们的本地缓存该失效了”。

最经典的方案是 发布/订阅(Pub/Sub)模式 结合 被动失效

工作流程如下:

  1. 数据更新:当一个写请求(如更新商品信息)发生时,服务会:

    • 更新数据库。

    • 删除 L2 缓存 (Redis) 中对应的缓存键(Cache-Aside Pattern)。

    • 关键步骤:通过 Redis 的 Pub/Sub 功能,发布一个“数据变更”消息。这个消息可以很简单,比如发布到名为 product:update 的频道,消息内容就是商品ID productId

  2. 消息订阅与本地缓存失效

    • 所有的应用服务器实例在启动时,都会订阅 product:update 这个频道。

    • 当任何一个实例接收到变更消息(如 productId: 123)时,它会立即 只删除自己本地 L1 缓存中对应的键 (product:123)。

为什么这样做?

  • 解耦:更新数据的服务不需要知道有多少台服务器、它们的地址是什么,只需要往消息队列里发个消息就行。

  • 高效:只是删除本地缓存,而不是去重新加载。下次请求这个商品时,会自然地触发上面提到的Read-Through流程,从L2或DB加载最新数据,这个过程也叫“懒加载(Lazy Loading)”,避免了不必要的数据加载。

  • 广播机制:Pub/Sub 模式能确保所有订阅者(即所有服务器实例)都能收到通知,从而保证所有实例的本地缓存都能被及时清理。

3. 提升本地缓存命中率的辅助策略

  1. 为本地缓存设置合理的容量和淘汰策略

    • 本地缓存因为使用JVM内存,容量不能无限大。需要根据业务场景预估热点商品数量,设置一个最大容量。

    • 使用高效的淘汰策略。对于商品这类信息,LFU (Least Frequently Used, 最不经常使用) 策略通常比 LRU (Least Recently Used, 最近最少使用) 更好。因为热销商品会被频繁访问,即使短时间内没有访问,也不应该被轻易淘汰。而LRU可能会淘汰掉一个刚刚还很热但暂时没被访问的商品。

  2. 本地缓存预热(Cache Warming)

    • 在应用启动时,可以提前将已知的、可预见的超热点商品数据加载到本地缓存中。例如,首页推荐的、活动主推的商品。这样服务一上线就能为这些热点商品提供最快的响应。
  3. 为不同层级的缓存设置不同的过期时间(TTL)

    • L1 本地缓存:可以设置一个较短的过期时间,例如 1-5 分钟。这可以作为Pub/Sub通知机制失败时的“兜底”,保证数据最终会过期。同时,短过期时间也能更快地释放内存给新的热点数据。

    • L2 Redis缓存:可以设置一个较长的过期时间,例如 30-60 分钟,或者根据业务容忍度设置更长。

4. 防止缓存雪崩和击穿

在高并发下,这种多级缓存架构也需要考虑雪崩和击穿问题。

  • 缓存击穿 (单个热点Key失效):当一个热点商品在L1和L2中同时失效,大量请求会穿透到数据库。

    • 解决方案:在查询数据库的逻辑前,使用分布式锁(例如基于Redis的SETNX或Redisson)。只有一个线程能获取到锁去查询数据库并回填各级缓存,其他线程则等待或短暂失败后重试。
  • 缓存雪崩 (大量Key同时失效):

    • 解决方案:在设置L2缓存的过期时间时,可以增加一个随机值(例如TTL = base_ttl + random(300)),避免大量Key在同一时刻集中失效。