并行获取机票信息—高并发场景微服务实战(七)

简介: 你好,我是程序员Alan, 很高兴遇见你。在《 需求分析—高并发场景微服务实战(二)》一文的最后,我提了一个问题 “你会用什么方式获取和聚合机票信息?”,今天我会详细地讲解解决这类问题的几种常用方法。

你好,我是程序员Alan,很高兴遇见你。

在《 需求分析—高并发场景微服务实战(二)》一文的最后,我提了一个问题 “你会用什么方式获取和聚合机票信息?”,今天我会详细地讲解解决这类问题的几种常用方法。

问题回顾

在开始讲解问题的解决方法之前,我们再来看一下问题的具体描述。搭建一个订票系统经常会有这样的需求 ,那就是同时获取多家航空公司的航班信息。比如,从深圳到三亚的机票钱是多少?有很多家航空公司都有这样的航班信息,所以应该把所有航空公司的航班、票价等信息都获取到,然后再聚合。由于每个航空公司都有自己的服务器,所以需要分别去请求它们的服务器,如下图所示:  

解决方法

1. 串行

我们想获取所有航空公司某个航班信息,要先去访问东航,然后再去访问南航,以此类推。每一个请求发出去之后,等它响应回来以后,我们才能去请求下一个航空公司,这就是串行的方式。

这样做的效率非常低下,如果航空公司比较多,假设每个航空公司都需要 1 秒钟的话,那么用户肯定等不及,所以这种方式是不可取的。

2. 并行

既然串行的方法很慢,那么我们可以并行地去获取这些机票信息,然后再把机票信息给聚合起来,这样的话,效率会成倍的提高。

这种并行虽然提高了效率,但也有一个缺点,那就是会“一直等到所有请求都返回”。如果有一个航空公司的响应特别慢,那么我们的整个服务就会被拖累。所以我们需要再改进一下,增加超时获取的功能。

3. 有超时的并行获取

上图的这种情况,就属于有超时的并行获取,同样也在并行的去请求各个公司的机票信息。但是我们规定了一个超时时间,如果没能在指定时间内响应信息,我们就把这些请求给忽略掉,这样用户体验就比较好了,它最多只需要等固定的时间就能获得机票信息,虽然拿到的信息可能是不全的,但是总比一直等更好。

实现这个目标有多种实现方案,我们一个个的来看看。

3.1 线程池的实现

第一个实现方案是用线程池,我们来看一下代码。

/*** @author alan* @create 2022 - 10 - 05  15:17*/publicclassThreadPoolDemo {
ExecutorServicethreadPool=Executors.newFixedThreadPool(3);
publicstaticvoidmain(String[] args) throwsInterruptedException {
ThreadPoolDemothreadPoolDemo=newThreadPoolDemo();
System.out.println(threadPoolDemo.getPrices());
    }
privateSet<Integer>getPrices() throwsInterruptedException {
Set<Integer>prices=Collections.synchronizedSet(newHashSet<Integer>());
threadPool.submit(newTask(1, prices));
threadPool.submit(newTask(2, prices));
threadPool.submit(newTask(3, prices));
Thread.sleep(3000);
returnprices;
    }
privateclassTaskimplementsRunnable {
IntegerproductId;
Set<Integer>prices;
publicTask(IntegerproductId, Set<Integer>prices) {
this.productId=productId;
this.prices=prices;
        }
@Overridepublicvoidrun() {
intprice=0;
try {
Thread.sleep((long) (Math.random() *6000));
price=productId;
            }catch (Exceptione){
e.printStackTrace();
            }
prices.add(price);
        }
    }
}

在代码中,新建了一个线程安全的 Set,命名为Prices 用它来存储价格信息,然后往线程池中去放任务。线程池是在类的最开始时创建的,是一个固定 3 线程的线程池。

在Task的run方法中,用一个随机的时间取模拟各个航空公司的响应时间,然后再返回我们传入的值作为票价,最后把这个票价放到Set中。

