如何让QPS提升20倍

一、什么是QPS

QPS,全称Queries Per Second,即每秒查询率,是用于衡量信息检索系统(例如搜索引擎或数据库)或请求-响应系统(如Web服务器)每秒能够处理的请求数或查询次数的一个性能指标。以下是对QPS的详细解释:

一、定义与意义

  • 定义:QPS表示系统在单位时间(通常为一秒)内能够成功处理的请求数量。在高并发场景下,这个指标尤为重要,因为它直接关系到系统的稳定性和用户体验。
  • 意义:QPS是衡量服务器性能的关键指标之一,它直接反映了系统处理请求的能力。通过监控和优化QPS,可以确保系统在高负载下依然保持高效稳定运行。

二、计算方法

QPS的计算公式为:QPS = 总请求数 / 时间段(秒)。具体计算步骤如下:

确定时间窗口:根据实际需求和服务器负载情况来确定时间窗口,可以是1秒、1分钟、5分钟等。

收集查询日志:收集服务器在该时间窗口内的查询日志,并统计成功处理的请求总数。

计算QPS:将总请求数除以时间窗口的总秒数,即可得到平均每秒的查询率。

例如,如果在1分钟内系统处理了3000个请求,则QPS = 3000 / 60 = 50 QPS,意味着系统平均每秒处理了50个请求。

三、与并发数和响应时间的关系

QPS与系统的并发数和响应时间紧密相关。具体来说:

并发数:并发数是指系统在同一时间内处理的请求数量。并发数越高,系统在单位时间内能够处理的请求数量也就越多,从而可能提高QPS。

响应时间:响应时间是指系统从接收到请求到返回响应结果所需的时间。响应时间越短,系统在单位时间内能够处理的请求数量也就越多,同样可能提高QPS。

因此,在优化系统性能时,可以通过增加并发数和缩短响应时间来提升QPS。

二、同步代码

所谓的同步代码,也就是从我们接受到请求直到请求返回都是由一个线程处理的,如果处理代码中有阻塞那么这个时候此线程就会阻塞,在请求量比较大的情况下,也就是并发场景,这个时候会有很多的请求发过来,那么tomcat只有两百的线程,如果线程阻塞时间较长,那么tomcat的线程会被全部阻塞,导致无法处理外部请求,进而系统的吞吐量就会很低

2.1、示意图

在这张图片中可以清晰的看到,前端(移动端+pc端)发过来一个请求,这时tomcat会开启一个线程处理,这个线程从接受请求是开启直到请求返回都是一个线程在处理,那么就会存在上面所说的同步阻塞问题

到这里先思考三秒钟,该情况如何优化
这是同步代码的第一个问题。(大家别慌,我们先提出同步代码的所有问题,然后我们一一解决)

接下来继续探索下一个问题

以上我们聊了从接受请求到处理请求都是由一个线程处理,当并发量大并且代码有阻塞的情况下,会将tomcat的线程耗尽,从而达到tomcat的瓶颈。那造成这个问题的原因是什么呢?

  • 第一个:由于tomcat的线程是有限的(200)
  • 第二个:由于处理代码耗时,导致线程阻塞,进而导致tomcat线程耗尽

2.2、同步处理代码图解

在这张图中,大家可以清晰的看到当需要完成这一个任务时,需要先完成任务1,再完成任务2,然后完成任务3。那么所消耗的时间就是 :time > 任务1 + 任务2 + 任务3,在这里我举个实际生活中的场景,如果你要下单,那么需要调用 用户服务(查询用户信息)--->商品服务(查询商品信息)--->积分服务(修改积分)--->订单服务(生成订单)--->库存服务(减库存)

2.3、代码示例:伪代码模拟

java 复制代码
JSONPObject createOrder(Integer userId,Integer goodsId){
        // 1、调用用户服务,获取用户信息
        User user = getUserById(userId); // 2s
        // 2、调用商品服务,获取商品详情
        Goods goods = getGoodsById(goodsId); // 2s
        // 3、调用积分服务,修改积分
        updatePoints(userId);  // 2s
        // 4、调用订单服务,生成订单
        createOrderByUserAndGoods(user,goods); // 2s
        // 5、调用库存服务,修改库存
        updateInventoryByGoodsId(goodsId);  //2s
        
        return null;
    }

