怎样提高服务器本地缓存的命中率
多级缓存(Multi-Level Cache)架构
我们将构建一个两级缓存架构:
-
L1 缓存 (一级缓存): 服务器本地缓存。直接在应用服务器的内存中,速度最快(无网络IO),但容量有限,且多台服务器之间不共享。
-
L2 缓存 (二级缓存): 外部集中式缓存 (如 Redis)。速度快(内网IO),容量大,所有服务器实例共享。
-
数据源: 数据库 (DB)。
我们的目标是让绝大多数的读请求在 L1 缓存中就得到满足。
设计方案
1. 缓存读取策略(Read-Through)
这是整个体系的基础,请求的查询顺序是固定的:
-
查询 L1 缓存:应用接收到请求后,首先查询自己的本地缓存。
-
命中 (Hit):如果命中,直接返回数据。这是最理想的情况。
-
未命中 (Miss):如果 L1 未命中,则继续查询 L2 缓存 (Redis)。
-
L2 命中:如果 Redis 中有数据,则将数据:
-
写入 L1 本地缓存(为了下一次请求能命中L1)。
-
返回给用户。
-
-
L2 未命中:如果 Redis 中也没有数据,则查询最终的数据源(数据库)。
-
DB 命中:从数据库查到数据后,依次:
-
将数据写入 L2 缓存 (Redis)。
-
将数据写入 L1 本地缓存。
-
返回给用户。
-
-
DB 未命中:说明数据不存在,为了防止缓存穿透,可以考虑在缓存中存入一个短时效的空对象。
2. 缓存更新与数据一致性策略
这是提高本地缓存命中率并保证数据相对一致的核心。当商品信息发生变更时(例如,后台修改了价格或库存),我们必须有一种机制来通知所有应用服务器实例,“你们的本地缓存该失效了”。
最经典的方案是 发布/订阅(Pub/Sub)模式 结合 被动失效。
工作流程如下:
-
数据更新:当一个写请求(如更新商品信息)发生时,服务会:
-
更新数据库。
-
删除 L2 缓存 (Redis) 中对应的缓存键(Cache-Aside Pattern)。
-
关键步骤:通过 Redis 的 Pub/Sub 功能,发布一个“数据变更”消息。这个消息可以很简单,比如发布到名为
product:update的频道,消息内容就是商品IDproductId。
-
-
消息订阅与本地缓存失效:
-
所有的应用服务器实例在启动时,都会订阅
product:update这个频道。 -
当任何一个实例接收到变更消息(如
productId: 123)时,它会立即 只删除自己本地 L1 缓存中对应的键 (product:123)。
-
为什么这样做?
-
解耦:更新数据的服务不需要知道有多少台服务器、它们的地址是什么,只需要往消息队列里发个消息就行。
-
高效:只是删除本地缓存,而不是去重新加载。下次请求这个商品时,会自然地触发上面提到的Read-Through流程,从L2或DB加载最新数据,这个过程也叫“懒加载(Lazy Loading)”,避免了不必要的数据加载。
-
广播机制:Pub/Sub 模式能确保所有订阅者(即所有服务器实例)都能收到通知,从而保证所有实例的本地缓存都能被及时清理。
3. 提升本地缓存命中率的辅助策略
-
为本地缓存设置合理的容量和淘汰策略:
-
本地缓存因为使用JVM内存,容量不能无限大。需要根据业务场景预估热点商品数量,设置一个最大容量。
-
使用高效的淘汰策略。对于商品这类信息,LFU (Least Frequently Used, 最不经常使用) 策略通常比 LRU (Least Recently Used, 最近最少使用) 更好。因为热销商品会被频繁访问,即使短时间内没有访问,也不应该被轻易淘汰。而LRU可能会淘汰掉一个刚刚还很热但暂时没被访问的商品。
-
-
本地缓存预热(Cache Warming):
- 在应用启动时,可以提前将已知的、可预见的超热点商品数据加载到本地缓存中。例如,首页推荐的、活动主推的商品。这样服务一上线就能为这些热点商品提供最快的响应。
-
为不同层级的缓存设置不同的过期时间(TTL):
-
L1 本地缓存:可以设置一个较短的过期时间,例如 1-5 分钟。这可以作为Pub/Sub通知机制失败时的“兜底”,保证数据最终会过期。同时,短过期时间也能更快地释放内存给新的热点数据。
-
L2 Redis缓存:可以设置一个较长的过期时间,例如 30-60 分钟,或者根据业务容忍度设置更长。
-
4. 防止缓存雪崩和击穿
在高并发下,这种多级缓存架构也需要考虑雪崩和击穿问题。
-
缓存击穿 (单个热点Key失效):当一个热点商品在L1和L2中同时失效,大量请求会穿透到数据库。
- 解决方案:在查询数据库的逻辑前,使用分布式锁(例如基于Redis的
SETNX或Redisson)。只有一个线程能获取到锁去查询数据库并回填各级缓存,其他线程则等待或短暂失败后重试。
- 解决方案:在查询数据库的逻辑前,使用分布式锁(例如基于Redis的
-
缓存雪崩 (大量Key同时失效):
- 解决方案:在设置L2缓存的过期时间时,可以增加一个随机值(例如
TTL = base_ttl + random(300)),避免大量Key在同一时刻集中失效。
- 解决方案:在设置L2缓存的过期时间时,可以增加一个随机值(例如