快取擊穿
在使用快取時,我們往往是先根據key從快取中取資料,如果拿不到就去資料來源載入資料,寫入快取。但是在某些高併發的情況下,可能會出現快取擊穿的問題,比如一個存在的key,在快取過期的一刻,同時有大量的請求,這些請求都會擊穿到DB,造成瞬時DB請求量大、壓力驟增。
一般解決方案
首先我們想到的解決方案就是加鎖,一種辦法是:拿到鎖的請求,去載入資料,沒有拿到鎖的請求,就先等待。這種方法雖然避免了併發載入資料,但實際上是將併發的操作序列化,會增加系統延時。
singleflight
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這種控制機制不僅可以用在快取擊穿的問題上,理論上可以解決各種分層結構的高併發效能問題。