getPrices 函数中,我们新建了三个任务,productId 分别是 1、2、3,为了实现等待固定时间的功能,在这里调用了 Thread 的 sleep 方法来休眠 3 秒钟,它就会在这里等待 3 秒,之后直接返回 prices。

此时,如果 Math.random() * 6000) 的值很小,任务的响应速度快的话,返回的prices 里面最多会有三个值,但是如果每一个响应时间都很慢,那么可能 prices 里面一个值都没有。

这就是用线程池去实现的最基础的方案。

3.2 CountDownLatch

上面的方法有一个优化的空间,比如说网络特别好时,每个航空公司响应速度都特别快,你根本不需要等三秒,有的航空公司可能几百毫秒就返回了,那么我们也不应该让用户等 3 秒。所以需要进行一下这样的改进,看下面这段代码:

/*** @author alan* @create 2022 - 10 - 05  15:32*/publicclassCountDownLatchDemo {
ExecutorServicethreadPool=Executors.newFixedThreadPool(3);
publicstaticvoidmain(String[] args) throwsInterruptedException {
CountDownLatchDemocountDownLatchDemo=newCountDownLatchDemo();
System.out.println(countDownLatchDemo.getPrices());
    }
privateSet<Integer>getPrices() throwsInterruptedException {
Set<Integer>prices=Collections.synchronizedSet(newHashSet<Integer>());
CountDownLatchcountDownLatch=newCountDownLatch(3);
threadPool.submit(newTask(1, prices, countDownLatch));
threadPool.submit(newTask(2, prices, countDownLatch));
threadPool.submit(newTask(3, prices, countDownLatch));
countDownLatch.await(3, TimeUnit.SECONDS);
returnprices;
    }
privateclassTaskimplementsRunnable {
IntegerproductId;
Set<Integer>prices;
CountDownLatchcountDownLatch;
publicTask(IntegerproductId, Set<Integer>prices,CountDownLatchcountDownLatch) {
this.productId=productId;
this.prices=prices;
this.countDownLatch=countDownLatch;
        }
@Overridepublicvoidrun() {
intprice=0;
try {
Thread.sleep((long) (Math.random() *6000));
price=productId;
            } catch (InterruptedExceptione) {
e.printStackTrace();
            }
prices.add(price);
countDownLatch.countDown();
        }
    }
}

这段代码使用 CountDownLatch 实现了这个功能,整体思路和之前是一致的,不同点在于我们新增了一个 CountDownLatch,并且把它传入到了 Task 中。在 Task 中,获取完机票信息并且把它添加到 Set 之后,会调用 countDown 方法,相当于把计数减 1。

这样一来,在执行 countDownLatch.await(3, TimeUnit.SECONDS) 这个函数进行等待时,如果三个任务都非常快速地执行完毕了,那么三个线程都已经执行了 countDown 方法,那么这个 await 方法就会立刻返回,不需要傻等到 3 秒钟。

如果有一个请求特别慢,相当于有一个线程没有执行 countDown 方法,来不及在 3 秒钟之内执行完毕,那么这个带超时参数的 await 方法也会在 3 秒钟到了以后,及时地放弃这一次等待,于是就把 prices 给返回了。所以这样一来,我们就利用 CountDownLatch 实现了这个需求,也就是说我们最多等 3 秒钟,但如果在 3 秒之内全都返回了,我们也可以快速地去返回,不会傻等,提高了效率。

3.3 CompletableFuture

我们再来看一下用 CompletableFuture 来实现这个功能的用法,代码如下所示:

***@authoralan*@create2022-10-0515:59*/publicclassCompletableFutureDemo {
publicstaticvoidmain(String[] args) throwsException {
CompletableFutureDemocompletableFutureDemo=newCompletableFutureDemo();
System.out.println(completableFutureDemo.getPrices());
    }
privateSet<Integer>getPrices() {
Set<Integer>prices=Collections.synchronizedSet(newHashSet<Integer>());
CompletableFuture<Void>task1=CompletableFuture.runAsync(newTask(1, prices));
CompletableFuture<Void>task2=CompletableFuture.runAsync(newTask(2, prices));
CompletableFuture<Void>task3=CompletableFuture.runAsync(newTask(3, prices));
CompletableFuture<Void>allTasks=CompletableFuture.allOf(task1, task2, task3);
try {
allTasks.get(3, TimeUnit.SECONDS);
        } catch (Exceptione) {
e.printStackTrace();
        }
returnprices;
    }
privateclassTaskimplementsRunnable {
IntegerproductId;
Set<Integer>prices;
publicTask(IntegerproductId, Set<Integer>prices) {
this.productId=productId;
this.prices=prices;
        }
@Overridepublicvoidrun() {
intprice=0;
try {
Thread.sleep((long) (Math.random() *6000));
price=productId;
            } catch (InterruptedExceptione) {
e.printStackTrace();
            }
prices.add(price);
        }
    }
}

getPrices 方法中,我们用了 CompletableFuture 的 runAsync 方法,这个方法会异步的去执行任务。

我们有三个任务,并且在执行这个代码之后会分别返回一个 CompletableFuture 对象,我们把它们命名为 task 1、task 2、task 3,然后执行 CompletableFuture 的 allOf 方法,并且把 task 1、task 2、task 3 传入。这个方法的作用是把多个 task 汇总,然后可以根据需要去获取到传入参数的这些 task 的返回结果,或者等待它们都执行完毕等。我们就把这个返回值叫作 allTasks,并且在下面调用它的带超时时间的 get 方法,同时传入 3 秒钟的超时参数。

它的效果是,如果在 3 秒钟之内这 3 个任务都可以顺利返回,那么会立即响应结果

但是如果有某一个任务没能来得及在 3 秒钟之内返回,那么这个带超时参数的 get 方法便会抛出 TimeoutException 异常,会被我们给 catch 住。

这样一来它就实现了这样的效果:会尝试等待所有的任务完成,但是最多只会等 3 秒钟,在此之间,如及时完成则及时返回,如果超时则抛出异常丢弃。

站在巨人的肩膀上

  • 徐隆曦——《Java 并发编程核心 78 讲》
