内容更新:

酒店预订平台是一个在关键路径上有严格正确性要求的双边市场:双重预订一个房间是严重的用户体验失败,而错误地阻塞可用库存损失收入。核心设计挑战是在全球用户群跨多个日历天的住宿下、在必须挺过网络故障、支付超时和用户放弃的结账流程中,维护准确的房间可用性——全都绝不把同一晚卖给两个不同客人。除了那个正确性挑战,平台还必须跨数十万家酒店服务快速、过滤、地理感知的搜索结果,管理每酒店复杂的取消和退款政策,并近实时给酒店运营者提供业务分析。

本文目录
  1. 第 1 步 — 澄清需求
  2. 第 2 步 — 容量估算
  3. 第 3 步 — API 设计
  4. 第 4 步 — 酒店和房间列表服务
  5. 第 5 步 — 数据模型
  6. 第 6 步 — 搜索服务
  7. 第 7 步 — 预订服务与并发控制
  8. 第 8 步 — 结账期间防超卖
  9. 第 9 步 — 预订状态机
  10. 第 10 步 — 支付处理与幂等
  11. 第 11 步 — 取消与退款
  12. 第 12 步 — 可用性日历
  13. 第 13 步 — 分析与报告
  14. 第 14 步 — 扩展与容错
  15. 第 15 步 — 为什么不用 Cassandra 做预订?
  16. 第 16 步 — 关键取舍
⚡ 速览要点
  • 全程 SQL——酒店/房间目录有界且结构化;预订需要 ACID;量不足以证明 NoSQL 复杂度合理。
  • DB 级约束防超卖——room_availability 表上的 CHECK (available_count >= 0);并发递减到 -1 在数据库失败,而非应用层。
  • 低争抢用乐观锁——用一个 version 列;冲突时重试;用户搜索和预订确认之间不持行级锁。
  • SQL 预留与过期回收——数据库保存截止时间并扣减库存;定时任务幂等释放,Redis 仅镜像预留状态。
  • Kafka → Elasticsearch——酒店/房间创建或更新事件异步喂搜索索引;售罄房间自动从结果移除。
  • 幂等预订 API——客户端提供的幂等键防止重试请求(双击、网络重试)的重复预订。
  • S3 + CDN 做媒体——酒店图片住在对象存储;CDN 边缘节点以低延迟全球服务它们。
tldr

全程用 SQL 以求 ACID 正确性——酒店和房间数据大小有界,且预订需要事务完整性。available_count 上的行级约束在数据库级别防超卖。SQL 预留桥接结账间隙,数据库定时扫描负责回收过期库存,Redis 仅用于加速。Kafka 把列表变更喂进 Elasticsearch 做搜索。CDN 全球分发酒店媒体。取消和退款是它们自己的幂等状态机路径。

酒店预订应用高层架构
酒店预订应用高层架构

第 1 步 — 澄清需求

画框前界定系统。酒店预订覆盖很广——把它收窄到一个可辩护、连贯的子集。

功能需求(酒店管理)

  • 酒店和房间的 CRUD 操作(名、描述、设施、位置、图片)。
  • 设房间定价——基础费率、季节费率、最少住宿、折扣规则。
  • 管理可用性日历——封锁特定日期、设每晚房间数。
  • 查看和管理预订:确认、取消、修改、处理 no-show。
  • 访问业务洞察:入住率、收入、预订提前期、取消率。

功能需求(用户)

  • 按城市/位置、入住和退房日期、客人数、过滤器(评分、设施、价格区间)搜索酒店。
  • 查看酒店详情:照片、房型、设施、可用性日历、评论。
  • 预订房间:选日期、客人数、附加项;用支付确认。
  • 查看和管理自己的预订:看确认、改日期、取消。
  • 在预订确认、提醒和取消时收 email/push 通知。

