缓存击穿
在使用缓存时,我们往往是先根据key从缓存中取数据,如果拿不到就去数据源加载数据,写入缓存。但是在某些高并发的情况下,可能会出现缓存击穿的问题,比如一个存在的key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到DB,造成瞬时DB请求量大、压力骤增。
一般解决方案
首先我们想到的解决方案就是加锁,一种办法是:拿到锁的请求,去加载数据,没有拿到锁的请求,就先等待。这种方法虽然避免了并发加载数据,但实际上是将并发的操作串行化,会增加系统延时。
singleflight
singleflight是groupcache这个项目的一部分,groupcache是memcache作者使用golang编写的分布式缓存。singleflight能够使多个并发请求的回源操作中,只有第一个请求会进行回源操作,其他的请求会阻塞等待第一个请求完成操作,直接取其结果,这样可以保证同一时刻只有一个请求在进行回源操作,从而达到防止缓存击穿的效果。下面是参考groupcache源码,使用Java实现的singleflight代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch;
public class SingleFlight {
private final ConcurrentMap<Object, Call> calls = new ConcurrentHashMap<>();
@SuppressWarnings("unchecked") public <V> V execute(Object key, Callable<V> callable) throws Exception { Call<V> call = calls.get(key); if (call == null) { call = new Call<>(); Call<V> other = calls.putIfAbsent(key, call); if (other == null) { try { return call.exec(callable); } finally { calls.remove(key); } } else { call = other; } } return call.await(); }
private static class Call<V> {
private V result; private Exception exc; private final CountDownLatch cdl = new CountDownLatch(1);
void finished(V result, Exception exc) { this.result = result; this.exc = exc; cdl.countDown(); }
V await() throws Exception { cdl.await(); if (exc != null) { throw exc; } return result; }
V exec(Callable<V> callable) throws Exception { V result = null; Exception exc = null; try { result = callable.call(); return result; } catch (Exception e) { exc = e; throw e; } finally { finished(result, exc); } } }
}
|
我们使用CountDownLatch来实现多个线程等待一个线程完成操作,CountDownLatch包含一个计数器,初始化时赋值,countDown()可使计数器减一,当count为0时唤醒所有等待的线程,await()可使线程阻塞。我们同样用CountDownLatch来模拟一个10次并发,测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public static void main(String[] args) { int count = 10; CountDownLatch cld = new CountDownLatch(count); SingleFlight sf = new SingleFlight(); for (int i = 0; i < count; i++) { new Thread(() -> { try { cld.await(); } catch (InterruptedException e) { e.printStackTrace(); } try { String execute = sf.execute("key", () -> { System.out.println("func"); return "bar"; }); System.out.println(execute); } catch (Exception e) { e.printStackTrace(); }
}).start(); cld.countDown(); } }
|
测试结果如下:
1 2 3 4 5 6 7 8 9 10 11
| func bar bar bar bar bar bar bar bar bar bar
|
可以看到回源操作只被执行了一次,其他9次直接取到了第一次操作的结果。
总结
可以看到singleflight可以有效解决高并发情况下的缓存击穿问题,singleflight这种控制机制不仅可以用在缓存击穿的问题上,理论上可以解决各种分层结构的高并发性能问题。