跳转到内容

Hotel Reservation — 酒店预订


V1「做个酒店浏览页面,先看看效果」

Section titled “V1「做个酒店浏览页面,先看看效果」”

朋友开了一家旅行社,想做个小网站让客户能浏览合作酒店。他说:“你先帮我做个页面出来,能看酒店信息就行,预订功能假的也没关系,我先拿去给客户演示。“你决定先用纯前端做个原型,酒店数据写死在 JSON 里,预订按钮点了只存到 localStorage,不需要后端。这样一天就能出效果。

做一个酒店浏览和模拟预订页面,数据全部在前端,不依赖任何后端服务。

做一个酒店浏览和预订页面,要求:
技术栈:单个 HTML 文件 + 内联 CSS/JS(或 React + Vite 均可)
数据:页面内嵌 JSON 数组,包含 20 家酒店,每家酒店字段:
- id, name, city(北京/上海/杭州/成都/三亚), district
- price_per_night(200-2000), rating(3.5-5.0), photos(3张占位图URL)
- room_types:[{name: "标准间", capacity: 2, price: X}, {name: "大床房", ...}, {name: "豪华套房", ...}]
- amenities:["WiFi", "早餐", "停车场", "泳池", "健身房"] 随机组合
功能:
1. 酒店列表页:卡片式展示,每张卡显示照片轮播、名称、城市、价格、评分
2. 筛选:按城市下拉筛选,按价格区间滑块筛选(200-2000),按评分筛选(4.0 以上)
3. 排序:价格升序/降序、评分降序
4. 点击酒店卡进入详情页(同页面内切换),展示所有房型、设施、3张照片
5. 选择房型 + 入住日期 + 退房日期,点击"预订"
6. 预订信息保存到 localStorage,key 为 "my_bookings"
7. "我的预订"页面展示所有已预订记录,支持取消(从 localStorage 删除)
UI 要求:
- 响应式布局,手机端卡片单列,桌面端三列
- 酒店卡片 hover 有阴影效果
- 价格用红色大字体突出显示
  • 页面加载显示 20 家酒店卡片,照片和信息完整
  • 选择城市”上海”后只显示上海的酒店
  • 价格滑块筛选和评分筛选联合生效
  • 排序切换后列表顺序正确变化
  • 点击酒店进入详情,能看到所有房型
  • 选择房型和日期后点击预订,“我的预订”中出现记录
  • 刷新页面后预订记录还在
  • 取消预订后记录消失
  • 前端状态管理:筛选、排序、分页的组合逻辑 → M9(数据处理)
  • localStorage 作为轻量持久化,适合原型验证 → M2(数据存储)
  • JSON 内嵌数据 vs API 获取数据的权衡 → M1(API 设计)

V2「老板说要真的能订,多个客户同时在用」

Section titled “V2「老板说要真的能订,多个客户同时在用」”

演示效果很好,朋友的旅行社决定正式上线。但问题来了:预订是假的,localStorage 只在自己浏览器里有效,客户 A 预订了并不会减少库存,客户 B 完全不知道。朋友说:“至少要能真的预订吧,不同客户看到的库存要是同一份。“你需要加一个后端,管理真实的房间库存,多个用户共享同一份数据。暂时用户量小,SQLite 够用。

搭建后端实现真实的酒店预订流程,包括库存管理、用户认证和订单记录。

在 V1 基础上增加真实的预订后端,要求:
后端:Go + Gin + SQLite(用 gorm)
数据模型:
- Hotel:id, name, city, district, rating, description
- RoomType:id, hotel_id, name, capacity, price_per_night, total_rooms
- Inventory:id, room_type_id, date, available_count
- User:id, email, password_hash, name, phone
- Order:id, user_id, hotel_id, room_type_id, check_in, check_out, total_price, status(pending/confirmed/cancelled), created_at
API 设计:
1. POST /api/auth/register — 用户注册
2. POST /api/auth/login — 登录返回 JWT
3. GET /api/hotels — 酒店列表,支持 ?city=&min_price=&max_price=&rating= 筛选
4. GET /api/hotels/:id — 酒店详情含房型列表
5. GET /api/hotels/:id/availability?check_in=2024-10-01&check_out=2024-10-03 — 查询指定日期的房间可用数量
6. POST /api/orders — 创建预订(需登录),请求体包含 room_type_id, check_in, check_out
- 检查日期范围内每天的 available_count >= 1
- 若可用,将日期范围内每天的 available_count 减 1
- 创建 Order 记录,status 为 confirmed
- 若不可用,返回 409 Conflict
7. GET /api/orders — 我的订单列表
8. PUT /api/orders/:id/cancel — 取消订单,恢复库存
初始数据(seed):
- 10 家酒店,每家 3 种房型,每种 5 间
- 未来 30 天的 Inventory 记录
前端更新:
- 注册/登录页面
- 酒店详情页显示实时库存数量
- 预订时如果库存不足显示"已满房"
- 我的订单页从 API 获取
  • 注册新用户并登录成功,拿到 JWT
  • 酒店列表 API 支持筛选参数,返回正确结果
  • 查询可用房间返回正确的库存数量
  • 预订成功后,再次查询同日期库存减少 1
  • 库存为 0 时预订返回 409
  • 取消订单后库存恢复
  • 用两个浏览器窗口模拟两个用户,一个订完另一个看到库存变化
  • 从前端假数据过渡到真正的关系型数据模型设计 → M2(数据存储)
  • RESTful API 设计:资源嵌套、状态查询、操作幂等性 → M1(API 设计)
  • JWT 认证流程:注册→登录→携带 token→鉴权 → M1(API 设计)
  • 库存是共享可变状态,多用户场景下必须由后端统一管理 → M6(事务与一致性)

