像 Booking.com 或 Airbnb 这样的酒店预订系统是一个常见的系统设计面试问题。
它看起来简单,直到它以最糟糕的方式崩溃。
两个用户在同一秒点击最后一个可用房间的”预订”。你的 API 做 SELECT 检查,所以两个请求都看到可用性,你为一张房间收取两张信用卡。接下来,你处理退款、愤怒的客户,以及一个无法信任的系统。
那不是 bug……那是如果你不正确设计它的默认结果。
这就是为什么 Airbnb 和 Booking.com 不仅仅是”表 + 端点”。
大多数开发者直接跳到盒子和箭头而不理解实际问题。他们错过并发陷阱,忘记双重预订,无法解释权衡。
在这期通讯中,我们将以面试和真实流量要求的方式设计它:库存的强一致性、搜索的快速读取,以及缓存和扩展的清晰权衡。
在我们设计任何东西之前,让我们放大,定义真正的问题,并逐步列出方法。
继续前进!
设计需求 - 如何范围系统
每个架构良好的系统设计从约束开始。
如果你跳过这一步,之后的所有都是猜测。
以下是我们将问的一些澄清问题关于设计需求。
规模
- 多少酒店?
- 总共多少房间?
- 单一连锁还是像 Booking.com 这样的平台?
支付时间
- 预订时支付还是入住时支付?
预订渠道
- 仅 Web?
- 移动应用?
- 电话预订?
取消
- 用户可以取消吗?
- 政策是什么?
超额预订
- 我们支持它吗?(酒店通常允许 5-10% 超额预订)
动态定价
- 房间价格根据需求/日期变化吗?
澄清这些约束后,我们现在可以定义系统实际必须做什么。
功能需求
- 酒店管理:管理员可以添加、更新和删除酒店和房间类型。他们调整价格和库存。
- 搜索和发现:用户可以按位置、日期、客人数量、价格范围和设施搜索酒店。
- 预订流程:用户可以预订房间、查看预订和取消预订。系统应防止双重预订。
- 支付处理:与支付网关集成。并优雅地处理故障。
- 通知:发送预订、取消和支付的确认。
非功能需求
- 强一致性:两个用户不能为相同日期预订同一房间。不可协商。
- 高并发:处理数千用户同时尝试预订同一酒店。
- 低延迟:搜索结果低于 500ms。预订确认在 2-3 秒内。
- 高可用性:99.9%+ 正常运行时间。
- 可扩展性:水平扩展而无需重新设计系统。
粗略估计计算
Booking Holdings 报告 2024 年超过 11 亿房间夜。
让我们将其转化为可用数字:
房间夜每年:11 亿
平均住宿:每次预订 2 晚
每年天数:365
每年预订数:
11 亿房间夜 / 每次预订 2 晚 ≈ 5.5 亿预订/年每天预订数:
5.5 亿 / 365 ≈ 150 万预订/天每秒预订数(TPS):
150 万 / 86,400 ≈ 17 预订/秒在 Booking.com 规模:
- 每天约 150 万预订
- 平均每秒约 17 预订
Similarweb 显示 booking.com 每月约 5.165 亿访问,每次访问约 7.56 页。
每天访问:
5.165 亿 / 30 ≈ 1700 万访问/天每秒访问:
1700 万 / 86,400 ≈ 200 访问/秒如果我们假设每次访问约 5 次后端读取(搜索查询、可用性检查、定价):
后端读取 QPS:
200 访问/秒 × 5 ≈ 1000 读取请求/秒关键洞察
- 这是强读重的
- 你处理约 1000 读取 QPS 和仅约 17 写入 TPS
- 你的架构应优化读取同时保持写入一致
库存表容量规划
对于库存表,按房间类型和日期跟踪可用性。
假设:
- 平均酒店入住率:70%
- 每年售出房间夜:11 亿
年度总容量:
11 亿 / 0.7 ≈ 15.7 亿房间夜/年平台上总房间数:
15.7 亿 / 365 ≈ 430 万房间如果平均酒店有 50 个房间:
- 全球约 85,000 家酒店
房间类型库存表行数:
- 酒店:约 85,000
- 每家酒店平均房间类型:10
- 范围:2 年 = 730 天
行数:
85,000 × 10 × 730 ≈ 6.2 亿行假设每行约 100 字节:
存储 ≈ 6.2 亿 × 100 字节 ≈ 62 GB 原始数据现实检查
带索引(通常是表大小的 2-3 倍)、相关表(预订、酒店、客人),你看起来像 500GB+ 总存储。
所以你需要适当的”分片和索引策略”。
理解规模和数据大小后,我们可以设计一个实际适合这些数字的架构……
高层设计
我们使用服务导向架构与清晰边界,但保持”紧密耦合”数据在同一数据库。

