在使用缓存时,咱们每每是先根据key从缓存中取数据,若是拿不到就去数据源加载数据,写入缓存。可是在某些高并发的状况下,可能会出现缓存击穿的问题,好比一个存在的key,在缓存过时的一刻,同时有大量的请求,这些请求都会击穿到DB,形成瞬时DB请求量大、压力骤增。java
首先咱们想到的解决方案就是加锁,一种办法是:拿到锁的请求,去加载数据,没有拿到锁的请求,就先等待。这种方法虽然避免了并发加载数据,但其实是将并发的操做串行化,会增长系统延时。golang
singleflight是groupcache这个项目的一部分,groupcache是memcache做者使用golang编写的分布式缓存。singleflight可以使多个并发请求的回源操做中,只有第一个请求会进行回源操做,其余的请求会阻塞等待第一个请求完成操做,直接取其结果,这样能够保证同一时刻只有一个请求在进行回源操做,从而达到防止缓存击穿的效果。下面是参考groupcache源码,使用Java实现的singleflight代码:缓存
//表明正在进行中,或已经结束的请求 public class Call { private byte[] val; private CountDownLatch cld; public byte[] getVal() { return val; } public void setVal(byte[] val) { this.val = val; } public void await() { try { this.cld.await(); } catch (InterruptedException e) { e.printStackTrace(); } } public void lock() { this.cld = new CountDownLatch(1); } public void done() { this.cld.countDown(); } }
//singleflight 的主类,管理不一样 key 的请求(call) public class CallManage { private final Lock lock = new ReentrantLock(); private Map<String, Call> callMap; public byte[] run(String key, Supplier<byte[]> func) { this.lock.lock(); if (this.callMap == null) { this.callMap = new HashMap<>(); } Call call = this.callMap.get(key); if (call != null) { this.lock.unlock(); call.await(); return call.getVal(); } call = new Call(); call.lock(); this.callMap.put(key, call); this.lock.unlock(); call.setVal(func.get()); call.done(); this.lock.lock(); this.callMap.remove(key); this.lock.unlock(); return call.getVal(); } }
咱们使用CountDownLatch来实现多个线程等待一个线程完成操做,CountDownLatch包含一个计数器,初始化时赋值,countDown()可以使计数器减一,当count为0时唤醒全部等待的线程,await()可以使线程阻塞。咱们一样用CountDownLatch来模拟一个10次并发,测试代码以下:并发
public static void main(String[] args) { CallManage callManage = new CallManage(); int count = 10; CountDownLatch cld = new CountDownLatch(count); for (int i = 0; i < count; i++) { new Thread(() -> { try { cld.await(); } catch (InterruptedException e) { e.printStackTrace(); } byte[] value = callManage.run("key", () -> { System.out.println("func"); return ByteArrayUtil.oToB("bar"); }); System.out.println(ByteArrayUtil.bToO(value).toString()); }).start(); cld.countDown(); } }
测试结果以下:分布式
func bar bar bar bar bar bar bar bar bar bar
能够看到回源操做只被执行了一次,其余9次直接取到了第一次操做的结果。高并发
能够看到singleflight能够有效解决高并发状况下的缓存击穿问题,singleflight这种控制机制不只能够用在缓存击穿的问题上,理论上能够解决各类分层结构的高并发性能问题。性能