V3「十一黄金周,同一间房被两个人订了」

Section titled “V3「十一黄金周,同一间房被两个人订了」”

十一黄金周到了,旅行社生意爆好,后台突然收到投诉:客户张先生和李女士都收到了同一间”三亚海景大床房”10月1日的确认短信。你查了数据库,发现同一个房型同一天的两笔订单都是 confirmed 状态。问题出在哪?两个请求几乎同时到达,各自读到 available_count=1,各自减 1,都成功了。SQLite 的并发写入能力有限,而且你的代码没有用事务锁来保护库存扣减。黄金周这种高峰期,必须解决超卖问题。

用 PostgreSQL + 事务锁彻底解决并发预订导致的超卖问题,并优化高峰期的查询性能。

在 V2 基础上解决并发超卖问题并提升高峰性能,要求:
后端:Go + Gin + PostgreSQL + Redis
核心改造 — 防超卖:
1. 将 SQLite 替换为 PostgreSQL
2. 预订 API 改为事务操作:
BEGIN
SELECT available_count FROM inventory
WHERE room_type_id = ? AND date BETWEEN ? AND ?
FOR UPDATE -- 行级悲观锁,阻塞其他并发预订
IF any available_count < 1 THEN ROLLBACK, return 409
UPDATE inventory SET available_count = available_count - 1
WHERE room_type_id = ? AND date BETWEEN ? AND ?
INSERT INTO orders (...)
COMMIT
3. 添加数据库约束:ALTER TABLE inventory ADD CHECK (available_count >= 0)
4. 记录锁等待指标:每次 FOR UPDATE 等待超过 100ms 记录日志
性能优化:
1. Redis 缓存热门酒店数据:
- 酒店详情缓存 key: hotel:{id},TTL 10 分钟
- 城市酒店列表缓存 key: hotels:city:{city},TTL 5 分钟
- 预订成功后主动失效相关缓存
2. 搜索优化:
- PostgreSQL 添加索引:inventory(room_type_id, date)、orders(user_id, status)
- 热门城市的搜索结果预热到 Redis
多步预订流程(Saga 模式概念):
1. POST /api/orders 改为两阶段:
- 第一步:创建 pending 订单 + 锁定库存(15 分钟有效)
- 第二步:POST /api/orders/:id/confirm 确认并完成支付(模拟)
- 超时未确认:定时任务释放库存,订单标记为 expired
2. 定时任务:每分钟扫描 pending 超过 15 分钟的订单
监控 API:
- GET /api/admin/stats — 返回:今日订单数、超卖拦截次数、缓存命中率、平均锁等待时间
前端更新:
- 预订流程分为"锁定→确认"两步,中间有 15 分钟倒计时
- 当库存 <= 3 时显示"仅剩 X 间"提醒
- 热门酒店搜索结果加载明显更快
  • 用 wrk 或脚本并发发送 20 个预订请求抢同一间房,只有 1 个成功,其余返回 409
  • 数据库中 available_count 永远 >= 0,没有超卖
  • 创建 pending 订单后 15 分钟不确认,库存自动释放
  • Redis 缓存命中:第二次查询同一酒店明显更快
  • /api/admin/stats 返回正确的统计数据
  • 数据库约束生效:手动尝试 UPDATE available_count = -1 被拒绝
  • SELECT FOR UPDATE 悲观锁:最直接的并发控制手段 → M6(事务与一致性)
  • 数据库约束作为最后一道防线,应用层 + DB 层双重保护 → M2(数据存储)
  • Saga 模式思想:长事务拆成多步,每步可补偿 → M6(事务与一致性)
  • Redis 缓存热数据 + 主动失效策略 → M4(缓存策略)
  • 从 SQLite 到 PostgreSQL:并发能力是数据库选型的关键考量 → M8(可扩展性)
  • 定时任务处理超时订单,保证系统自愈 → M9(数据处理)

