应用场景:
当一个接口存在高并发的使用环境时,有一万个请求就会访问数据库一万次,数据库会直接崩溃,这种情况是我们不愿意看到的。
很多人第一时间想到的就是redis缓存,将数据缓存一份到redis中,然后在redis中获取数据
但是如果是海量数据量的情况,把所有数据都缓存一份到redis也不太现实
这时我们可以对接口进行请求合并优化,让一万次请求合并为一次批量操作,只对数据库进行一次操作,然后再将结果分发给对应的请求.这样数据库的压力就会小很多,提高系统性能及稳定性
那么接口请求合并这么好,是不是所有的接口都可以合并呢?
答案显然是否定的。
只有业务场景所执行的sql可以合并为一次批量操作的时候才可以合并。比如添加删除就比较容易合并,简单的条件查询也可以合并。条件复杂的查询不太容易变成一条sql。修改在一般情况下,并发量不会太高,所有也不会做接口请求合并
整体思路大概是如下图
合并前:
每一次请求到达系统后系统都会访问一次数据库,在大并发量的情况下数据库很容易造成崩溃
合并后:
将请求合并为一条批量执行sql,然后再访问一次数据库,最后将结果响应给对应的请求
以下是代码实现
一.
首先我们需要创建一个自定义的请求对象
请求id:用来存储请求编号,响应结果需要通过请求id去找到对应的请求线程
参数:我这个示例是根据userId去查询用户信息,根据业务需求可以灵活的调整
CompletableFuture对象(泛型就是响应的对象)
二.当请求访问controller层时,创建一个callable,将响应对象返回
三.生成一个请求编号和CompletableFuture,然后将请求编号,参数,CompletableFuture封装到自定义的请求对象里,然后将请求对象放入阻塞队列中
future.get() 当调用get方法时,线程会被阻塞,直到CompletableFuture完成并返回结果为止
阻塞队列这里我使用的是LinkedBlockQueue
private final Queue<Request> queue = new LinkedBlockingQueue();
四.然后再写一个方法去定时获取阻塞队列里的请求对象,将请求合并为list集合后给到service层查询
将结果通过请求id找到对应的请求线程响应回去
五.service层进行业务的sql执行,将结果封装成一个Map集合.键为业务参数,值为响应的对象
返回Map集合后会在第四步,根据请求编号进行匹配各自的线程进行返回
实现效果:
我这里是使用了JMeter压测,开启十个线程,模拟十个用户并发访问
恭喜你:已经成功的理解了接口请求合并的思路了,那么接下来就是代码实现了
代码时刻:
封装的请求对象
/**
* 请求类,code为查询的共同特征,例如查询商品,通过不同id的来区分
* CompletableFuture将处理结果返回
*/
public class Request {
// 请求id 唯一
String requestId;
// 参数
Integer userId;
//TODO Java 8 的 CompletableFuture 并没有 timeout 机制
CompletableFuture<Users> completableFuture;
public String getRequestId() {
return requestId;
}
public void setRequestId(String requestId) {
this.requestId = requestId;
}
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
public CompletableFuture getCompletableFuture() {
return completableFuture;
}
public void setCompletableFuture(CompletableFuture completableFuture) {
this.completableFuture = completableFuture;
}
}
controller层:
package com.bwie.user.controller;
import com.bwie.common.pojo.User;
import com.bwie.user.service.UserWrapBatchService;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.concurrent.Callable;
/**
* @author FangShiBa
* @date 2024/2/20
* @apiNote
*/
@Log4j2
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserWrapBatchService service;
@PostMapping("/findById/{userId}")
public Callable<User> findById(@PathVariable("userId") Integer userId){
//返回
return new Callable<User>() {
@Override
public User call() throws Exception {
return service.findById(userId);
}
};
}
}
package com.bwie.user.service;
import com.bwie.common.pojo.User;
import com.bwie.common.pojo.Users;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.*;
@Service
public class UserWrapBatchService {
@Resource
private UserService userService;
/**
* 最大任务数
**/
public static int MAX_TASK_NUM = 100;
/**
* userId是查询的参数
*
*/
public class Request {
// 请求id 唯一
String requestId;
// 参数
Integer userId;
CompletableFuture<Users> completableFuture;
public String getRequestId() {
return requestId;
}
public void setRequestId(String requestId) {
this.requestId = requestId;
}
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
public CompletableFuture getCompletableFuture() {
return completableFuture;
}
public void setCompletableFuture(CompletableFuture completableFuture) {
this.completableFuture = completableFuture;
}
}
private final Queue<Request> queue = new LinkedBlockingQueue();
//表示该方法应该在类的构造函数执行完毕后、依赖注入完成之后执行。
@PostConstruct
public void init(){
//创建一个可以定时定间隔执行任务的线程池
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
//这里是用Lambda表达式,Runnable线程
scheduledExecutorService.scheduleAtFixedRate(() -> {
//获取此时阻塞队列里的请求对象个数
int size = queue.size();
//如果阻塞队列的长度等于0,说明没有请求,直接结束任务
if(size==0){
return;
}
//创建一个集合用来接收单位时间的请求对象
ArrayList<Request> list = new ArrayList<>();
System.out.println("合并了 [" + size + "] 个请求");
//遍历阻塞队列,将阻塞队列里的请求放入集合中
for (int i = 0; i < size; i++) {
//这个判断是限制一次合并的最大请求数量
if(i<MAX_TASK_NUM){
list.add(queue.poll());
}
}
//然后调用service层去执行批量操作
Map<String, User> response = userService.queryUserByIdBatch(list);
//遍历存放请求的集合,通过请求编号获取对应的响应结果
for (Request request : list) {
User user = response.get(request.getRequestId());
//将各自线程的响应结果返回
//执行完本行,controller层的future.get()方法才会完成并返回结果
request.getCompletableFuture().complete(user);
}
},1000,100,TimeUnit.MILLISECONDS);
// initialDelay 第一次执行任务的等待间隔
//period 每次执行任务的周期时间
// unit 前两个时间的时间单位
}
public User findById(Integer userId) {
//创建请求对象
Request request = new Request();
//查询条件放入请求对象
request.setUserId(userId);
//将请求编号放入请求对象中
request.setRequestId(UUID.randomUUID().toString().replace("-",""));
CompletableFuture<User> future = new CompletableFuture<>();
request.setCompletableFuture(future);
//将请求对象放入队列
queue.add(request);
try {
//将一步操作的结果返回(返回的对象就是CompletableFuture的泛型)
return future.get();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
}
service层:
package com.bwie.user.service.impl;
import com.alibaba.nacos.client.naming.utils.CollectionUtils;
import com.bwie.common.pojo.User;
import com.bwie.user.dao.UsersMapper;
import com.bwie.user.service.UserService;
import com.bwie.user.service.UserWrapBatchService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
public class UserServiceImpl implements UserService {
@Resource
private UsersMapper usersMapper;
@Override
public Map<String, User> queryUserByIdBatch(List<UserWrapBatchService.Request> userReqs) {
//将传过来的请求集合的业务参数处理成为批量操作的格式(根据业务场景不同会随动,我这里是根据ID批量查询,所以将编号进行字符串拼接)
String ids = "";
for (UserWrapBatchService.Request userReq : userReqs) {
ids += "," + userReq.getUserId();
}
//再创建一个HashMap集合用来存放响应数据(键是请求编号,值是响应的结果)
HashMap<String, User> result = new HashMap<>();
//执行批量操作的sql
List<User> users = usersMapper.findByIds(ids.substring(1));
//根据业务参数进行分组(键为请求参数,值为响应结果)
Map<Integer, List<User>> userGroup = users.stream().collect(Collectors.groupingBy(User::getUserId));
//然后遍历请求集合
userReqs.forEach(res -> {
//根据请求对象里的业务参数取到相对应的响应对象
List<User> users1 = userGroup.get(res.getUserId());
//将结果存入Map集合
if (!CollectionUtils.isEmpty(users1)) {
result.put(res.getRequestId(), users1.get(0));
} else {
result.put(res.getRequestId(), null);
}
});
//返回map集合
return result;
}
}