使用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这种控制机制不只能够用在缓存击穿的问题上,理论上能够解决各类分层结构的高并发性能问题。性能

相关文章
相关标签/搜索