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
相关产品推荐
相关产品推荐