这里只给出了个示例,实际中链路会很长,那这个时候是不是需要花费很长的时间,那这里也将是我们需要优化的点

三、异步代码优化

首先我们使用异步代码优化第二个问题,也就是刚刚提到的代码串行所造成的耗时,进而导致的线程阻塞。

3.1、图解异步代码

先来张图

在这幅图中可以清晰看到只要到我们的处理代码,我们开启了四个线程处理,在这里我将订单服务放到了用户服务和商品服务完成之后处理,这里和你的系统设计有关系,也可以和其他服务同时并发处理,那么经过这次优化后,处理时间 time > 前四个服务中最长的 + 订单服务,这样既完成了代码串行问题的优化。

很多小伙伴在这个时候是不是想着光理论没用,要能代码实现。放心,肯定会有代码实现的。

3.2、代码示例:

java 复制代码
JSONPObject createOrder2(Integer userId, Integer goodsId) {
        // 1、调用用户服务,获取用户信息
        CompletableFuture<User> future1 = CompletableFuture.supplyAsync(() -> {
            // 2s
            return getUserById(userId);
        });
        // 2、调用商品服务,获取商品详情
        CompletableFuture<Goods> future2 = CompletableFuture.supplyAsync(() -> {
            return getGoodsById(goodsId); // 2s
        });
        // 3、调用积分服务,修改积分
        CompletableFuture<Void> future3 = CompletableFuture.runAsync(() -> {
            updatePoints(userId);  // 2s
        });
        // 4、调用订单服务,生成订单(在用户服务和商品服务调用结束后执行)
        CompletableFuture<Void> completableFuture = future1.thenCombineAsync(future2, (user, goods) -> {
            createOrderByUserAndGoods(user, goods); // 2s
            return null;
        });
        // 5、调用库存服务,修改库存
        CompletableFuture.runAsync(() -> {
            updateInventoryByGoodsId(goodsId);  //2s
        });
        return null;
    }

在这里大家你要纠结为什么用户服务和商品服务完成后调用订单服务,这个和你的业务逻辑有关系,怎么写都无所谓,在这里大量使用了CompletableFuture,接下来我详细介绍一下CompletableFuture

3.3、CompletableFuture 详讲

CompletableFuture是Java 8中引入的一个类,它实现了Future和CompletionStage接口,为异步编程提供了强大的支持。以下是对CompletableFuture的详细介绍:

3.3.1、基本概念与特性

  • 异步执行:CompletableFuture允许任务在后台线程中异步执行,不会阻塞主线程,从而提高了应用程序的响应性和性能。
  • 可组合性:CompletableFuture的操作可以组合成一个或多个CompletableFuture对象,构成复杂的异步计算链。这包括结果的转换、组合以及异常处理等。
  • 异常处理:通过exceptionally()等方法,CompletableFuture可以捕获计算中的异常并返回默认值,或者通过handle()等方法同时处理正常结果和异常。
  • 取消与超时:支持取消异步任务和设置超时时间,避免任务的无限等待。
  • 非阻塞式等待:提供了非阻塞式的等待方法,如join()和getNow(),可以在不阻塞当前线程的情况下获取任务的结果。
  • 并行处理:在处理多个耗时操作时,如I/O操作、数据库访问或网络请求,CompletableFuture可以并行执行这些任务,提高系统吞吐量和响应能力。

3.3.2、创建CompletableFuture实例

1、supplyAsync():用于创建返回结果的异步任务。例如:

java 复制代码
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    // 执行异步任务并返回结果
    return "Hello, CompletableFuture!";
});

2、runAsync():用于创建不返回结果的异步任务。例如:

java 复制代码
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    // 执行异步任务
    System.out.println("Task running asynchronously");
});

3.3.3、任务编排方法

3.3.3.1、转换类方法
  • thenApply() / thenApplyAsync():将上一个任务的结果转换为新的结果。thenApply()在同一个线程中执行,而 thenApplyAsync()可能在新的线程中执行。
  • thenAccept() / thenAcceptAsync():处理上一个任务的结果,但不返回新的值。thenAccept()在同一个线程中执行,而thenAcceptAsync()可能在新的线程中执行。
  • thenRun() / thenRunAsync():在上一个任务完成后执行一个操作,不使用上一个任务的结果。
