本地缓存限流
这个项目做了什么
这个项目用JUC工具类做了一个本地缓存防击穿演示。
前端可以一次性发起很多请求,后端统计这些请求里有多少命中缓存、多少真正回源、多少被限流拦截。最后通过图表把结果展示出来。
核心目的不是做一个完整缓存框架,而是把ConcurrentHashMap、Semaphore、AtomicInteger这些JUC工具放到一个具体场景里理解。
核心流程
text
请求进入/api/cache/get/{key}
->先查ConcurrentHashMap本地缓存
->命中缓存就直接返回
->没命中就尝试获取Semaphore许可
->抢到许可的线程执行慢查询
->查询结果写入本地缓存
->没抢到许可的请求直接返回系统繁忙
->前端请求/api/cache/stats展示统计数据我觉得难的地方
难点是理解为什么不能让所有线程都去查数据库。
如果热点Key刚好不存在,很多请求会同时进来。没有保护的话,这些请求会全部绕过缓存去查数据库,形成缓存击穿。
这个项目用Semaphore控制同一时间只有一个线程能回源,其他线程快速返回系统繁忙。这样虽然有些请求被拦截了,但数据库不会被瞬间打爆。
用到的技术
ConcurrentHashMap:保存JVM本地缓存Semaphore:控制回源线程数量AtomicInteger:统计命中、回源、拦截次数Callable:把真实查询逻辑传入缓存组件- Vue3+ECharts:展示并发请求结果
关键实现
Controller只负责接收请求和返回统计数据:
java
@GetMapping("/get/{key}")
public Map<String, Object> getData(@PathVariable("key") String key) {
long start = System.currentTimeMillis();
String result = cacheManage.get(key, () -> {
TimeUnit.SECONDS.sleep(2);
return "Real_Database" + key;
});
long duration = System.currentTimeMillis() - start;
Map<String, Object> response = new HashMap<>();
response.put("result", result);
response.put("duration", duration);
response.put("timestamp", System.currentTimeMillis());
return response;
}缓存防击穿核心:
java
String value = cache.get(key);
if (value != null) {
hitCount.incrementAndGet();
return value;
}
if (backOriginSemaphore.tryAcquire()) {
try {
value = cache.get(key);
if (value != null) {
hitCount.incrementAndGet();
return value;
}
missCount.incrementAndGet();
value = loader.call();
if (value != null) {
cache.put(key, value);
}
return value;
} finally {
backOriginSemaphore.release();
}
}
blockedCount.incrementAndGet();
return "系统繁忙";统计指标
text
hitCount:命中本地缓存的次数
missCount:真正回源查询的次数
blockedCount:被Semaphore拦截的次数
hitRate:命中率测试效果:

当前版本的限制
- 缓存没有过期时间,数据会一直留在JVM内存里
- 现在所有Key共用一个
Semaphore,不同Key之间会互相影响 - 被拦截的请求直接返回
系统繁忙,没有等待重试 - 本地缓存只适合单机演示,多实例部署时缓存数据不共享
可以继续优化
- 给缓存加过期时间
- 不同Key使用不同的
Semaphore - 被拦截请求可以短暂等待,再尝试读取缓存
- 增加最大缓存数量,避免本地内存无限增长