目录
应用场景
缓存热点key失效,缓存击穿,大量请求到数据库。
核心思想
如果有多个并发请求同时请求同一个资源,那么只需要执行一次实际的请求,然后将结果返回给所有的请求者。
代码演示和讲解
要点
此功能实现主要在以下两点
1)由哪个线程去获取资源。
2)获取资源的过程中其他线程如何阻塞,如何获取到结果。
代码
1)Call类
import java.util.concurrent.CountDownLatch;
public class Call {
private Object val;//使用Object,可以转换成java对象,更具有通用性
private CountDownLatch cld;//做阻塞使用,阻塞其他线程
public Object getVal() {
return val;
}
public void setVal(Object 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();
}
}
2)CallManage类
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;
public class CallManage {
private final Lock lock = new ReentrantLock();//锁,保证callMap的并发安全
private Map<String, Call> callMap = new HashMap<>();//存请求key和对应的调用信息,包括返回结果等
public Object run(String key, Supplier<Object> func) {
this.lock.lock();//尝试获取锁,获取不到阻塞等待,这个地方也可以改成this.lock.tryLock(time,unit),防止查库的线程阻塞住引发其他的都阻塞
Call call = this.callMap.get(key);
if (call != null) {//call != null流程
//已经存在处理中的请求
this.lock.unlock();//释放锁,下面的call.await()维持阻塞
call.await();//第一个线程还没调用call.done()之前回在此阻塞,调用call.done()之后call已经有了val,可以直接获取
return call.getVal();
}
//call=null,是当前key接收到的第一个请求
call = new Call();
call.lock();//初始化CountDownLatch
this.callMap.put(key, call);//call放入map,相同key的其他线程也是获取同一个call
this.lock.unlock();//释放锁后,相同key的其他线程获取锁之后会进入call != null流程,此流程等待读取
call.setVal(func.get());//查询数据库,获取缓存,放入到call.val
call.done();//调用call.done之前,其他线程处于call.await阻塞等待状态
this.lock.lock();//也可以换成只锁自己当前key
this.callMap.remove(key);//删除key,这一轮的并发结束
this.lock.unlock();
return call.getVal();//返回value,(是查询数据库之后的value)
}
}
3)TestClass类
public class TestClass {
public static void main(String[] args) throws InterruptedException {
//缓存失效后,走的流程
CallManage callManage = new CallManage();//全局bean
int count = 1000;
String key = "key_asdf";
for (int i = 0; i < count; i++) {
new Thread(new Runnable() {
@Override
public void run() {
Object value = callManage.run(key,()->{
System.out.println("查询数据库,更新缓存");
try {
Thread.sleep(1000);//休眠,模拟查库的时间
} catch (InterruptedException e) {
e.printStackTrace();
}
return "新的缓存:"+System.currentTimeMillis();//这个地方也可以是返回java bean,更通用
});
System.out.println("获取到的值为"+((String)value).toString());//如果返回的是java bean,这个地方可以强转bean
}
}).start();
}
}
}
流程示意图
测试结果
1000个线程,数据库查询流程只走了一次
结束语
如有任何问题可以留言,我会找时间回复,如有异议或者建议也欢迎提出。