相关文章
|
6月前
|
Cloud Native Serverless API
微服务架构实战指南:从单体应用到云原生的蜕变之路
🌟蒋星熠Jaxonic,代码为舟的星际旅人。深耕微服务架构,擅以DDD拆分服务、构建高可用通信与治理体系。分享从单体到云原生的实战经验,探索技术演进的无限可能。
微服务架构实战指南:从单体应用到云原生的蜕变之路
|
6月前
|
监控 Cloud Native Java
Spring Boot 3.x 微服务架构实战指南
🌟蒋星熠Jaxonic,技术宇宙中的星际旅人。深耕Spring Boot 3.x与微服务架构,探索云原生、性能优化与高可用系统设计。以代码为笔,在二进制星河中谱写极客诗篇。关注我,共赴技术星辰大海!(238字)
1144 2
Spring Boot 3.x 微服务架构实战指南
|
8月前
|
负载均衡 监控 Java
微服务稳定性三板斧:熔断、限流与负载均衡全面解析(附 Hystrix-Go 实战代码)
在微服务架构中,高可用与稳定性至关重要。本文详解熔断、限流与负载均衡三大关键技术,结合API网关与Hystrix-Go实战,帮助构建健壮、弹性的微服务系统。
800 1
微服务稳定性三板斧:熔断、限流与负载均衡全面解析(附 Hystrix-Go 实战代码)
|
8月前
|
监控 Java API
Spring Boot 3.2 结合 Spring Cloud 微服务架构实操指南 现代分布式应用系统构建实战教程
Spring Boot 3.2 + Spring Cloud 2023.0 微服务架构实践摘要 本文基于Spring Boot 3.2.5和Spring Cloud 2023.0.1最新稳定版本,演示现代微服务架构的构建过程。主要内容包括: 技术栈选择:采用Spring Cloud Netflix Eureka 4.1.0作为服务注册中心,Resilience4j 2.1.0替代Hystrix实现熔断机制,配合OpenFeign和Gateway等组件。 核心实操步骤: 搭建Eureka注册中心服务 构建商品
1244 3
|
9月前
|
数据采集 监控 网络协议
基于aiohttp的高并发爬虫实战:从原理到代码的完整指南
在数据驱动时代,传统同步爬虫效率低下,而基于Python的aiohttp库可构建高并发异步爬虫。本文通过实战案例解析aiohttp的核心组件与优化策略,包括信号量控制、连接池复用、异常处理等,并探讨代理集成、分布式架构及反爬应对方案,助你打造高性能、稳定可靠的网络爬虫系统。
704 0
|
10月前
|
缓存 负载均衡 监控
微服务架构下的电商API接口设计:策略、方法与实战案例
本文探讨了微服务架构下的电商API接口设计,旨在打造高效、灵活与可扩展的电商系统。通过服务拆分(如商品、订单、支付等模块)和标准化设计(RESTful或GraphQL风格),确保接口一致性与易用性。同时,采用缓存策略、负载均衡及限流技术优化性能,并借助Prometheus等工具实现监控与日志管理。微服务架构的优势在于支持敏捷开发、高并发处理和独立部署,满足电商业务快速迭代需求。未来,电商API设计将向智能化与安全化方向发展。
540 102
|
10月前
|
缓存 NoSQL 算法
高并发秒杀系统实战(Redis+Lua分布式锁防超卖与库存扣减优化)
秒杀系统面临瞬时高并发、资源竞争和数据一致性挑战。传统方案如数据库锁或应用层锁存在性能瓶颈或分布式问题,而基于Redis的分布式锁与Lua脚本原子操作成为高效解决方案。通过Redis的`SETNX`实现分布式锁,结合Lua脚本完成库存扣减,确保操作原子性并大幅提升性能(QPS从120提升至8,200)。此外,分段库存策略、多级限流及服务降级机制进一步优化系统稳定性。最佳实践包括分层防控、黄金扣减法则与容灾设计,强调根据业务特性灵活组合技术手段以应对高并发场景。
2771 7
|
10月前
|
NoSQL Java 微服务
2025 年最新 Java 面试从基础到微服务实战指南全解析
《Java面试实战指南:高并发与微服务架构解析》 本文针对Java开发者提供2025版面试技术要点,涵盖高并发电商系统设计、微服务架构实现及性能优化方案。核心内容包括:1)基于Spring Cloud和云原生技术的系统架构设计;2)JWT认证、Seata分布式事务等核心模块代码实现;3)数据库查询优化与高并发处理方案,响应时间从500ms优化至80ms;4)微服务调用可靠性保障方案。文章通过实战案例展现Java最新技术栈(Java 17/Spring Boot 3.2)的应用.
854 9
|
10月前
|
缓存 监控 Cloud Native
Java Solon v3.2.0 高并发与低内存实战指南之解决方案优化
本文深入解析了Java Solon v3.2.0框架的实战应用,聚焦高并发与低内存消耗场景。通过响应式编程、云原生支持、内存优化等特性,结合API网关、数据库操作及分布式缓存实例,展示其在秒杀系统中的性能优势。文章还提供了Docker部署、监控方案及实际效果数据,助力开发者构建高效稳定的应用系统。代码示例详尽,适合希望提升系统性能的Java开发者参考。
506 4
Java Solon v3.2.0 高并发与低内存实战指南之解决方案优化
|
12月前
|
SQL 安全 测试技术
2025接口测试全攻略:高并发、安全防护与六大工具实战指南
本文探讨高并发稳定性验证、安全防护实战及六大工具(Postman、RunnerGo、Apipost、JMeter、SoapUI、Fiddler)选型指南,助力构建未来接口测试体系。接口测试旨在验证数据传输、参数合法性、错误处理能力及性能安全性,其重要性体现在早期发现问题、保障系统稳定和支撑持续集成。常用方法包括功能、性能、安全性及兼容性测试,典型场景涵盖前后端分离开发、第三方服务集成与数据一致性检查。选择合适的工具需综合考虑需求与团队协作等因素。
1840 24
下一篇
开通oss服务