SingleFlight设计模式-Java实现

2 阅读2分钟

应用场景

缓存热点key失效了,缓存击穿,大量请求打到数据库

核心思想

如果有多个并发请求同时请求同一个资源,那么只需要执行一次实际的请求,然后将结果返回给所有的请求者。

原理图在这里插入图片描述

SingleFlight代码


import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;

/**
 * @author hyy
 * @Description
 * @create 2023-12-23 20:01
 */
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);
        //代码的精髓所在因为concurrentHashMap#get,put的并发安全的才能这么写
        if (Objects.isNull(call)) {
            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();
    }

    public static class Call<V> {
        private V result;
        private Exception exc;
        private final CountDownLatch cdl = new CountDownLatch(1);


        public void finished(V result, Exception exc) {
            this.result = result;
            this.exc = exc;
            cdl.countDown();
        }

        public V await() throws Exception {
            cdl.await();
            if (exc != null) {
                throw exc;
            }
            return result;
        }

        public 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);
            }
        }
    }

    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 {
                    //模拟10个并发
                    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();
        }
    }
}

互斥锁实现

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;

/**
 * @author hyy
 * @Description
 * @create 2023-12-23 21:42
 */
public class Cache {
    private static final Map<String, String> memo = new ConcurrentHashMap<>();

	//getValue是精髓
	//通过key.intern()进行上锁+double check之后不走MySQL
    public static String getValue(String key) {
        if (memo.containsKey(key)) {
            return memo.get(key);
        } else {
            synchronized (key.intern()) {
                if (memo.containsKey(key)) {
                    return memo.get(key);
                }

                String val = fetchVal();
                memo.put(key, val);

                System.out.println("bar");
                return val;
            }
        }
    }

    private static String fetchVal() {
        try {
            Thread.sleep(2000);
            return "singleFlight";
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        int count = 10;
        CountDownLatch cdl = new CountDownLatch(count);
        for (int i = 0; i < count; i++) {
            new Thread(() -> {
                try {
                    cdl.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(getValue("key"));
            }).start();
            cdl.countDown();
        }
    }
}

问题

上述问题,main线程都结束为什么还能看到输出?

  1. main线程确实结束了,但是子线程和main线程不影响。
  2. 在main线程中使用countDownLatch开启了count个子线程,当然可以看到输出。

上述多次运行后,结果为什么不同

exec方法结束后会remove(key),需要重新获取“锁”了

互斥锁和Single Flight有什么区别

Single Flight 当第一个请求返回时立刻通知唤醒所有等待的请求协程。此刻过后,所有的线程都不会阻塞,可以并发的继续执行。

而互斥锁在第一个请求返回后会释放锁,其他阻塞的请求协程需要逐个获取互斥锁,然后进行 double check 发现缓存已经存在,然后释放锁。也就是说所有请求的线程由于互斥锁的缘故,都是串行的。

参考资料

caihc.site/posts/f49da… ost.51cto.com/posts/16465 ayang.ink/