非功能需求

  • 高一致性——无双重预订;可用性计数必须始终正确。
  • 低延迟——搜索和酒店详情页 200 ms p99 内;结账须感觉即时。
  • 高可用——预订服务宕意味损失收入;目标 99.99%。
  • 可扩展——处理全球用户负载和季节性高峰(夏季、假日、大型活动)。
  • 持久性——一个已确认预订绝不丢失。
面试提示

显式划出范围:忠诚度计划、第三方 OTA(在线旅行社)集成、动态房间升级逻辑、欺诈检测。这些是真实产品功能,但 45 分钟吃完整范围会让难的部分——并发控制和预订状态机——没时间。

第 2 步 — 容量估算

动手估算:用 QPS、带宽与存储计算器调整流量假设,区分峰值吞吐与每日新增数据量。

为面试估算设定以下假设:平台收录 50 万家酒店,每天处理 100 万次预订。这不是对 Booking.com 或 Expedia 实际规模的声明。

流量

  • 搜索请求:假设 50M DAU × 5 搜索/天 = 250M 搜索/天 ≈ ~2,900 搜索/秒平均;峰值(周五下午、假日季)5× → ~14,500 搜索/秒。
  • 预订写:1M 预订/天 ÷ 86,400 ≈ ~12 预订/秒平均;闪促期间峰值可能 10× → ~120 预订/秒。带恰当索引的 SQL 可管理。
  • 读写比 ≈ 250:1——搜索的重缓存必不可少;预订写相对罕见但必须完美正确。

存储

  • 酒店:假设平台收录 ~50 万家 × 每条 ~5 KB(元数据、坐标、设施标志)≈ ~2.5 GB——很小,轻易装进 SQL 还有余。
  • 房间:~3M 房型 × ~2 KB ≈ ~6 GB。
  • 可用性表:3M 房型 × 365 天 × 每行 ~20 字节 ≈ ~22 GB/年——仍 SQL 可管理;按日期分区使查询快。
  • 预订:1M/天 × ~500 字节 × 365 天 ≈ ~180 GB/年预订记录——单个 SQL 实例舒适;需要时分片。
  • 媒体:酒店照片/视频在 S3——为 PB 级规划;不在 DB。
注解

数据量确认 SQL 是正确的主存储——没有规模理由用 NoSQL 复杂度。难问题不是存储而是可用性表上的并发写正确性。

第 3 步 — API 设计

跨酒店管理和面向用户服务的干净 REST 面:

REST API
# 酒店管理 — 列表管理
POST   /hotels              -- 创建酒店
PUT    /hotels/{id}         -- 更新酒店
GET    /hotels/{id}         -- 获取酒店详情
PUT    /hotels/{id}/rooms/{room_id}   -- 更新房间
GET    /hotels/{id}/rooms             -- 列出酒店房间
PUT    /rooms/{room_id}/availability  -- 按日期范围设 available_count

# 用户 — 搜索与发现
GET    /hotels/search?city=Paris&checkin=2025-07-01&checkout=2025-07-05&guests=2&max_price=200
GET    /hotels/{id}/availability?checkin=...&checkout=...

# 用户 — 预订(经 Idempotency-Key 头幂等)
POST   /bookings
       Idempotency-Key: "client-uuid-123"
       { hotel_id, room_id, checkin, checkout, guests, payment_method_id }
       → 201 { booking_id, status: "RESERVED", hold_expires_at }

GET    /bookings/{id}       -- 预订状态和详情
DELETE /bookings/{id}       -- 取消预订(触发退款)
PUT    /bookings/{id}/modify -- 改日期(若酒店政策允许)

POST /bookings 端点携带一个 Idempotency-Key 头。用同键重试的请求(双击、网络超时)返回已存在的预订而非创建第二个。这必不可少,因为预订流程涉及一次支付扣款——没有幂等,客户端和服务器间的网络故障能导致给客人卡上扣两次。

第 4 步 — 酒店和房间列表服务

