高并发下的系统优化-请求合并
在互联网的场景下, 总会有一些接口长期大量的被访问,如个人信息,订单查询,商品查询等。这些请求的都有一个特点,就是查询简单,只需要传入一个id就可以进行查询。应对这种大量频繁的查询,我们可以做请求合并。
前提:在做请求合并有个重要前提,就是系统必须支持一次传入多个查询条件,然后得到返回结果,通俗的讲就是支持in的查询。
模拟场景:根据产品ID查询产品信息,并发2000次/秒
请求流程如下:
- 前台用户请求查询产品的接口 --> 后台controller接收请求 --> service调用mapper查询数据库或者第三方接口查询数据 --> 返回结果给前台
- 重复2000次如上请求。
造成的后果会在1s内查询2000次数据库或者第三方系统。
简单的代码示例:
controller
package springboot.controller;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import springboot.model.Product;
import springboot.service.ProductService;
/**
*
* @author liuhongya328
*
* @date 2019-11-21
*/
@RestController
public class ProductController {
@Autowired
private ProductService productService;
/**
* 根据产品编号查询产品信息
* @throws Exception
*/
@RequestMapping("/findProductById")
@ResponseBody
public Map<String, Object> findProductById(String productId) throws Exception{
Map<String, Object> returnMap = new HashMap<String, Object>();
Product product = productService.findProductById(productId);
returnMap.put("returnValue",product);
return returnMap;
}
}
service
package springboot.service.impl;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import springboot.mapper.ProductMapper;
import springboot.model.Product;
import springboot.service.ProductService;
/**
*
* @author liuhongya328
*
* @date 2019-11-21
*/
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private ProductMapper productMapper;
//正常的业务逻辑,每次请求从controller传递来调用一次service的方法,去数据库或者调用远程接口获取一次数据。
@Override
public Product findProductById(String productId) {
System.out.println(System.currentTimeMillis()+":"+Thread.currentThread().getName()+"--进行请求!");
return productMapper.selectByPrimaryKey(productId);
}
}
mapper
package springboot.mapper;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Mapper;
import springboot.model.Product;
@Mapper
public interface ProductMapper {
Product selectByPrimaryKey(String productId);
List<Product> findAllProduct(List<String> productIds);
}
用jmeter模拟1秒请求2000次的请求结果:
...
...
...
1574330132478:http-nio-18080-exec-16--进行请求!
1574330132478:http-nio-18080-exec-162--进行请求!
1574330132478:http-nio-18080-exec-189--进行请求!
1574330132479:http-nio-18080-exec-169--进行请求!
1574330132479:http-nio-18080-exec-163--进行请求!
1574330132479:http-nio-18080-exec-116--进行请求!
1574330132479:http-nio-18080-exec-200--进行请求!
1574330132480:http-nio-18080-exec-4--进行请求!
1574330132480:http-nio-18080-exec-185--进行请求!
1574330132478:http-nio-18080-exec-39--进行请求!
1574330132502:http-nio-18080-exec-116--进行请求!
1574330132503:http-nio-18080-exec-16--进行请求!
1574330132503:http-nio-18080-exec-39--进行请求!
1574330132504:http-nio-18080-exec-185--进行请求!
1574330132504:http-nio-18080-exec-169--进行请求!
1574330132504:http-nio-18080-exec-200--进行请求!
1574330132504:http-nio-18080-exec-189--进行请求!
1574330132503:http-nio-18080-exec-162--进行请求!
1574330132503:http-nio-18080-exec-4--进行请求!
1574330132503:http-nio-18080-exec-163--进行请求!
下面开始进行请求合并:
思路:需要设计一个线程每隔一定的时间将请求收集起来,批量去查询数据库或者调用第三方系统。
只需要改动service,代码如下
package springboot.service.impl;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import springboot.mapper.ProductMapper;
import springboot.model.Product;
import springboot.service.ProductService;
/**
*
* @author liuhongya328
*
* @date 2019-11-21
*/
@Service
public class ProductServiceImpl implements ProductService {
class Request{
String productId;
CompletableFuture<Product> future;
}
@Autowired
private ProductMapper productMapper;
//队列用来存储请求
BlockingQueue<Request> queue = new LinkedBlockingQueue<Request>();
@PostConstruct
public void init() {
//创建定时任务线程池,每隔50ms就执行一次,读取队列中的请求,放入到统一list中
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
scheduledExecutorService.scheduleAtFixedRate(() -> {
int size = queue.size();
if(size == 0 )
return;
ArrayList<Request> requests = new ArrayList<Request>();
for(int i = 0; i < size; i++) {
Request request = queue.poll();
requests.add(request);
}
System.out.println("现在批量请求了:"+size+"条请求");
List<String> productIds = new ArrayList<String>();
for(Request request : requests) {
productIds.add(request.productId);
}
//调用批量查询的方法
List<Product> responses = productMapper.findAllProduct(productIds);
//请求合并完成,开始将结果分发返回给每一个具体的request
Map<String,Product> returnMap = new HashMap<String,Product>();
for(Product product : responses) {
returnMap.put(product.getProductId(), product);
}
for(Request request : requests) {
Product product = returnMap.get(request.productId);
//返回结果传给future,即通知外部线程可以get到当前请求的数据。
request.future.complete(product);
}
}, 0, 50, TimeUnit.MILLISECONDS);
}
//请求合并-->2000次请求同时执行,前提需要有批量查询的方法或者接口
@Override
public Product findProductById(String productId) throws Exception {
Request request = new Request();
request.productId = productId;
CompletableFuture<Product> future = new CompletableFuture<>();
request.future = future;
//拦截请求,将请求放入容器
queue.add(request);
//放弃单一调用
//return productMapper.selectByPrimaryKey(productId);
//用future是因为需要从另外个线程中取数据,线程中的数据交互可以用future
return future.get();
}
}
用jmeter模拟1秒请求2000次的请求结果:
...
...
...
现在批量请求了:22条请求
现在批量请求了:31条请求
现在批量请求了:23条请求
现在批量请求了:30条请求
现在批量请求了:25条请求
现在批量请求了:27条请求
现在批量请求了:29条请求
现在批量请求了:165条请求
现在批量请求了:59条请求
现在批量请求了:197条请求
现在批量请求了:3条请求
现在批量请求了:35条请求
现在批量请求了:36条请求
效果实现,如有疑问或者错误,欢迎交流和指正!