V4「搜索结果慢,用户搜完等3秒才出来」

Section titled “V4「搜索结果慢,用户搜完等3秒才出来」”

旅行社业务增长到千家合作酒店,注册用户过千。用户反馈最多的问题是搜索慢:“我搜上海的酒店,页面转圈转了 3 秒才出来。“你分析了原因:每次搜索都要查 PostgreSQL 做多表 JOIN(酒店 + 房型 + 库存),城市+日期范围的查询没有高效索引,而且很多用户搜的是同样的条件(同城市同日期)。更糟的是,首页的热门推荐也是实时查库,每个用户访问首页都触发一次重查询。

用 Redis 缓存搜索结果、优化数据库索引和查询,以及异步预计算库存快照,将搜索响应从 3 秒降到 200ms 以内。

在 V3 基础上优化搜索性能,要求:
后端:Go + Gin + PostgreSQL + Redis
1. 搜索结果缓存:
- 缓存 key 设计:search:{city}:{check_in}:{check_out}:{sort}:{page}
- TTL 3 分钟(库存变化频率不高,短暂不一致可接受)
- 预订成功后主动失效该城市的搜索缓存
- GET /api/hotels/search 优先查 Redis,未命中再查 DB 并回填
2. 数据库索引优化:
- 复合索引:inventory(room_type_id, date, available_count)
- 复合索引:hotels(city, rating DESC)
- 覆盖索引:避免回表查询
- 用 EXPLAIN ANALYZE 验证查询计划
3. 库存快照异步预计算:
- 后台 goroutine 每 5 分钟计算各城市未来 30 天的可用酒店摘要
- 存入 Redis Hash:availability_snapshot:{city}:{date} → {hotel_id: min_price, available_rooms}
- 搜索列表页直接用快照数据,点击详情页再查精确库存
4. 分页优化:
- 使用 cursor-based 分页替代 offset 分页
- cursor 基于 (rating, hotel_id) 复合排序
- 每页 20 条,返回 next_cursor
5. 热门推荐预计算:
- 每小时计算各城市 Top 10 酒店(按预订量排序)
- 缓存到 Redis List:popular:{city},TTL 1 小时
- 首页热门推荐直接读缓存,零 DB 查询
前端:
1. 搜索结果骨架屏加载,缓存命中时近乎即时展示
2. 搜索列表显示"约 X 家可选"(来自快照),详情页显示精确库存
3. 首页热门推荐区域秒开
4. 翻页使用无限滚动 + cursor 分页
  • 相同搜索条件第二次请求响应 < 50ms(命中缓存)
  • EXPLAIN ANALYZE 显示查询走了复合索引,无全表扫描
  • 库存快照数据与实际库存基本一致(5 分钟内延迟可接受)
  • 预订成功后搜索缓存被正确失效,下次搜索返回新数据
  • cursor 分页正确工作,翻到最后一页返回空 next_cursor
  • 首页热门推荐读取速度 < 20ms
  • 整体搜索 P95 响应时间 < 200ms
  • 搜索缓存策略:缓存 key 的维度设计与主动失效 → M4(缓存策略)
  • 数据库索引优化:复合索引的列顺序、覆盖索引避免回表 → M2(数据存储)
  • 异步预计算:用”最终一致性”换取查询性能 → M5(消息与事件)
  • Cursor 分页 vs Offset 分页:大数据量下的性能差异 → M1(API 设计)
  • 数据新鲜度与性能的权衡:列表用快照、详情用精确值 → M9(数据处理)

V5「节假日流量是平时的10倍,系统顶不住」

Section titled “V5「节假日流量是平时的10倍,系统顶不住」”

又一个黄金周来了,这次用户量达到一万。平时系统运行良好,但节假日搜索量暴增到平时 10 倍,系统接连出问题:后端 CPU 跑满导致接口超时,数据库连接池耗尽,预订请求排队等锁时间过长。更麻烦的是,支付回调偶尔丢失导致订单状态不一致,客服电话被打爆。你意识到单机架构已经到了极限,需要水平扩展。

通过多实例部署、数据库读写分离、队列化订单处理和熔断机制,让系统在 10 倍流量下也能稳定运行。