酒店和房间目录存在一个 SQL 数据库。理由刻意简单:世界上酒店数量有限且可预测地增长。关系数据库是正确契合——数据结构化、增长可预见、查询模式(按 ID 查酒店、按酒店列房间)很适合 SQL 索引。

酒店图片和视频存在 AWS S3(或等价对象存储)。SQL 数据库持有这些媒体资产的引用(URL)。一个 CDN 坐在 S3 前以低延迟服务全球用户群——从地理上邻近的 CDN 边缘节点取酒店图片比命中 us-east-1 的源桶快数个数量级。

酒店或房间被创建或更新时,变更作为事件发布到 Kafka。一个消费者读这些事件并异步更新 Elasticsearch 搜索索引,使搜索结果与真相来源最终一致。售罄房间随 available_count 降到零从结果移除——这个移除流经同一 CDC 管道,而非预订路径上的直接 Elasticsearch 写。

第 5 步 — 数据模型

核心表跨酒店目录和预订系统。全住在 SQL 以求 ACID 正确性:

SQL schema
CREATE TABLE hotels (
  id            BIGINT PRIMARY KEY,
  name          VARCHAR(255) NOT NULL,
  city          VARCHAR(100),
  country       VARCHAR(2),     -- ISO 3166-1 alpha-2
  lat           DECIMAL(9,6),
  lng           DECIMAL(9,6),
  star_rating   SMALLINT,
  description   TEXT,
  amenities     JSONB,            -- 泳池、wifi、停车、宠物友好...
  created_at    TIMESTAMP
);

CREATE TABLE rooms (
  id            BIGINT PRIMARY KEY,
  hotel_id      BIGINT REFERENCES hotels(id),
  room_type     VARCHAR(64),    -- STANDARD|DELUXE|SUITE
  max_guests    SMALLINT,
  base_rate_cents INT,
  description   TEXT
);

CREATE TABLE room_availability (
  room_id         BIGINT REFERENCES rooms(id),
  date            DATE,
  available_count INT NOT NULL CHECK (available_count >= 0),
  price_cents     INT NOT NULL, -- 这个特定日期的每晚费率
  version         INT NOT NULL DEFAULT 0, -- 乐观锁
  PRIMARY KEY (room_id, date)
);

CREATE TABLE bookings (
  id              BIGINT PRIMARY KEY,
  user_id         BIGINT NOT NULL,
  room_id         BIGINT NOT NULL,
  checkin_date    DATE   NOT NULL,
  checkout_date   DATE   NOT NULL,
  guest_count     SMALLINT,
  total_cents     BIGINT NOT NULL,
  status          VARCHAR(32),  -- RESERVED|BOOKED|CANCELLED|COMPLETED
  idempotency_key VARCHAR(64) UNIQUE,
  payment_intent_id VARCHAR(128),
  hold_expires_at TIMESTAMP,   -- 权威预留截止时间;Redis 仅镜像
  created_at      TIMESTAMP,
  updated_at      TIMESTAMP
);

关键 schema 决策:CHECK (available_count >= 0) 约束是防超卖的数据库级守卫。version 列支持乐观并发控制。idempotency_key UNIQUE 约束在 DB 级别防止重复预订。room_availability 上的 price_cents 列捕获预订时的每晚费率——价格在预订后能变,但客人确认的费率锁在预订记录的 total_cents 里。

酒店和房间数据库 schema
酒店和房间数据库 schema

第 6 步 — 搜索服务

搜索由一个 Elasticsearch 集群驱动(Solr 是可比替代——两者都建在 Apache Lucene 上)。对列表持续更新(价格变化、可用性、新酒店)的酒店平台,Elasticsearch 因其实时索引和内建地理距离查询是更好契合。

特性ElasticsearchSolr
最适合时序数据、实时索引、地理查询带重缓存的静态数据集
模糊搜索优秀(Levenshtein 自动机)良好
地理距离过滤原生 geo_point 类型 + 距离过滤经空间模块支持
Type-ahead内建 completion suggester经 edge n-gram 支持

