缓存存在虚拟机栈中,做缓存预热如何保证服务平滑(缓存预热和缓存读写同时进行)

51 阅读3分钟

虚拟机栈(线程栈)中做缓存的场景通常指的是线程本地缓存(ThreadLocal 缓存)或堆外缓存(如 Java 的 DirectByteBuffer)。当进行缓存预热时,如果不加控制,可能会导致缓存读写的竞争,影响服务的平滑性可用性

主要问题

  1. 缓存预热与正常请求竞争,导致缓存命中率降低,影响服务响应时间。

  2. 缓存更新时,可能导致缓存不一致(部分线程读取旧数据,部分线程读取新数据)。

  3. 如果缓存直接失效并重新加载,可能会瞬间增加数据库或下游服务的压力(缓存击穿)

解决方案

为了保证缓存预热和缓存读写同时进行,可以采用以下方案:

方案 1:双缓存(读写分离)

核心思路:维护两个副本缓存(读缓存 + 预热缓存),在预热时不会影响原来的缓存访问。

实现步骤

  1. 主缓存 A(正在提供服务)

  2. 备份缓存 B(用于缓存预热) ,定期进行数据预热。

  3. 预热完成后,原子性地切换缓存指针,保证用户请求不会受影响。

实现方式

// 维护两个缓存副本
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:分段预热

核心思路逐步更新缓存,而不是一次性全部替换,减少缓存抖动。

实现方式

  1. 将缓存分成多个小分片(如基于 key 的 Hash 值)。

  2. 逐个更新缓存分片,而不是一次性全部替换。

  3. 通过异步线程更新缓存,不影响主线程的查询。

实现方式

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)

核心思路

  1. 使用Copy-On-Write(写时复制)思想,避免对现有缓存直接修改。

  2. 预热完成后,直接用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 预热(分布式缓存方案)

核心思路

  1. 预加载缓存到 Redis(使用 Lua 脚本保证原子性)。

  2. 让应用端无缝切换到新的 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% 用户访问新缓存)。

数据库压力控制,预热时避免短时间内大量查询数据库