在 V4 基础上实现水平扩展和高可用,要求:
多实例部署:
1. 后端部署 3 个实例,Nginx 负载均衡
- 健康检查:GET /health 每 5 秒探测
- 会话无状态:JWT 不依赖服务器内存,所有状态存 Redis/PostgreSQL
- 优雅停机:收到 SIGTERM 后等待当前请求完成(最多 30 秒)再退出
2. Nginx 配置:
- 加权轮询策略
- 慢节点自动摘除(3 次超时后移除,30 秒后重试)
- 限流:单 IP 每秒最多 20 个请求
数据库读写分离:
1. PostgreSQL 主从配置:
- 主库:处理写操作(预订、取消、库存更新)
- 从库(1 个副本):处理读操作(搜索、列表、详情查询)
2. Go 代码中区分读写数据源:
- 写操作用 db.Master()
- 读操作用 db.Replica()
- 预订后的立即查询走主库(避免主从延迟导致看不到刚下的订单)
队列化订单处理:
1. 预订请求解耦:
- POST /api/orders 只做参数验证,然后发消息到 Redis Stream(order_queue)
- 返回 202 Accepted + order_id,前端轮询订单状态
- 后台 Worker(独立 goroutine 池)消费队列,执行库存锁定和订单创建
2. Worker 处理逻辑:
- 从 Redis Stream 消费消息(XREADGROUP)
- 执行事务锁定库存 + 创建订单
- 更新订单状态到 Redis:order:{id}:status → confirmed/failed
- 失败重试 3 次,最终失败标记为 failed 并通知用户
3. 订单状态查询:
- GET /api/orders/:id/status — 先查 Redis,再查 PostgreSQL
熔断器模式:
1. 对下游服务(支付回调、短信通知等)添加熔断器
- 连续 5 次失败 → 熔断器打开(直接返回错误,不调下游)
- 30 秒后 → 半开状态(放 1 个请求试探)
- 试探成功 → 关闭熔断器,恢复正常
2. GET /api/admin/circuit-breakers — 查看各熔断器状态
监控增强:
1. 各实例 QPS/延迟/错误率独立上报
2. Redis Stream 队列积压长度监控
3. 数据库主从延迟监控
4. 节假日流量对比大盘(今日 vs 平日平均)
前端:
1. 预订提交后显示"排队中...",轮询订单状态直到 confirmed/failed
2. 搜索接口超时后自动重试 1 次
3. 非核心功能降级:流量过大时隐藏"推荐酒店"模块,减少接口调用
  • 3 个后端实例正常运行,Nginx 负载均衡分发请求
  • 停掉 1 个实例,请求自动切到其他实例,用户无感知
  • 搜索请求走从库,预订请求走主库,日志中可验证
  • 预订后立即查询订单走主库,能看到刚创建的订单
  • 高并发预订时请求进入队列,Worker 有序处理,无超卖
  • Redis Stream 中消费进度正常推进,无大量积压
  • 模拟下游服务故障,熔断器正确打开/半开/关闭
  • 单 IP 限流生效:超过 20 请求/秒返回 429
  • 压测 10 倍流量,系统整体可用,无雪崩
  • 水平扩展基础:无状态应用 + 负载均衡 + 共享存储 → M8(可扩展性)
  • 读写分离:用从库分担读压力,主从延迟的应对策略 → M3(数据复制)
  • 消息队列解耦:削峰填谷,异步处理高并发写入 → M5(消息与事件)
  • 熔断器模式:防止级联故障扩散到整个系统 → M16(DevOps)
  • 优雅降级:非核心功能在高峰期主动关闭 → M8(可扩展性)
  • Nginx 限流与健康检查:入口层的流量管控 → M13(网络与内容分发)

V6「扩张到全国100个城市,数据库单表千万行」

Section titled “V6「扩张到全国100个城市,数据库单表千万行」”

旅行社融资成功,业务扩张到全国 100 个城市,合作酒店上万家,日活用户突破十万。数据库问题全面爆发:inventory 表已有千万行,搜索即使有索引也开始变慢;单个 PostgreSQL 主库写入 QPS 到达瓶颈,高峰期锁等待时间飙升;热门城市(三亚、丽江)假期间库存竞争极其激烈。与此同时,产品经理提出了动态定价需求——根据供需关系实时调整价格,这进一步增加了系统复杂度。

通过数据库分片、独立搜索服务、Saga 跨分片事务、动态定价引擎和多级缓存,支撑全国级酒店预订平台。