搜索查询流程

一个典型搜索——"巴黎的酒店,2 个成人,7 月 1–5,最高 €200/晚"——执行如下:

  1. Elasticsearch 查询——按 city = "Paris"(或从"Paris"中心点 geo_distance)、最低星级、设施过滤。返回按相关性分(混合文本匹配、热度、转化率)排序的酒店 ID。
  2. 可用性过滤——对每个返回的酒店,查 room_availability SQL 表以验证至少一个房型对请求范围内每晚有 available_count > 0 且 price_cents <= 200 × 100。过滤掉无合格房间的酒店。
  3. 价格计算——对合格酒店,跨住宿日期求和每晚 price_cents,应用促销和忠诚折扣,算出显示总价。
  4. 返回结果——带缩略图、每晚费率、住宿总价、可用性徽章的分页酒店列表。
搜索缓存

热门城市+日期组合(如"巴黎 7 月 4 日周末")的搜索结果能在 Redis 缓存配短 TTL(60–120 s)。可用性变化使受影响酒店 ID 的缓存失效。这显著减少高流量搜索查询的 Elasticsearch 和 SQL 负载,而不长时间服务陈旧可用性数据。

第 7 步 — 预订服务与并发控制

预订服务用 SQL 数据库求其 ACID 保证——双重预订不可接受,而 ACID 事务是防止它的干净方式。核心挑战是多晚可用性:5 晚住宿需要 room_availability 里五个独立行 available_count > 0,且所有五个递减必须原子地全成或全败——没有有效的部分预订。

悲观锁

短事务内用 SELECT ... FOR UPDATE 锁住全部住宿日期的库存行,再扣减并提交,是有效的悲观锁方案。外部支付调用必须在提交后执行;是否持锁等待支付不是悲观锁本身的要求。

乐观锁(首选)

首选方法用 room_availability 上的 version 列做乐观并发控制。用户搜索和预订确认之间不持锁——事务只在实际写的那一刻打开:

SQL
-- 第 1 步:读当前状态(无锁)
SELECT date, available_count, version
FROM room_availability
WHERE room_id = 42
  AND date BETWEEN '2025-07-01' AND '2025-07-04';
-- → 返回 4 行,version=7,8,7,9,available_count=3,3,2,3

-- 第 2 步:开事务;对每个日期尝试条件递减
BEGIN;
UPDATE room_availability
  SET available_count = available_count - 1,
      version = version + 1
  WHERE room_id = 42
    AND date = '2025-07-01'
    AND version = 7        -- 乐观锁:若行被修改则失败
    AND available_count > 0; -- 防止变负
-- 对住宿所有日期重复
-- 若任何 UPDATE 返回 0 行 → ROLLBACK(有人抢先)
-- 若全成功 → INSERT 预订记录 → COMMIT

若 version 检查失败(另一个并发预订修改了其中一行),事务立即回滚,服务用新数据重试。低争抢下(大多数晚),首次尝试成功。高争抢下(热门酒店在高峰周末),服务可能重试 1–3 次再成功或返回"房间不再可用"错误。CHECK (available_count >= 0) 约束是数据库级别的第二道防线——即便应用逻辑有 bug,DB 也拒绝任何低于零的递减。

为什么不跨服务 2PC?

跨预订和支付服务的两阶段提交会给原子结账——要么都成要么都不成。但 2PC 在整个事务期间跨服务持锁,造成协调者瓶颈并在负载下使可用性暴跌。选的方法(SQL 预留 + 乐观锁 + 异步支付确认)用严格原子性换高可用,对失败情况带显式补偿动作。

第 8 步 — 结账期间防超卖

交互练习:打开 并发预订与迟到支付案例,逐步比较旧值覆盖与原子扣减,并推演过期任务延迟、重复支付回调和退款去重。

