优雅的减少数据库吞吐量————接口请求和并

应用场景:

当一个接口存在高并发的使用环境时,有一万个请求就会访问数据库一万次,数据库会直接崩溃,这种情况是我们不愿意看到的。

很多人第一时间想到的就是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;
    }
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值