You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SpringBoot缓存重载接口并发调用致数据重复的解决方案咨询

问题的核心是同一table+date组合的缓存更新操作没有并发控制——短时间内两次调用时,第一个请求刚删除缓存,还没完成数据加载,第二个请求就再次删除缓存并启动加载,最终两次加载的数据都写入缓存,导致数据翻倍。

下面是几种高效的解决思路:

方案一:基于锁的互斥控制

单实例场景:本地锁

用ConcurrentHashMap维护针对table+date的专属锁,确保同一资源同一时间只有一个线程执行更新:

private final ConcurrentHashMap<String, Object> lockMap = new ConcurrentHashMap<>();

public boolean reloadCache(String table, String date){
    // 生成唯一锁键
    String lockKey = table + ":" + date;
    // 原子性获取或创建锁对象
    Object lock = lockMap.computeIfAbsent(lockKey, k -> new Object());
    
    synchronized (lock) {
        try {
            LocalDate localDate = Util.methodToConvertStringToLocalDate(date);
            deleteCache(table, date);
            loadCache(table, date);
            return true;
        } finally {
            // 执行完毕移除锁对象,避免内存泄漏
            lockMap.remove(lockKey);
        }
    }
}

优化提示:如果是高频热点table+date,可以保留锁对象不删除,用定时任务清理过期锁,减少对象创建开销。

集群场景:分布式锁

多实例部署时本地锁无效,推荐用Redis分布式锁(以Redisson为例,自带过期续期避免锁超时):

@Autowired
private RedissonClient redissonClient;

public boolean reloadCache(String table, String date){
    String lockKey = "cache:reload:" + table + ":" + date;
    RLock lock = redissonClient.getLock(lockKey);
    
    try {
        // 尝试加锁,等待5秒,锁有效期30秒(根据实际加载时长调整)
        if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
            LocalDate localDate = Util.methodToConvertStringToLocalDate(date);
            deleteCache(table, date);
            loadCache(table, date);
            return true;
        } else {
            // 加锁失败,说明已有线程在执行更新,直接返回成功(或根据业务返回"处理中")
            return true;
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        return false;
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

方案二:请求合并与防抖

用缓存做防抖,短时间内重复请求合并为一次执行(以Guava Cache为例):

@Autowired
private ReloadCacheService self;

// 防抖缓存:100ms内同一table+date的请求复用同一次执行结果
private final LoadingCache<String, Boolean> reloadDebounceCache = CacheBuilder.newBuilder()
        .expireAfterWrite(100, TimeUnit.MILLISECONDS)
        .build(new CacheLoader<String, Boolean>() {
            @Override
            public Boolean load(String key) throws Exception {
                String[] parts = key.split(":", 2);
                return self.doReloadCache(parts[0], parts[1]);
            }
        });

// 对外接口方法改为调用防抖缓存
public boolean reloadCache(String table, String date){
    try {
        String key = table + ":" + date;
        return reloadDebounceCache.get(key);
    } catch (ExecutionException e) {
        throw new RuntimeException("Reload cache failed", e.getCause());
    }
}

// 实际执行更新的内部方法
public boolean doReloadCache(String table, String date){
    LocalDate localDate = Util.methodToConvertStringToLocalDate(date);
    deleteCache(table, date);
    loadCache(table, date);
    return true;
}

方案三:异步任务队列+状态标记

用线程池异步处理更新,同时维护正在执行的任务列表,重复请求直接返回:

private final ThreadPoolTaskExecutor executor;
private final ConcurrentHashMap<String, Future<?>> runningTasks = new ConcurrentHashMap<>();

// 构造函数注入Spring线程池(推荐用Spring托管的线程池,便于资源管控)
public ReloadCacheService(ThreadPoolTaskExecutor executor) {
    this.executor = executor;
}

public boolean reloadCache(String table, String date){
    String key = table + ":" + date;
    Future<?> existingTask = runningTasks.get(key);
    
    if (existingTask != null && !existingTask.isDone()) {
        // 已有任务在执行,直接返回成功
        return true;
    }
    
    Future<?> future = executor.submit(() -> {
        try {
            LocalDate localDate = Util.methodToConvertStringToLocalDate(date);
            deleteCache(table, date);
            loadCache(table, date);
        } finally {
            runningTasks.remove(key);
        }
    });
    
    runningTasks.put(key, future);
    return true;
}

内容的提问来源于stack exchange,提问作者U B

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 21:04:58