在使用緩存時,咱們每每是先根據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這種控制機制不只能夠用在緩存擊穿的問題上,理論上能夠解決各類分層結構的高併發性能問題。性能