应用场景
缓存热点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线程都结束为什么还能看到输出?
- main线程确实结束了,但是子线程和main线程不影响。
- 在main线程中使用countDownLatch开启了count个子线程,当然可以看到输出。
上述多次运行后,结果为什么不同
exec方法结束后会remove(key),需要重新获取“锁”了
互斥锁和Single Flight有什么区别
Single Flight 当第一个请求返回时立刻通知唤醒所有等待的请求协程。此刻过后,所有的线程都不会阻塞,可以并发的继续执行。
而互斥锁在第一个请求返回后会释放锁,其他阻塞的请求协程需要逐个获取互斥锁,然后进行 double check 发现缓存已经存在,然后释放锁。也就是说所有请求的线程由于互斥锁的缘故,都是串行的。
参考资料
caihc.site/posts/f49da… ost.51cto.com/posts/16465 ayang.ink/