预留以 SQL 为准:在同一事务中扣减每晚库存,并写入带 hold_expires_at 的 RESERVED 预订。Redis 可以镜像这个截止时间以加速读取,但 Redis 键消失不会自动恢复 SQL 库存。

  1. 预留:校验所有住宿日期,按日期顺序更新库存,并将预订记录一起提交。任意一晚缺失或条件更新失败,都回滚整个预留。
  2. 支付:提交事务后,用该支付操作固定的幂等键调用支付渠道。本文采用预订时付款;外部调用期间不持有数据库锁。
  3. 确认:验证回调签名、支付归属、金额和币种。在事务中锁定预订行,仅当数据库中的预留尚未过期时,才从 RESERVED 转为 BOOKED;事件去重记录和确认事件的 outbox 记录也在这个事务中写入。
  4. 过期:定时任务扫描到期预订并锁定预订行。只有成功将 RESERVED 转为 HOLD_EXPIRED 的任务,才能在同一事务中回补每晚库存。取消流程也遵循先锁预订、再按日期更新库存的顺序。

Redis 过期通知在订阅断连期间可能丢失,不能充当可靠的调度器。通知只能触发一次 SQL 检查,数据库定时扫描仍是恢复机制。

第 9 步 — 预订状态机

确认与过期争用同一条预订记录,通过行锁串行决定谁先完成转换。重复回调或取消请求不能再次回补库存。

  • RESERVED → BOOKED:支付验证通过且预留有效;库存已在预留时扣减,不再重复扣减。
  • RESERVED → HOLD_EXPIRED:超过截止时间;仅恢复一次库存。
  • RESERVED / BOOKED → CANCELLED_BY_USER:验证取消政策后,恢复符合条件的库存,并记录必要的退款操作。
  • BOOKED → COMPLETED:住宿完成,不释放未来库存。
预订过期后支付才成功

如果过期流程先完成,迟到的支付成功回调不能直接恢复预订,因为房间可能已被其他客人订走。应记录支付结果,写入幂等退款任务并通知用户。另一种策略是在重新原子预留全部住宿日期成功后确认;不能仅凭支付成功就确认。重复回调应复用已有退款操作。

状态变更与待发送事件一同写入事务 outbox,再由后台转发到 Kafka,避免数据库提交后、发送消息前进程崩溃造成事件丢失。消费端仍需要去重。

第 10 步 — 支付处理与幂等

本文主流程采用预订时收款。到店付款需要另行定义授权、扣款与预订确认的时机;仅创建 PaymentIntent 不会完成卡授权或扣款。

  • 预订请求:将幂等键绑定到已认证用户和请求指纹。同键但日期或金额不同应报错;合法重试返回同一预订。
  • 渠道操作:持久化 PaymentIntent ID,并为每次创建、扣款或退款操作保存固定的幂等键。超时代表结果未知,应查询状态或重试同一操作,不能直接创建新扣款。
  • 异步回调:验证签名,将已处理事件 ID 与本地状态变更放入同一事务。Stripe 回调可能重复或乱序,状态条件也要拒绝过时的转换。

若采用授权与扣款分离,需配置 capture_method=manual,确认支付后,在实际 capture_before 截止时间之前扣款。不能假定授权一直有效到数月后的入住日。参见 Stripe 授权与扣款流程。

恢复未知支付结果

按预留有效期选择对账频率,另以低频审计兜底。核对渠道状态与有效、已过期预订,并复用回调相同的条件确认或退款路径。支付已扣款不等于房间仍为该用户保留。

第 11 步 — 取消与退款

先校验用户权限与预订时保存的取消条款。只有成功转换状态才能释放房间。下面的 PostgreSQL 示例把状态转换和库存回补关联起来:重复请求的 cancelled 结果为空,不会修改库存。

WITH cancelled AS (
  UPDATE bookings
  SET status = 'CANCELLED_BY_USER', updated_at = NOW()
  WHERE id = :booking_id AND status IN ('RESERVED', 'BOOKED')
  RETURNING room_id, checkin_date, checkout_date
)
UPDATE room_availability AS a
SET available_count = a.available_count + 1,
    version = a.version + 1
