上篇文章说到了“三高”系统设计的手段,详情请移步至
如何设计高并发、高性能、高可用的系统
那么这篇文章主要讲讲其中的一个手段----请求合并
一、什么是请求合并
请求合并顾名思义就是将客户端产生的多个请求归并到一个一次对下游的请求
二、为什么要进行请求合并
假如有100个订单入库的操作,如果不合并请求的话,即便利用了数据库连接池的技术,也要对数据库操作100次。而这100次请求完全可以合并成一个或者几个请求去操作数据库,以减少对数据库的压力,同时也可以减少连接数据库带来系统损耗,提高网络I/O能力
三、实现
请求合并本身是用时间换空间的思为方法,就是牺牲一定的时长来换取西能提升的一种手段。我们可以用jdk 1.8的 CompleteFuture来实现我们的需求。
基本的实现思路是,单个入库请求过来后给请求添加一个CompleteFuture监听,然后将请求放入一个缓冲队列,然后起一个线程每隔一段时间去队列拉取请求组装成批量参数去请求批量接口。
我们以订单入库为例来实现一下:
package order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.*;
import java.util.concurrent.*;
@Service
public class Order {
private LinkedBlockingQueue<OrderRequest> orderRequestPool;
@Autowired
private RemoteCall remoteCall;
@PostConstruct
private void init() {
orderRequestPool = new LinkedBlockingQueue<>();
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
executor.scheduleAtFixedRate(() -> {
int size = orderRequestPool.size();
if (0 == size) {
return;
}
List<Map<String,String>> requests = new ArrayList<Map<String,String>>();
Map<String,CompletableFuture<Map<String,String>>> futures = new HashMap<>();
for (int i = 0; i < size; i++) {
OrderRequest orderRequest = orderRequestPool.poll();
Map<String,String> map = new HashMap<>();
map.put("orderNo", orderRequest.getOrderNo());
map.put("orderId", orderRequest.getOrderId());
requests.add(map);
futures.put(orderRequest.getOrderNo(), orderRequest.getFuture());
}
List<Map<String,String>> responses = remoteCall.batchRequest(requests);
for (Map<String,String> response : responses) {
futures.get(response.get("orderNo")).complete(response);
}
System.out.println("共处理请求个数:" + responses.size());
}, 0, 10, TimeUnit.MILLISECONDS);
}
public Map<String,String> addOrder(String orderId) throws ExecutionException, InterruptedException {
CompletableFuture<Map<String,String>> future = new CompletableFuture<>();
OrderRequest orderRequest = new OrderRequest();
orderRequest.setOrderId(orderId);
orderRequest.setOrderNo(UUID.randomUUID().toString());
orderRequest.setFuture(future);
orderRequestPool.add(orderRequest);
return future.get();
}
}
addOrder是客户端请求的当入库请求,服务端收到请求后会将请求封装成入库请求放到缓冲队列,并通过future.get来监听返回结果,如果有返回结果立刻返回给客户端。
init方法在spring创建Order Bean调用构造方法后执行,合并逻辑就在这里,每隔10ms就去缓冲队列里取数据(这里没做请求数量限制,一直拿到队列为空为止。可以根据需要限制每次获取的最大请求量),取完数据后调用远程批量接口。
远程批量接口如下:
package order;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
@Service
public class RemoteCall {
private AtomicLong atomicCounter;
private final static Logger logger = LoggerFactory.getLogger(RemoteCall.class);
@PostConstruct
private void init() {
atomicCounter = new AtomicLong(0);
}
public List<Map<String,String>> batchRequest(List<Map<String,String>> requests) {
long currentNo = atomicCounter.addAndGet(1);
logger.info("批量接口被调用了,当前是第{}次调用,本次请求共合并{}个", currentNo, requests.size());
List<Map<String,String>> response = new ArrayList<>();
for (int i = 0; i < requests.size(); i++) {
Map<String, String> map = new HashMap<String,String>();
map.put("orderNo", requests.get(i).get("orderNo"));
map.put("orderId",i + "");
response.add(map);
}
return response;
}
}
atomicCounter用来统计请求的次数,为了方便这里就mock了接口来模拟数据库的操作,这里直接返回操作结果。
我们来写个单元测试来测试一下这个接口,模拟1000个并发来看看实际操作数据库的次数:
package order;
import main.ApplicationStart;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ApplicationStart.class)
public class OrderTest {
@Autowired
private Order order;
@Test
public void testAddOrder() throws InterruptedException {
int count = 1000;
CountDownLatch countDownLatch = new CountDownLatch(count);
for (int i = 0; i < count; i++) {
int finalI = i;
Thread t = new Thread(() -> {
try {
countDownLatch.countDown();
countDownLatch.await();
Map<String,String> response = order.addOrder(finalI + "");
} catch (ExecutionException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t.start();
}
Thread.sleep(2000);
}
}
下面是执行结果:
如上图:本来是请求1000次的数据库调用最后只调用了6次。当然用CompleteFuture实现也有问题,它不支持超时机制,假如下游接口挂掉了,它会一直在等待接口返回。可以用LinkBlockQueue来替换CompleteFuture,LinkBlockQueue的poll提供了一个超时机制,可以自行去实现一下,这里不再赘述
总结
请求合并本质上是时间换空间(性能)的一个思路,利用时间的消耗来积累请求并实现合并请求提高性能的目的,本身也是一种优化手段。有人会说时间延长时间不是让请求时间处理变长了吗?未必,这要结合业务场景以及业务量来综合考量。时间换空间、空间换时间最终的目的都是提升我们系统性能的手段,没有哪个是绝对好的,我们要综合考虑,结合系统现状看哪个带来的收益大就用哪个手段