目录
什么是缓存击穿
指一个Key非常热,在不停的扛着并发,并发集中对这一个点进行访问,当这个Key在Redis失效的瞬间,持续的并发请求就穿破缓存去数据库查找,对数据库造成极大压力。
Go singleflight防止缓存击穿
Go singleflight常用来防止缓存击穿,简单来说就是一个key只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存中获取数据即可。
Java代码简单实现
有以下需要注意的功能点
- 同一个key多个线程只允许一个线程更新,其他线程阻塞等待
- 同一个key,多个线程共享一个排他的写锁,缓存就绪后允许多线程并发读
- 当一个线程更新完Redis,其他线程能并行读取Redis里面的缓存,提高吞吐量
- 本地缓存一段时间key的访问与更新记录,定期清理
下面看代码。核心使用了Semaphore来限流,通过ConcurrentHashMap为key绑定一个Semaphore令牌。
Semaphore初始阶段令牌数为1,多线程请求同一个key,只有一个线程能拿到令牌,其他线程阻塞。
通过AtomicLong记录线程总数,后续用来申请令牌数,实现并发读取。
当单线程更新好缓存后,通过Semaphore.release(线程访问总数-1)申请令牌,其他阻塞的线程拿到足够令牌并发读取Redis。
定期清理ConcurrentHashMap避免内存吃紧。
public class TokenEntity {
//令牌,一开始只允许一个线程拿到令牌,实现多线程更新串行化
public Semaphore semaphore = new Semaphore(1);
//记录线程访问次数
public AtomicLong count = new AtomicLong(0);
//记录当前时间,方面后续缓存清理
public Long time = System.currentTimeMillis();
}
public class SingleFlight {
// 定期清理访问记录
private static ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(
1);
//模拟远程cache
private static final Map<Integer, String> userCache = new ConcurrentHashMap<>();
//请求访问记录
private static Map<Integer, Long> requestRecordMap = new ConcurrentHashMap<>();
//key对应的令牌,初始只允许一个线程拿到令牌
private static Map<Integer, TokenEntity> semaphoreMap = new ConcurrentHashMap<>();
//线程池
private static final ExecutorService executorService = Executors.newFixedThreadPool(20);
public static String getValue(int id) {
//读取缓存
String result = userCache.get(id);
if (result == null) {
//访问记录不为null表示已经有其他线程更新过了,直接从缓存读取
if (requestRecordMap.get(id) != null) {
return userCache.get(id);
} else {
//只允许一个线程访问,为当前请求id绑定一个令牌
TokenEntity tokenEntity = semaphoreMap.computeIfAbsent(id, k -> new TokenEntity());
//同一个id记录访问数量,方便后续增大令牌
tokenEntity.count.incrementAndGet();
//获取一个令牌
try {
tokenEntity.semaphore.acquire();
//拿到令牌后,再次检查缓存是否被更新过,被更新过直接读取缓存
if (requestRecordMap.get(id) != null) {
//释放自己的令牌
tokenEntity.semaphore.release();
//System.out.println("直接缓存读取 = " + Thread.currentThread().getName());
return userCache.get(id);
}
result = Thread.currentThread().getName();
//缓存更新一次
System.out.println("只有一个线程更新缓存");
userCache.put(id, result);
//记录访问
requestRecordMap.put(id, System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放并增大令牌,允许剩下的线程并发拿到令牌
tokenEntity.semaphore.release(tokenEntity.count.intValue() - 1);
}
}
} else {
System.out.println("直接缓存读取 2");
}
return result;
}
public static void main(String[] args) throws Exception {
for (int i = 0; i < 20; i++) {
final int j = i;
executorService.submit(() -> {
String result = getValue(9);
//System.out.println(result + " " + j);
});
}
for (int i = 0; i < 20; i++) {
final int j = i;
executorService.submit(() -> {
String result = getValue(9);
// System.out.println(result + " " + j);
});
}
executorService.shutdown();
//清理本地缓存,本地缓存清理周期要小于远程缓存过期时间
scheduledExecutorService.scheduleAtFixedRate(() -> {
System.out.println("定时清理");
clearRequestRecordMap();
}, 0, 10, TimeUnit.SECONDS);
}
private static void clearRequestRecordMap() {
Iterator<Entry<Integer, Long>>
iterator = requestRecordMap.entrySet().iterator();
long current = System.currentTimeMillis();
// Iterate over the HashMap
while (iterator.hasNext()) {
Map.Entry<Integer, Long> entry = iterator.next();
// 大于十秒删除缓存
if (current - entry.getValue() > 10000) {
iterator.remove();
semaphoreMap.remove(entry.getKey());
System.out.println("清理缓存");
}
}
}
}