在虚拟机栈(线程栈)中做缓存的场景通常指的是线程本地缓存(ThreadLocal 缓存)或堆外缓存(如 Java 的 DirectByteBuffer)。当进行缓存预热时,如果不加控制,可能会导致缓存读写的竞争,影响服务的平滑性和可用性。
主要问题
-
缓存预热与正常请求竞争,导致缓存命中率降低,影响服务响应时间。
-
缓存更新时,可能导致缓存不一致(部分线程读取旧数据,部分线程读取新数据)。
-
如果缓存直接失效并重新加载,可能会瞬间增加数据库或下游服务的压力(缓存击穿) 。
解决方案
为了保证缓存预热和缓存读写同时进行,可以采用以下方案:
方案 1:双缓存(读写分离)
核心思路:维护两个副本缓存(读缓存 + 预热缓存),在预热时不会影响原来的缓存访问。
实现步骤:
-
主缓存 A(正在提供服务) 。
-
备份缓存 B(用于缓存预热) ,定期进行数据预热。
-
预热完成后,原子性地切换缓存指针,保证用户请求不会受影响。
实现方式
// 维护两个缓存副本
volatile Map<String, Object> activeCache = new ConcurrentHashMap<>();
volatile Map<String, Object> backupCache = new ConcurrentHashMap<>();
// 读取缓存
public Object getFromCache(String key) {
return activeCache.get(key); // 读取当前生效的缓存
}
// 预热缓存
public void preheatCache() {
// 加载新数据到 backupCache
Map<String, Object> newCache = new ConcurrentHashMap<>();
loadData(newCache);
// 预热完成后,原子切换缓存
backupCache = newCache;
activeCache = backupCache;
}
// 加载数据的方法
private void loadData(Map<String, Object> cache) {
cache.put("key1", "value1");
cache.put("key2", "value2");
}
✅ 优点:
• 不会影响当前正在使用的缓存,服务可以平滑运行。
• 原子切换缓存,避免数据不一致问题。
❌ 缺点:
• 需要额外的内存来存储两个缓存实例。
方案 2:分段预热
核心思路:逐步更新缓存,而不是一次性全部替换,减少缓存抖动。
实现方式:
-
将缓存分成多个小分片(如基于 key 的 Hash 值)。
-
逐个更新缓存分片,而不是一次性全部替换。
-
通过异步线程更新缓存,不影响主线程的查询。
实现方式
public void incrementalCacheUpdate() {
ExecutorService executor = Executors.newFixedThreadPool(10);
for (String key : cache.keySet()) {
executor.submit(() -> {
Object newValue = fetchFromDatabase(key);
cache.put(key, newValue);
});
}
executor.shutdown();
}
✅ 优点:
• 逐步替换,不会出现瞬间的缓存击穿。
• 对已有数据影响小,读请求仍然能获取旧数据。
❌ 缺点:
• 预热速度较慢,不适用于大规模缓存更新。
方案 3:异步双缓存(Copy-On-Write)
核心思路:
-
使用Copy-On-Write(写时复制)思想,避免对现有缓存直接修改。
-
预热完成后,直接用CAS(Compare And Swap) 方式更新缓存。
实现方式
volatile Map<String, Object> cache = new ConcurrentHashMap<>();
public void updateCacheAsync() {
new Thread(() -> {
// 复制当前缓存
Map<String, Object> newCache = new ConcurrentHashMap<>(cache);
// 加载新数据
loadData(newCache);
// 使用 CAS 替换缓存
cache = newCache;
}).start();
}
✅ 优点:
• 保证并发安全,不会影响正常请求。
• 避免缓存抖动,数据更新后一次性生效。
❌ 缺点:
• 仍然需要额外的内存来存储新旧缓存。
方案 4:基于 Redis 预热(分布式缓存方案)
核心思路:
-
预加载缓存到 Redis(使用 Lua 脚本保证原子性)。
-
让应用端无缝切换到新的 Redis 缓存。
实现方式
-- Lua 脚本:保证缓存更新的原子性
local key = KEYS[1]
local newValue = ARGV[1]
redis.call("SET", key, newValue)
✅ 优点:
• 分布式适用,支持多个实例同时读写缓存。
• 减少内存占用,只在 Redis 中维护缓存。
❌ 缺点:
• 需要 Redis 支持,适用于分布式架构。
总结
👉 最佳方案:
• 单机缓存:双缓存策略(方案 1)+ 异步 Copy-On-Write(方案 3)。
• 分布式缓存:Redis 预热(方案 4)。
最佳实践
• 缓存预热前,可以短时间降级(如返回静态数据)。
• 缓存替换前,采用灰度发布策略(如 10% 用户访问新缓存)。
• 数据库压力控制,预热时避免短时间内大量查询数据库。