FROM cancelled AS c
WHERE a.room_id = c.room_id
  AND a.date >= c.checkin_date AND a.date < c.checkout_date;

这个语句仅演示库存释放。生产事务还应在需要退款时写入唯一退款操作或 outbox 记录;后台任务使用原幂等键重试,并将退款完成状态与预订取消状态分开记录。事务失败时,状态与库存一起回滚。参见 PostgreSQL 数据修改型 WITH 查询。

第 12 步 — 可用性日历

可用性日历是驱动搜索过滤和房间详情视图("给我看这个房型哪些日期可用")的数据结构。带 (room_id, date) 主键的 room_availability 表是权威来源。对面向用户的日历 UI,一个预计算缓存必不可少:

  • 把每房间未来 90 天的可用性作有序集合或紧凑位集(每天一位,90 天 = ~12 字节)缓存在 Redis。查找 O(1)。
  • 每当一个房间的可用性变化(预订、取消、酒店管理更新)使其缓存条目失效。
  • 酒店详情页取缓存日历——对热门酒店通常是完整缓存命中,因为可用性变化比浏览少。
多晚可用性检查

"巴黎,7 月 1–5"的搜索必须验证至少一个房型在所有四晚(7 月 1、2、3、4)有可用性。一个带 GROUP BY 和 HAVING count = 4 的 room_availability SQL 查询正确工作。为规模化的搜索时性能,预计算每房间的"从日期 X 到日期 Y 可用"范围并用日期范围字段在 Elasticsearch 索引它们。这允许 Elasticsearch 做过滤而无需每个搜索命中都打 SQL DB。

第 13 步 — 分析与报告

分析服务从 Kafka 消费两个事件流:搜索动作(用户搜索什么、点击哪些结果)和预订交易(预订、取消、收入)。这些馈送驱动酒店管理仪表盘和平台级报告:

  • 入住率——每房型每周期 已预订晚 ÷ 总可用晚。
  • 收入趋势——每酒店每日/周/月收入,按房型细分。
  • 预订提前期——(入住日期 − 预订日期)的分布;告诉酒店主客人提前多久预订并指导定价策略。
  • 取消率——按取消政策类型分段;识别哪些政策驱动收入留存。
  • 搜索到预订漏斗——多少搜索导致酒店详情视图 → 持有 → 确认预订;识别流失点。

分析事件从 Kafka 流入一个数据仓库(ClickHouse 或 Redshift),SQL 聚合在那运行而不碰运营 DB。酒店管理仪表盘查仓库;运营预订服务从不用于分析查询。

第 14 步 — 扩展与容错

酒店预订读主导(搜索远超预订)但写关键(预订写必须正确)。各自的扩展策略不同:

搜索扩展

  • Elasticsearch 横向可扩展——加 shard 给更大索引、加副本给更高读吞吐。
  • 在 Redis 缓存热门搜索查询(城市 + 日期范围)配 60–120 s TTL。大多数搜索流量高度重复(同样热门城市、同样热门日期)。
  • 可用性过滤(Elasticsearch 后的 SQL 查找)是重搜索负载下的瓶颈。用读副本和 Redis 里预计算的可用性缓存缓解。

预订写扩展

  • ~12 预订/秒平均(120 峰值),单个 SQL 主库舒适处理负载。乐观锁让事务短——无长持行锁。
  • 若写规模成关切:按 hotel_id 哈希分片 room_availability 表。一个酒店的所有可用性写去一个分片;预订无需跨分片 join(一个客人一次预订一个酒店)。
  • 预订 bookings 与对应库存按 hotel_id 共置,保留本地事务;“我的预订”用按用户构建的查询投影。

