Tomcat服务我们是通过Nginx来做负载均衡,用Lua脚本区分是国际航线还是国内航线,基于航线类型,Nginx会跳转不同搜索服务器:主要是国际搜索、国内搜索(基于业务、数据模型、商业模式,完全分开部署)。不光如此,Lua还用来敏捷开发一些基本服务:比如维护城市列表、机场列表等。
航班数据
上文我们一直提到航班数据,接下来简单介绍下航班的概念和基本类型,让大家有个印象,明白的同学可以跳过:
单程航班:也叫直达航班,比如BJ(北京)飞NY(纽约)。
往返航班:比如BJ飞NY,然后又从NY返回BJ。
带中转:有单程中转、往返中转;往返中转可以一段直达,一段中转。也可以两段都有中转,如下图:
其实,还有更复杂的情况:
如果哪天在BJ(北京)的你想来一次说走就走的旅行,想要去NY(纽约)。你选择了BJ直飞NY的单程航班。后来,你觉得去趟米国老不容易,想顺便去LA玩。那你可以先BJ飞到LA,玩几天,然后LA再飞NY。
不过,去了米国要回来吧,你也许:
NY直接飞回BJ。
突然玩性大发,中途顺便去日本,从NY飞东京,再从东京飞BJ。
还没玩够?还要从NY飞夏威夷玩,然后夏威夷飞东京,再东京飞首尔,最后首尔返回北京。
……有点复杂吧,这是去程中转、回程多次中转的航班路线。
对应国际航班还算非常正常的场景,比如从中国去肯尼亚、阿根廷,因为没有直达航班,就会遇到多次中转。所以,飞国外有时候是蛮有意思、蛮麻烦的一件事。
通过上面例子,大家了解到了机票中航线的复杂程度。但是,我们的缓存其实是有限的,它只保存了两个地方的航班信息。这样简单的设计也是有必然出发点:考虑用最简单的两点一线,才能较大限度上组合复杂的线路。
所以在前台搜索,我们还有大量工作要做,总而言之就是:
按照最终出发地、目的地,根据一定规则搜索出用户想要的航班路线。这些规则可能是:飞行时间最短、机票价格最便宜(一般中转就会便宜)、航班中转最少、最宜飞行时间。
你看,机票里面的航线是不是变成了数据结构里面的有向图,而搜索就等于在这个有向图中,按照一定的权重求出最优路线的过程!
高并发下多线程应用
我们后端技术栈基于Java。为了搜索变得更快,我们大量把Java多线程特性用到了并行运算上。这样,充分利用CPU资源,让计算航线变得更快。比如下面这样中转航线,就会以多线程方式并行先处理每一段航班。类似这样场景很多:
Java的多线程对于高并发系统有下面的优势:
JavaExecutor框架提供了完善线程池管理机制:譬如newCachedThreadPool、SingleThreadExecutor等线程池。
FutureTask类灵活实现多线程的并行、串行计算。
在高并发场景下,提供了保证线程安全的对象、方法。比如经典的ConcurrentHashMap,它比起HashMap,有更小粒度的锁,并发读写性能更好。线程安全的StringBuilder取代String、StringBuffer等等(Java在多线程这块实现是非常优秀和成熟的)。
高并发下数据传输
因为每次搜索机票,返回的航班数据是很多的:
包含各种航线组合:单程、单程一次中转、单程多次中转,往返更不用说了。
航线上又区分上百种航空公司的组合。比如北京到纽约,有美国航空,国航,大韩,东京等等各个国家的各大航空公司,琳琅满目。
那么,最早航班数据用标准的XML、JSON存储,不过随着搜索量不断飙升,CPU和带宽压力很大了。后来采取自己定义一种txt格式来传输数据:一方面数据压缩到原来30%~40%,极大的节约了带宽。同时CPU的运算量大大减低,服务器数量也随之减小。
在大用户量、高并发的情况下,是特别能看出开源系统的特点:比如机票的数据解析用到了很多第三方库,当时我们也用了Fastjson。在正常情况下,Fastjson确实解析很快,一旦并发量上来,就会越来越吃内存,甚至JVM很快出现内存溢出。原因呢,很简单,Fastjson设计初衷是:先把整个数据装载到内存,然后解析,所以执行很快,但很费内存。
当然,这不能说Fastjson不优秀,现在看GitHub上有8000多star。只是它不适应刚才的业务场景。
这里顺便说到联想到一个事:互联网公司因为快速发展,需要新技术来支撑业务。那么,应用新的技术应该注意些什么呢?我的体会是:
好的技术要大胆尝试,谨慎使用。
优秀开源项目,注意是优秀。使用前一定弄清他的使用场景,多做做压力测试。
高并发的用户系统要做A/B测试,然后逐步导流,最后上线后还要有个观察期。
后台搜索
后台搜索系统的核心任务是从外部的GDS系统抓取航班数据,然后异步写入缓存。