题干与适用场景
这是一道低层设计与编码题,重点是把预约、桌位、时间区间和等候队列转化为职责清晰的对象。公开面经中出现过餐厅预约设计,OOD 方法也强调先澄清范围、列出需求,再确定对象与接口。题目假设单店、固定营业时间和内存内模块;若面试官要求持久化,再补充存储和并发策略。
面试官考察点
- 能否把
Restaurant、Table、Reservation、Waitlist和分配策略分离。 - 能否正确处理半开时间区间、桌位容量、拼桌规则和取消后的释放。
- 能否设计状态机、幂等请求和并发下的防重复分配。
- 能否用测试覆盖边界条件,并解释复杂度与可扩展点。
回答前需要澄清的问题
先确认预约是指定桌位还是只指定人数,是否允许拼桌;预约时长和清桌缓冲是多少;是否支持修改、迟到、爽约和临时到店;等候队列按先到先得还是按人数与时间匹配;一个请求是否可能重试。还要确认时间精度、时区、单店还是多店,以及是否要求跨进程持久化。
30 秒回答框架
我会先定义不可变的 TimeRange 和预约状态,再让 AvailabilityService 负责冲突查询,让 TableAllocator 负责按容量和策略选桌,ReservationService 负责编排创建、取消和通知,Waitlist 单独管理候补。创建时用幂等键和同一临界区检查“可用再占用”,取消只允许合法状态转换并释放桌位。最后用边界测试验证相邻时段、并发请求、重复取消和候补晋级。
分步骤深入解答
1. 建模时间、桌位和预约状态
用半开区间 [start, end) 表示预约,只有 start < end 才有效;两个区间在 a.start < b.end && b.start < a.end 时冲突。Table 保存容量、编号和可用状态,Reservation 保存顾客、人数、区间、桌位和状态。状态可为 HELD、CONFIRMED、SEATED、CANCELLED、NO_SHOW,非法转换直接拒绝。
2. 让分配策略可替换
TableAllocator 接收候选桌位和预约需求,默认选择“容量最小但足够”的桌,减少大桌浪费;也可以注入“优先连桌”或“无障碍桌优先”策略。分配器不负责写预约,避免把搜索、决策和状态持久化耦合到一个巨大类中。
3. 实现冲突查询和创建
查询按餐厅、时间区间和桌位索引过滤,再由分配器选择。创建流程先校验输入、计算缓冲后的区间、生成幂等键,然后在同一锁或事务边界内重新读取可用桌并写入预约;不能依赖查询阶段的旧结果。示意代码应把核心不变量写出来:同一桌的重叠 CONFIRMED 预约数量必须为零。
create(request, key):
if idempotency.exists(key): return idempotency.result(key)
range = TimeRange(request.start, request.end + cleanupBuffer)
lock(restaurantId, range):
table = allocator.choose(availableTables(range), request.partySize)
if table is null: return WAITLISTED
reservation = Reservation.confirm(request, table, range)
store(reservation)
idempotency.save(key, reservation.id)
return reservation4. 处理取消、迟到和候补晋级
取消只允许 HELD 或 CONFIRMED 进入 CANCELLED,重复取消返回同一个结果,不重复触发通知。到店后转为 SEATED,超过规则时间可转 NO_SHOW 并释放桌位。释放事件交给 WaitlistMatcher,按等待时间、人数适配和优先级挑选候补,再走同一创建临界区,防止候补与新预约竞争时双重分配。
5. 测试并说明复杂度
测试相邻区间 [19:00,20:00) 与 [20:00,21:00) 不冲突,反向输入被拒绝,缓冲时间会扩大冲突范围,重复幂等键不新增预约,取消后候补最多晋级一次,并发创建只能成功一个。若每张桌按时间排序保存预约,单桌冲突查询可为 O(log n + k);候选桌为 m 张时,选择和写入约为 O(m log n),可再按容量分桶或使用区间索引。
高质量示范回答
我会用半开 TimeRange、明确的预约状态机和可替换分配策略建模。AvailabilityService 负责按餐厅、时间和桌位过滤,TableAllocator 选择容量最小但足够的桌,ReservationService 在同一锁或事务边界中重新检查并写入,使用幂等键防止重试重复创建。取消、迟到和爽约通过合法状态转换释放桌位,候补匹配随后复用同一临界区。测试覆盖相邻区间、清桌缓冲、重复取消、并发创建和候补竞争,并说明按桌排序索引后的复杂度。
常见错误
- 把所有逻辑塞进
Restaurant,对象职责和替换策略不清楚。 - 用闭区间判断时间,导致相邻预约被错误判为冲突。
- 先查询可用桌,再异步写入,没有在写入边界重新检查。
- 取消接口不是幂等的,重试会重复释放桌位或发送通知。
- 只实现顾客预约,忽略前台 walk-in、迟到、爽约和候补。
- 只给类图,不写不变量、边界测试和复杂度。
追问及应对
如果允许拼桌,怎么扩展?
把分配结果从单个 tableId 改成有序桌位集合,并增加“组合后容量足够、桌间可连接、组合不与其他预约冲突”的约束。策略接口保持不变,新增 ComposableTableAllocator。
跨进程部署时如何防止重复预订?
把冲突约束下沉到持久化层,例如按桌位和时间建立可验证的锁或事务约束;应用层锁只作为减少竞争的优化,不能作为唯一正确性保障。
用户连续点击创建怎么办?
要求客户端提供幂等键,服务端按餐厅和用户维度保存结果。相同键返回原预约,不同键仍需经过统一冲突检查,不能用用户限流替代业务幂等。
如何优化热门晚餐时段?
按餐厅和时间分片,缓存只用于查询提示,最终创建仍走强一致临界区。可以预计算容量桶和候补候选,但必须在写入时验证版本,避免缓存过期造成超卖。
预约取消后通知失败怎么办?
状态变更与 outbox 事件在同一事务中提交,通知消费者幂等重试。桌位释放不应等待短信成功,通知失败进入重试和人工可见的死信队列。