客户端层
- Web 和移动应用
- CDN 用于静态资产(图片、JavaScript、酒店照片)
API 网关
- 单一入口点
- 认证、速率限制、请求路由
- 在服务实例之间负载均衡
服务
- 酒店服务:酒店和房间信息。 mostly static,重缓存
- 费率服务:按日期的房间定价。基于需求的动态定价
- 预订服务:处理预订逻辑、库存检查和预订创建/取消。这是并发重要的地方
- 支付服务:与支付网关集成。更新预订状态
- 通知服务:发送电子邮件和推送通知
- 搜索服务:管理 Elasticsearch 集成用于酒店发现
数据层
- PostgreSQL:主事务存储。ACID 保证
- Redis:热数据缓存
- Elasticsearch:搜索功能(全文、地理空间、过滤)
架构决策
在这个规模上,纯单体或完整微服务设置将是次优的。
单一单体简化事务但难以独立扩展和部署。带独立数据库的完整微服务引入分布式事务和一致性问题,在这个流量水平不存在。
这个设计使用服务级分离与共享事务数据用于紧密耦合域(预订 + 库存)。它在应用层保持部署灵活性,同时在最重要的地方保持强一致性。
每个服务拥有其域并独立扩展。
酒店服务可能需要 10 个实例用于搜索流量,而预订服务只需要 3 个因为预订 TPS 低。
关键决策
预订和库存数据生活在同一数据库。
这让我们使用本地 ACID 事务而不是分布式事务。我们在应用层有服务级分离,但对紧密耦合实体共享数据。
这是务实的。
分布式事务为这里最小利益添加巨大复杂性。替代方案(带 saga 模式或 2PC 的独立数据库)是过度杀伤,当你可以将相关数据保持在一起时。
替代方案
将预订和库存分成独立数据库,并用 Saga 模式或两阶段提交(2PC)协调。
2PC 提供强一致性但增加延迟、阻塞和故障复杂性。协调器故障可能停滞系统。
Saga 模式避免锁定但引入最终一致性、补偿动作和复杂故障处理。这对用户面对预订流程有风险,其中”最终一致”意味着愤怒用户。
在约 17 写入 TPS,增加的复杂性不合理……本地 ACID 事务更简单、更安全、更快。
API 设计
所有操作的 RESTful API:
酒店管理
GET /v1/hotels/{id}- 获取酒店详情POST /v1/hotels- 添加新酒店(仅管理员)PUT /v1/hotels/{id}- 更新酒店信息(仅管理员)DELETE /v1/hotels/{id}- 删除酒店(仅管理员)
房间管理
GET /v1/hotels/{id}/rooms/{id}- 获取房间类型详情POST /v1/hotels/{id}/rooms- 添加房间类型(仅管理员)PUT /v1/hotels/{id}/rooms/{id}- 更新房间类型(仅管理员)DELETE /v1/hotels/{id}/rooms/{id}- 删除房间类型(仅管理员)
预订管理
GET /v1/reservations- 获取用户的预订历史GET /v1/reservations/{id}- 获取特定预订详情POST /v1/reservations- 创建新预订DELETE /v1/reservations/{id}- 取消预订
搜索
GET /v1/search- 参数:location、checkIn、checkOut、guests、beds、priceRange
- 返回:可用酒店和房间类型
幂等性键
POST /v1/reservations 请求包括幂等性键:
{ "idempotency_key": "reservation-uuid"}这个 reservationId 在前端生成并防止用户多次点击提交时双重预订。
处理并发
以下是关键洞察:
问题
双重预订发生在两个用户同时尝试预订同一房间时。
解决方案
数据库锁:
- 悲观锁:SELECT FOR UPDATE
- 乐观锁:版本号检查
原子操作:
UPDATE inventorySET available = available - 1WHERE room_id = ? AND date = ? AND available > 0队列系统:
- 串行化预订请求
- 确保顺序处理
分布式锁:
- Redis 锁
- Zookeeper 锁
本文为学习目的的个人翻译,译文仅供参考。
原文链接:Airbnb System Design。
版权归原作者或原刊登方所有。本文为非官方译本;如有不妥,请联系删除。