3.3.3.2、组合类方法
  • thenCompose() / thenComposeAsync():将两个CompletableFuture组合成一个。当一个任务依赖另一个任务的结果时,可以使用此方法。
  • thenCombine() / thenCombineAsync():组合两个独立任务的结果。需要两个独立任务的结果进行计算时,可以使用此方法。
3.3.3.3、多任务协调方法
  • allOf():等待所有任务完成。适用于需要等待多个任务都完成的场景。
  • anyOf():等待任意一个任务完成。适用于多个任务中只需要最快的结果的场景。
3.3.3.4、异常处理机制
  • exceptionally():处理异常并提供默认值。当CompletableFuture中的任务抛出异常时,可以捕获该异常并返回一个默认值。
  • handle() / handleAsync():处理正常结果和异常。无论任务是否成功完成,都可以使用此方法处理结果或异常。
  • whenComplete() / whenCompleteAsync():任务完成时的回调(正常或异常)。可以在任务完成后执行一些清理工作或记录日志等。
3.3.3.5、使用示例

以下是一个简单的使用示例,展示了如何创建CompletableFuture对象、进行任务编排以及处理异常:

java 复制代码
public class CompletableFutureExample {
    public static void main(String[] args) {
        // 创建两个异步任务
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(1000); // 模拟耗时操作
                return "Result from future1";
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return "Task interrupted";
            }
        });

        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(500); // 模拟耗时操作
                return "Result from future2";
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return "Task interrupted";
            }
        });

        // 组合两个异步任务的结果
        CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (result1, result2) -> {
            return result1 + " and " + result2;
        });

        // 处理异常并提供默认值
        CompletableFuture<String> safeFuture = combinedFuture.exceptionally(ex -> {
            return "Default value due to error: " + ex.getMessage();
        });

        // 获取结果并打印
        try {
            String result = safeFuture.get(); // 阻塞等待结果返回
            System.out.println(result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,我们创建了两个异步任务future1和future2,它们分别在不同的线程中执行并返回结果。然后,我们使用thenCombine()方法将这两个任务的结果组合成一个新的CompletableFuture对象combinedFuture。接着,我们使用exceptionally()方法处理可能发生的异常,并提供一个默认值。最后,我们使用get()方法阻塞等待结果返回并打印出来。

综上所述,CompletableFuture是Java异步编程的强大工具,它提供了一种简洁且强大的方式来处理异步任务。通过丰富的API和灵活的任务编排能力,CompletableFuture可以轻松地创建、组合和链式调用异步操作,从而提高了程序的响应速度和资源利用率。
大家看完这里CompletableFuture的介绍后,再回过头去看看我们写的伪代码就知道怎么回事儿了(为什么是异步的,为什么会提高系统执行时间)

3.4、接口异步

在这里我们已经优化了同步代码所造成的线程阻塞问题,那我们如何优化tomcat因线程有限(200)而造成的吞吐量下降问题呢?

首先我们分析一下问题在哪里:1、tomcat线程池有限(200);2、占用tomcat线程时间过长

占用时间过长我们已经做了优化,针对第一个问题,最简单的方法时配置tomcat的线程数量,但是这种方法并不是我们研究的重点。这里我们依然采用异步的方式去解决问题

解决的核心思路:tomcat主线程接受请求------> 交给子线程处理 ----->找tomcat线程返回

3.4.1、图解

在这里这样写大家可能看不懂,上图:

针对这张图,我i在这里做详细介绍:

前端发起请求,tomcat接受到请求后,通过Spring MVC的DispatcherServlet将请求交给响应的 controller 处理,但这个controller返回的是一个CompletableFuture对象,那么这个时候任务就会交给子线程处理,tomcat 线程将被释放,并且spring boot会开启一个监听器,监听你返回的 CompletableFuture 对象的状态,一旦CompletableFuture对象状态被修改为完成,那么这个时候就会找到tomcat线程返回相应的结果

3.4.2代码示例

controller

java 复制代码
 @GetMapping("name")
   public CompletableFuture<String> getUserName(){
       return userService.getUserName();
   }

   @GetMapping("setName")
   public void setName(){
       userService.setUserName();
   } 

service

java 复制代码
 CompletableFuture<String> completableFuture = new CompletableFuture<>();
 
    @Override
   public CompletableFuture<String> getUserName() {
       return completableFuture;
   }

   @Override
   public void setUserName() {
       completableFuture.complete("siyu");
   }

在这里你就会看到你请求name接口时并拿不到数据,当你在请求一下setName接口时name接口就拿到了值,这里就实现了异步操作,当然实际代码中你肯定不会这么用,这只是个示例,实际代码中设置name这一步你可能会用定时任务什么的去实现,我就不过多赘述了。

在这里优化思路已经讲完了。那来个实际优化案例,本例使用(异步+合并)的方式提升系统并发量

四、实际场景优化案例

controller 代码示例

java 复制代码
@RestController
@RequestMapping("user")
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping
    public CompletableFuture<User> getUserById(@RequestParam("userId") Integer userId) throws ExecutionException, InterruptedException {
        return userService.getUser(userId);
    }
}

service 代码示例

java 复制代码
public interface UserService {
    CompletableFuture<User> getUser(Integer userId) throws ExecutionException, InterruptedException;
}

serviceImpl 代码示例

java 复制代码
@Slf4j
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;

    private final LinkedBlockingDeque<Request> blockingDeque = new LinkedBlockingDeque<>();
    private final ExecutorService executorService = Executors.newFixedThreadPool(16);
    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(16);

    @Data
    static class Request {
        Integer userId;
        CompletableFuture<User> completableFuture;
    }

    @Override
    public CompletableFuture<User> getUser(Integer userId) {
        CompletableFuture<User> future = new CompletableFuture<>();
        Request request = new Request();
        request.userId = userId;
        request.completableFuture = future;
        blockingDeque.add(request);
        return future;
    }
    @PostConstruct
    public void init() {
        AtomicInteger count = new AtomicInteger(0);
        scheduler.scheduleAtFixedRate(() -> {
            if (blockingDeque.isEmpty()) return;
            List<Request> requests = new ArrayList<>();
            blockingDeque.drainTo(requests);
            Set<Integer> userIds = requests.stream().map(Request::getUserId).collect(Collectors.toSet());
            List<User> usersFromDb = userMapper.selectByIds(userIds);
            log.info("查询数据库{}次,处理{}个请求", count.incrementAndGet(), requests.size());
            Map<Integer, User> userMap = usersFromDb.stream().collect(Collectors.toMap(User::getUserId, user -> user));

            for (Request request : requests) {
                CompletableFuture.runAsync(() ->{
                    User user = userMap.getOrDefault(request.userId, null);
                    request.completableFuture.complete(user);
                }).exceptionally(ex ->{
                    log.error(ex.getMessage());
                    return null;
                });
            }
        }, 200, 200, TimeUnit.MILLISECONDS);
    }

    @PreDestroy
    public void destroy() {
        scheduler.shutdown();
        try {
            if (!scheduler.awaitTermination(60, TimeUnit.SECONDS)) {
                scheduler.shutdownNow();
                if (!scheduler.awaitTermination(60, TimeUnit.SECONDS)) {
                    System.err.println("Scheduler did not terminate!");
                }
            }
        } catch (InterruptedException ex) {
            scheduler.shutdownNow();
            Thread.currentThread().interrupt();
        }
        executorService.shutdown();
        try {
            if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
                executorService.shutdownNow();
                if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
                    System.err.println("ExecutorService did not terminate!");
                }
            }
        } catch (InterruptedException ex) {
            executorService.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }
}

首先说明一下,本例中使用的userId,那这个场景肯能不是很多,比如说多人查询同一个热门商品,那就很好用了。

代码设计思想解读:

大量请求发过来后,构建 Request 对象,Request 对象包含请求的userId和一个Completablefuture对象,然后将 Request 放入阻塞队列,等待定时任务处理,接口直接返回Completablefuture对象。

定时任务从阻塞队列中定时弹出所有请求进行处理。拿到请求后,根据 userId 去重,然后调用批量查询接口查询数据,拿到数据后,比对 Request 中的userId和获取到数据的userId,如果相等,将获取后的数据设置到对应Request 的Completablefuture对象。完结散花。

五、祝愿

路漫漫其修远兮,吾将上下而求索。
愿明天的您遇见更好的自己

相关推荐
excel1 分钟前
webpack 核心编译器 十四 节
前端
excel8 分钟前
webpack 核心编译器 十三 节
前端
腾讯TNTWeb前端团队7 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰10 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪11 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪11 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy11 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom12 小时前
快速开始使用 n8n
后端·面试·github
uhakadotcom12 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom12 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试