用户在携程输入出发地和目的地,点击搜索按钮的那一刻,系统要在几百毫秒内完成数百个航司库存的查询、过滤、排序和报价组装。这背后牵涉的数据量和请求量非常惊人:热门航线的运价组合动辄数十万条,大促期间单秒搜索请求可能达到数十万级。如果把这些查询和计算都压在同一批服务上,要么响应慢到用户流失,要么成本高到无法承受。缓存与计算分离,正是应对这类大规模库存查询的经典解法。

为什么机票库存查询天然适合缓存计算分离
机票库存查询有一个非常鲜明的特点:读多写少,且同一份数据会被反复访问。比如北京飞上海这条航线,每天有成千上万的用户搜索,但航司侧的座位库存和运价变化频率相对有限。这种访问模式意味着,绝大多数查询结果是可以被复用的,没必要每次都穿透到最底层的计算逻辑。
另一个特点是计算本身很重。一次搜索不仅要查询直飞航班,还可能涉及中转组合拼接、不同舱位运价计算、税费叠加、退改签规则匹配等,单次全量计算可能消耗几十上百毫秒的CPU时间。如果把这部分计算和查询入口耦合在一起,高峰期任何一环变慢都会拖垮整个链路。把计算独立出来做成无状态集群,缓存层只负责快速回吐结果,两层各自独立扩缩容,整体吞吐能力就能线性提升。
多级缓存的设计与命中率优化
实际工程中很少只用一层缓存。携程这类平台通常会采用本地缓存加分布式缓存的多级结构。本地缓存部署在应用进程内,访问延迟在纳秒到微秒级,用来承接最热的那部分数据,比如热门城市对的基础航班列表;分布式缓存一般基于Redis集群,容量大、可共享,存放完整的搜索结果或中间数据。本地缓存命中率哪怕只提升百分之十,对下游的减压效果都非常可观。
缓存key的设计直接决定命中率。常见的做法是以搜索条件的核心维度(出发城市、到达城市、日期、舱位等级)拼装key,而对排序偏好、筛选条件这类个性化参数则不进key,而是在缓存结果上做二次加工。这样同一个城市对的不同用户可以共享同一份基础数据,命中率大幅提升。
缓存过期时间的设置需要精细权衡。机票价格存在时效性,缓存太久会返回过期价格引发投诉,缓存太短又失去意义。实践中常用的方案是差异化TTL:热门航线设置较短TTL配合主动刷新,冷门航线设置较长TTL,配合价格变更消息做精准失效。
计算层的无状态化与弹性伸缩
缓存分离出去之后,计算层就可以专心做重计算。前提是计算服务必须无状态化,任何节点不保存会话和本地数据依赖,所有状态要么放缓存,要么放存储。这样计算集群才能根据缓存未命中率驱动的负载指标自动扩容,大促前临时加几百个容器节点,活动结束后释放,成本可控。
计算层内部还会进一步拆分。比如把库存查询、运价计算、中转拼接拆成不同的服务,各自独立部署和伸缩。中转拼接通常是CPU消耗大户,因为它要组合两个航段的候选航班,组合空间呈乘积级增长,这类服务往往单独扩容并配合剪枝策略控制计算规模。
一致性、击穿与降级:落地时绕不开的坑
缓存与数据源之间的一致性是永恒的话题。机票场景下主要靠两条腿走路:一是航司侧的价格变更回调或轮询触发的主动失效,二是TTL兜底保证最坏情况下数据过期时间可控。对于价格这类敏感数据,宁可 miss 后回源计算,也不要冒险用过期数据。
缓存击穿和雪崩必须有预案。热点key失效瞬间大量请求同时回源,可能瞬间打垮计算层。常见手段包括:对回源请求加分布式互斥锁,同一key只放一个请求去计算,其余等待结果;对TTL增加随机抖动,避免大批key同时过期;对计算层设置熔断和排队,超出容量的请求降级返回缓存中的稍旧数据或精简结果。
| 问题场景 | 常见现象 | 应对策略 |
|---|---|---|
| 热点key击穿 | 某热门航线缓存失效,请求集中回源 | 互斥回源、逻辑过期、热点key永久缓存加异步刷新 |
| 缓存雪崩 | 大批key同时过期,计算层被打满 | TTL随机抖动、多级缓存、熔断降级 |
| 数据不一致 | 展示价格与实际支付价格不符 | 主动失效加TTL兜底、下单前二次校验 |
| 大促流量洪峰 | 瞬时请求量超出常态数倍 | 缓存预热、计算层弹性扩容、结果降级 |
写在最后
缓存与计算分离并不是什么新颖概念,但在机票搜索这种高并发、强时效、重计算的场景里,它依然是最务实的选择。核心思路说起来简单:让擅长的层做擅长的事,缓存层追求极致的命中率和低延迟,计算层追求无状态和弹性,中间用精细的失效策略和降级预案把两者粘合起来。对于正在做类似库存查询、商品搜索系统的团队来说,先想清楚数据的读写比例和时效要求,再决定缓存的粒度和分层方式,往往比盲目堆机器更有效。