使用singleflight防止緩存擊穿(Java)

緩存擊穿

在使用緩存時,咱們每每是先根據key從緩存中取數據,若是拿不到就去數據源加載數據,寫入緩存。可是在某些高併發的狀況下,可能會出現緩存擊穿的問題,好比一個存在的key,在緩存過時的一刻,同時有大量的請求,這些請求都會擊穿到DB,形成瞬時DB請求量大、壓力驟增。java

通常解決方案

首先咱們想到的解決方案就是加鎖,一種辦法是:拿到鎖的請求,去加載數據,沒有拿到鎖的請求,就先等待。這種方法雖然避免了併發加載數據,但其實是將併發的操做串行化,會增長系統延時。golang

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

相關文章
相關標籤/搜索