在 V5 基础上构建全国级酒店预订平台,要求:
数据库按城市分片:
1. 分片策略:按城市维度分库
- shard_1:华东城市(上海、杭州、南京...)
- shard_2:华南城市(广州、深圳、三亚...)
- shard_3:华北城市(北京、天津、青岛...)
- shard_4:西部城市(成都、重庆、昆明...)
2. 分片路由中间件:
- 根据 city 参数路由到对应分片
- 跨城市搜索(如"搜索全国五星酒店")并行查询所有分片,合并结果
3. 每个分片独立的主从配置(1 主 1 从)
4. 全局表(不分片):用户表、订单总表(只存 order_id + shard_id 的映射)
独立搜索服务(Elasticsearch):
1. 酒店数据同步到 Elasticsearch:
- 字段:name, city, district, rating, price_range, amenities, room_types, available_dates
- 酒店信息变更时通过消息队列异步同步
2. 搜索 API 改为查 Elasticsearch:
- 支持全文搜索(酒店名、地区)
- 支持多维度筛选 + 聚合(各价位段酒店数量)
- 搜索响应 < 100ms
3. 预订仍走 PostgreSQL 分片(强一致性)
Saga 模式跨分片预订:
1. 场景:用户一次预订多个城市的酒店(行程规划)
2. Saga 协调器:
- Step 1: 在 shard_1 锁定上海酒店库存
- Step 2: 在 shard_2 锁定三亚酒店库存
- Step 3: 创建统一订单记录
- 任一步骤失败 → 执行补偿操作(释放已锁定的库存)
3. Saga 状态机持久化到 Redis,支持断点恢复
动态定价引擎:
1. 定价因素:
- 供需比:available_rooms / total_rooms,低于 30% 加价
- 时间因素:节假日系数(1.5-3.0 倍)、提前天数折扣
- 竞对参考:同城市同星级酒店的平均价
2. 价格计算流程:
- 基础价 × 供需系数 × 节假日系数 × 提前预订折扣 = 最终价
- 最终价有上下限保护(0.5x - 5x 基础价)
3. 价格缓存:
- Redis 缓存当前价格,TTL 10 分钟
- 库存变化时触发价格重算
4. GET /api/hotels/:id/pricing?check_in=&check_out= — 返回每日价格明细
多级缓存架构:
1. 本地缓存(Go sync.Map):热门酒店基础信息,TTL 1 分钟
2. Redis 缓存:搜索结果、库存快照、价格,TTL 3-10 分钟
3. PostgreSQL:最终一致的持久化数据
4. 缓存更新策略:
- 写穿透:预订成功后更新 Redis + 失效本地缓存
- 定时刷新:价格和库存快照定时从 DB 重建
- 缓存击穿防护:热 key 用 singleflight 防止并发回源
监控与运维:
1. 各分片负载监控:QPS、连接数、慢查询
2. Elasticsearch 索引健康度和同步延迟
3. Saga 事务成功率和补偿触发率
4. 动态定价效果大盘:各城市平均售价趋势、收益对比
前端:
1. 搜索体验升级:输入即搜索(debounce 300ms),关键词高亮
2. 价格日历:显示未来 30 天每日价格波动,标注"今日特价"
3. 行程规划:支持多城市酒店一键预订
4. 价格走势图:展示该酒店过去 30 天价格变化
  • 不同城市的酒店数据存储在对应分片,查询路由正确
  • 跨城市搜索(全国五星酒店)返回所有分片的结果,响应 < 500ms
  • Elasticsearch 搜索支持全文匹配和多维度筛选
  • 酒店信息更新后 Elasticsearch 在 10 秒内同步
  • 多城市预订 Saga 正常完成,任一步骤失败正确触发补偿
  • Saga 中途服务重启,能从 Redis 恢复状态继续执行
  • 动态定价在库存紧张时正确加价,价格在合理范围内
  • 多级缓存逐层命中:本地 → Redis → DB,日志可追踪
  • singleflight 防止热 key 并发回源,相同 key 只有 1 个请求查 DB
  • 各分片负载均衡,无单分片过热
  • 数据库分片策略:按业务维度分片降低单库压力 → M8(可扩展性)
  • 搜索与交易分离:Elasticsearch 处理搜索、PostgreSQL 处理事务 → M2(数据存储)
  • Saga 模式实战:跨分片的分布式事务与补偿机制 → M6(事务与一致性)
  • 动态定价:供需模型在工程中的实现 → M9(数据处理)
  • 多级缓存架构:本地 → 分布式 → 持久化的三层缓存体系 → M4(缓存策略)
  • singleflight 防缓存击穿:Go 并发控制的经典应用 → M4(缓存策略)
  • 消息队列实现数据异步同步:保证搜索索引最终一致 → M5(消息与事件)
  • 全国级系统的可观测性:分片级监控 + 业务指标大盘 → M15(可观测性)