这几天基本每天都会看视频跟着学习一些东西,然后总结一下。
今天的总结是关于请求合并。
业务背景:优化接口的处理。预防系统为线程开销过大导致OOM。
处理思路:时间换空间的一种处理方式,在方法级别上 将同一时间请求的参数暂时堆积一起,然后合并成一个批量请求到数据库或者下游业务上。将线程开销的压力阻塞在上游服务上。
应用场景:部分业务在高并发的情况下会因为系统线程开销出现问题,但是专门为这个业务去增设服务器后,业务平峰的情况下服务器完全闲置产生性能浪费。出现的一种解决方案。
优缺点:优点我理解就是处理性能上的提升,系统开销的减少。缺点:我理解多次请求合并成一个。查询功能还好,
如果是复杂的业务,有一个失败导致的异常,会导致别的参数也失败。那么需要使用者对接口的业务实现做到知根知底。
首先我们利用countdownlatch模拟一下1000个线程并发问题的业务场景。
或者我们可以利用测试的一些并发工具去做这些事情。
然后我们去看调用的requestSumService.getStr方法。
public String getStr(int id)throws Exception ;
很简单的一个根据id返回String的一个接口。里面的实现暂定返回 return id+"test";
正常的业务场景下,我们将在系统启动后,启动1000个线程进行并发访问。每个返回耗时基本在2-3毫秒。
如果查数据库的话,我们的数据库就要同时创建1000个线程进行处理。
这个时候我们在方法内部做一些改变。
思路:我们的方法新增一个全局的无界队列(LinkedBlockingQueue),在收到请求后,不进行处理,而是将参数封装成一个对象Request,然后将这个对象放入队列。
同时,我们在类的中间新增一个方法,加上@PostConstruct,内部实现一个定时器,每20ms从队列中取出收到的请求数据,将请求数据作为参数,调用一个批量处理的下游业务处理方法。从而实现合并请求。
上代码:
@Service
public class RequetSumService implements IRequestSumService {
/**
* 无界阻塞队列 用来存储临时保存的参数信息 先进先出
*
*/
private LinkedBlockingQueue<Request> queue = new LinkedBlockingQueue<>();
/**
* 参数传递和返回的中间介质
*/
class Request{
/**
* 请求参数信息
*/
public int id;
/**
* 线程future信息 分发返回信息用 @since 1.8
*/
public CompletableFuture<String> future;
}
/**
* 利用类初始加载时执行一次的特性 进行一个定时器的加载
* 定时的业务:每20毫秒扫描一次队列的数据,取出请求参数 改成批量查询数据库或者第三方
* 空间换时间
*/
@PostConstruct
public void timerMethod() {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
scheduledExecutorService.scheduleAtFixedRate(()->{
int size = queue.size();
if (size > 0) {
List<Request> requestList = Lists.newArrayList();
log.info("请求一次,本次请求数量-{}", size);
for(int i = 0 ;i < size;i++){
requestList.add(queue.poll());
}
// 批量请求合并成一个请求去处理数据
List<Integer> idList = Lists.newArrayList();
for(Request request : requestList){
idList.add(request.id);
}
Map<Integer, String> resultMap = getStrByIdList(idList);
// 收到集体返回的信息后 进行分发 这里涉及到多线程中间的通信和传值问题 这里用到futrue
for(Request request : requestList){
request.future.complete(resultMap.get(request.id));
}
}
},0,20, TimeUnit.MILLISECONDS);
}
/**
* 单次请求的方法
* 思路:将请求的数据 以数据的形式 放到类初始的队列中 然后利用futrue值传递带回来值
* 在特定的业务场景下有用处
* @param id
* @return
*/
public String getStr(int id) throws Exception {
CompletableFuture<String> future = new CompletableFuture<>();
Request request = new Request();
request.id = id;
request.future = future;
//将参数合并 装入队列中 等待定时器合并执行
queue.add(request);
return future.get();
}
/**
* 模拟数据库和其他业务方的批量获取方法 这里写成本地方法 没有判空和去重等校验
* @param idList
* @return
*/
public Map<Integer, String> getStrByIdList(List<Integer> idList) {
Map<Integer, String> resultMap = Maps.newHashMap();
for (int id : idList) {
resultMap.put(id, id + "test");
}
return resultMap;
}
}
批量调用数据返回的是众多请求参数的一个结果集,那么如果来区分哪个请求对应哪个结果呢。
这里用到的是future. ->CompletableFuture. @since1.8
默认我们Thread继承的是Runable,返回值是void,是达不到我们的要求的。
CompletableFuture可以异步等待返回值。上面我们在获取到结果集后,根据封装参数里带的future可以来设置value。
然后通过future.get()来获取值。
扩展:get()和join()都可以实现获取异步返回值。
两者的区别一个是报错的异常不一样,一个是get()是有等待时间的。两者在线程没有获取到值前,
都会调用wait(boolean flag)来获取值,flag为是否可以中断。get为true,false为false. get()方法是会主动中断的。
要学习的地方还很多。知道越多才越发现自己的无知。