system-design-notes第22章:设计酒店预订系统完整指南
【免费下载链接】system-design-notesNotes of the book System Desgin Interview - An Insider's Guide项目地址: https://gitcode.com/GitHub_Trending/sy/system-design-notes
本文是 system-design-notes 第 22 章的完整指南,带你用系统设计的标准流程——需求分析、容量估算、数据建模、并发控制、分片扩展与数据一致性——一步步设计一个酒店预订系统。酒店预订系统是面试高频题,同一套思路也适用于 Airbnb、机票预订、电影售票等场景。读完本文,你将掌握避免"重复预订"、数据库分片和微服务一致性的实用方案。
🎯 第1步:先弄清需求,明确酒店预订系统的边界
在动手设计前,先和面试官确认范围。第22章给出的核心设定:
- 一个拥有5000 家酒店、100 万间客房的酒店集团网站
- 用户在预订时全额付款,仅通过网站/App 下单,支持取消预订
- 允许10% 的超售(overbooking)——酒店会多卖房间,赌有人会取消
- 房价每天变化,价格与当日入住率相关
非功能性需求:高峰期大量用户可能同时订同一家酒店,系统要支持高并发;预订延迟中等即可(几秒内完成可以接受)。
💡 面试技巧:先问清楚"预订的是具体房间还是房型",这会直接影响数据模型设计。
用"纸面估算"算清系统规模
假设 70% 客房被占用、平均住 3 天,则每日约100万 × 0.7 / 3 ≈ 24 万笔预订,折算平均TPS 只有 3 左右——写压力很小。但页面浏览量按漏斗反推:

若 3 笔预订、每页 10% 转化率,则预订详情页约 30 次/秒、房型页约 300 次/秒。读多写少,这个特征决定了后面的数据库选型。
🏗️ 第2步:高层设计——API、数据模型与微服务架构
酒店预订系统的 RESTful API 设计
核心接口分三组(详见 22. Hotel Reservation System/README.md):
| 分组 | 关键接口 |
|---|---|
| 酒店 | GET /v1/hotels/{id}、POST /v1/hotels(仅运营) |
| 房间 | GET /v1/hotels/{id}/rooms/{id} |
| 预订 | POST /v1/reservations、DELETE /v1/reservations/{id} |
注意预订请求中带有一个reservationID字段,它是幂等键,后文解决重复预订时要用到。
为什么选关系型数据库
- 读多写少,关系型数据库(如 MySQL)正合适
- 预订涉及金钱,必须靠ACID 事务防止重复扣款、负数库存
- 酒店-房型-预订的结构非常清晰,关系模型好表达
初始表结构包含 hotel、room、reservation 三张表,其中status字段描述房间状态机:

但这个初版模型有个缺陷:用户预订的是房型而非具体房间号,房间号应在下单时才分配。后面第3步会修正它。
微服务架构图:六个服务的职责划分

- 公共 API 网关:负责限流、认证(限流方案可参考 04. Rate Limiter/Readme.md)
- 酒店服务 / 价格服务:酒店和房型数据变化慢,可激进缓存;价格服务按未来日期提供房费
- 预订服务:接收预订并管理房间库存
- 支付服务:处理付款,成功后更新预订状态
- 服务间通信用 gRPC 这类 RPC 框架
📊 第3步:改进数据模型——按"房型"预订才是关键
修正后的 API 不再传roomID,而是传roomTypeID+ 数量roomCount。更新后的表结构如下:

新增的room_type_inventory表是精髓,每行记录(酒店, 房型, 单一日期)的库存:
total_inventory:该房型总房间数(扣除临时下架的)total_reserved:该日期已预订数- 判断是否有房:
total_reserved + 要订数 ≤ 110% × total_inventory(支持 10% 超售)
行由每日 CRON 任务预生成。按 5000 家酒店 × 20 种房型 × 2 年 × 365 天估算,仅7300 万行,单台数据库服务器就能扛住,加读写副本即可保证高可用。若预订表膨胀,可按hash(hotel_id)分库分表(所有查询都带 hotel_id,分片键天然合适),历史数据可归档到冷存储。
⚔️ 第4步:并发难题——防止重复预订的3种实战方案
这是本章最核心的考点。要解决两个问题:同一用户连点两次"预订";多个用户同时抢同一房型。
问题一:同一用户双击预订
前端禁用按钮只是权宜之计,可靠做法是幂等 API:

流程:用户填信息时先生成一个全局唯一的reservation_id;无论点击几次"完成预订",提交的都是同一个 id;数据库对该列建唯一约束,重复记录直接写入失败:

问题二:多用户同时抢订
在低隔离级别下,两个事务可能都看到"还有房",双双写入成功,造成超售:

三种解法对比:
方案1:悲观锁(Pessimistic Locking)
用SELECT ... FOR UPDATE在更新前锁住行。

- ✅ 实现简单,串行化更新,适合高争用场景
- ❌ 长事务会阻塞其他所有事务,可能死锁,扩展性差
方案2:乐观锁(Optimistic Locking)⭐ 本章推荐
给表加version列,更新时校验版本号并 +1,失败则重试。

- ✅ 不加数据库锁,性能好;避免基于过期数据修改
- ❌ 高争用下大量回滚,性能下降
- 酒店预订 TPS 只有个位数,争用低,乐观锁是最佳选择(建议用版本号而非时间戳,服务器时钟可能不准)
方案3:数据库约束
直接用 CHECK 约束兜底:total_inventory - total_reserved >= 0。

- ✅ 最简单,小争用场景表现好
- ❌ 部分数据库不支持,约束难以像代码一样版本管理
💡 面试加分点:说明"乐观锁 + 唯一约束"组合拳——幂等键防单人重复,版本/约束防多人竞态,数据库约束做最后防线。
🚀 第5步:可扩展性——数据库分片 + Redis 缓存
若系统被 booking.com 级别的流量采用(QPS 放大 1000 倍),瓶颈在哪?
所有服务都是无状态,直接横向复制即可(原理见 01. Scaling/Readme.md)。真正难扩展的是有状态的数据库,解法是分片:

假设 QPS 达 30,000,拆成 16 个分片后每片仅 1875 QPS,在单 MySQL 集群能力之内。
再叠加Redis 库存缓存,库存 key 按hotelID_roomTypeID_{date}存储:

- 缓存与数据库通过CDC 流式机制(如 Debezium)异步同步
- 短暂不一致没关系:最终写入时数据库约束会拦截非法预订,最坏情况只是用户刷新页面看到"没房了"
- 为已过期日期设置 TTL 自动清理
缓存的代价是一致性难维护,需评估其对用户体验的影响——这是经典的缓存与一致性权衡(可延伸阅读 06. Key-Value Store/Readme.md)。
🧩 第6步:微服务间的数据一致性权衡
单体应用可以用同一个关系型数据库的事务保证原子性;而拆成微服务后,跨服务操作不再原子:

本章的务实选择是混合架构:预订和库存 API 放在同一个服务内,继续享受 ACID 事务的红利,避免不必要的复杂度。若坚持纯微服务(每服务独立数据库),则要用:
- 两阶段提交(2PC):跨节点原子提交,但一个节点卡顿会阻塞所有节点,性能差
- Saga:一系列本地事务 + 失败时的补偿事务,属于最终一致方案
⚖️ 核心思想:跨服务一致性会显著抬高系统复杂度,先问"值不值得",能用单库事务解决的别上分布式协议。
✅ 本章核心要点速记
| 步骤 | 关键决策 |
|---|---|
| 需求与估算 | TPS≈3 读多写少;支持 10% 超售 |
| 技术选型 | MySQL(ACID)+ 微服务 + gRPC |
| 数据模型 | 按"房型+日期"库存表,CRON 预生成 |
| 并发控制 | 幂等键 + 乐观锁 + 唯一约束 |
| 扩展 | hotel_id 分片 + Redis 库存缓存 + CDC 同步 |
| 一致性 | 混合架构优先,必要时 Saga / 2PC |
延伸阅读:容量估算方法见 02. Back Of the Envelope Estimation/Readme.md,本章完整源码笔记见 22. Hotel Reservation System/README.md。把这套"估算 → 建模 → 防竞态 → 分片 → 一致性"的流程记熟,机票、门票、演出订票类面试题都能直接复用。
【免费下载链接】system-design-notesNotes of the book System Desgin Interview - An Insider's Guide项目地址: https://gitcode.com/GitHub_Trending/sy/system-design-notes
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考