容错

  • Redis 故障——回退到 SQL 读取预留状态。库存预留、截止时间和回收本就持久化在数据库,不依赖 Redis 可用性。
  • 支付处理器超时——沿用支付对象 ID 与操作幂等键查询或重试;结果未明时显示处理中,并按预留期限及时对账。
  • Kafka 故障——预订继续;Elasticsearch 略落后(搜索可能显示售罄房间直到 Kafka 恢复、索引追上)。DB 始终是真相来源——Elasticsearch 是下游投影。
  • DB 主库故障——自动故障转移到备用副本(如带 Patroni 的 PostgreSQL 或 Amazon RDS Multi-AZ)。故障转移期间短暂只读窗口;新预订排队并在新主库选出时重试。

第 15 步 — 为什么不用 Cassandra 做预订?

不像电商订单历史(高量、追加重、适合 Cassandra 归档),酒店预订量低到一个 SQL 主库能保留完整预订历史而无需专用归档服务。在此数据量,ACID 事务的正确性好处胜过 NoSQL 的可扩展好处。具体地:

  • 1M 预订/天 × 365 天 × 500 字节 ≈ 180 GB/年——很好地在 SQL 的甜点内。
  • ACID 多行更新(跨多个日期递减可用性 + 在一个事务里插入预订记录)不被 Cassandra 的轻量事务原生支持(LWT 是每分区且受限)。
  • Cassandra 的最终一致模型会需要复杂的应用级冲突解决来防止双重预订——正是 SQL 行级约束平凡解决的问题。

第 16 步 — 关键取舍

决策选择接受的取舍
主存储全程 SQL比 NoSQL 更低横向写规模;由自然低的预订写率缓解
并发控制乐观锁冲突时需重试;热门房间高争抢下重试率飙升——加指数退避
搜索索引Elasticsearch(最终)新预订房间在搜索里可能显示可用数秒直到 Kafka 传播;用短 TTL 可用性缓存补偿
结账预留SQL 截止时间 + 回收任务Redis 仅加速读取;回收任务必须幂等并与支付确认互斥
支付时机本例预订时付款过期后的成功支付要补偿;延期扣款需另行管理授权期限
分析Kafka → 数据仓库分析最终一致(分钟到小时滞后);对报告用例可接受
总结

酒店预订设计全关于一致性:SQL + 行级约束在数据库级别防双重预订;SQL 预留与幂等回收解决结账间隙;乐观锁让事务短且高并发;Kafka 异步喂 Elasticsearch 使搜索准确。"为什么不 NoSQL"的答案简单——预订量不足以证明牺牲 ACID 合理,且酒店数有界。预订 API 上的幂等键是正确系统和在网络重试时给客人双重扣款的系统之间的区别。

🎯 面试速答

并发请求下怎么防止双重预订? 在同一事务中检查并扣减全部住宿日期的库存,用条件更新和非负约束守住库存不小于零。库存只剩一间时只能有一个预留成功;仍有余量时,其他请求可重试成功。
为什么不像电商用 Cassandra 做订单历史那样做预订? 本例优先选择 SQL,是因为多晚库存与预订记录需要原子修改。是否引入 Cassandra 应由实际读写模式和运维需求决定,而不是仅凭产品类别。
用户选房间后放弃结账会怎样? 数据库定时任务根据 hold_expires_at 扫描过期预订。只有成功转换预订状态的事务才回补库存;Redis 过期通知可以加速处理,但不能作为唯一依据。
怎么让预订 API 幂等? 幂等键绑定用户及请求指纹,并以唯一约束防止重复创建;同键不同请求报错。支付操作另有持久化的渠道幂等键。
怎么处理预订确认时支付处理器超时? 保存支付对象 ID 和操作幂等键,查询渠道状态或重试同一操作。预留仍有效才可确认;若已过期并释放库存,则进入幂等退款或重新预留流程。

继续练习

用 电商系统设计对比单件库存与跨服务结账,再通过 DDIA 第 7 章复习事务边界、通过 第 8 章复习超时与部分失败。

← 上一篇
设计 URL 短链