01用户主要要做什么——浏览并搜索活动,还是只有订票?
用户可以查看活动及其场馆座位图与接近实时的可用状态,并按关键字、日期、地点搜索活动。
免费完整指南
设计票务系统。请覆盖Seat inventory model and seat-map representation for a venue/event, including seat status transitions, Reservation hold with TTL: reserve then confirm two-phase flow, and auto-release when...
面试节奏保持精简,让页面能把注意力花在真正的设计决策上。
不要只是陈述需求,要主动问出来。每张卡把设计约束和一句你可以在画架构前说出口的澄清问句配成一组。
01用户主要要做什么——浏览并搜索活动,还是只有订票?
用户可以查看活动及其场馆座位图与接近实时的可用状态,并按关键字、日期、地点搜索活动。
02当用户选好座位,付款期间会拿到临时保留吗?
用户可以选座并取得短暂的保留(5-10 分钟):先预留,再以付款确认;未确认的保留会自动释放。
03有哪一条保证是我们绝对不能打破的?
订票一旦成立即为最终:每个座位只会售出一次——永远不会重复售出,即使在秒杀开卖期间也一样。
04座位图选座、best-available 配座,还是两者都要?
两者都要:对号场馆的座位图选座,以及 best-available(最佳可选)——而 best-available 仍然会对具体座位取得原子保留,绝不是一个模糊的数量。
05买家可以在结账途中把座位加进既有的保留吗?
在保留有效期间可以:加选的座位会取得各自的保留,并原子性地并入同一笔订票——要么一起确认,要么这次加选干净地失败。
06卖光了——我们需要候补列表吗?
第一天不在范围内——但候补列表(waitlist)是很可能的后续,所以第一天的选择不该让日后加上它变得痛苦。
范围外热门活动的动态定价 · 创建活动用的后台与活动统筹工具 · 查看历史订票、票券转让与转售
01哪里需要强一致性,哪里的数据可以是过时的?
订票要一致性——一个座位绝不售出两次;浏览/搜索要可用性——座位图可以比现实落后几秒。
02秒杀开卖的尖峰长什么样子?
单一热门活动在开卖瞬间可吸引约 1,000 万名用户——系统必须吸收那个尖峰,而让用户公平等候、而不是对他们报错,是可以接受的。
03浏览与搜索要感觉多快?
搜索在约 500 ms 内;座位图视图即使在抢购潮中也必须感觉即时。整体读取密集,约 100:1。
04保留能维持多久,过期时会发生什么?
保留很短(5-10 分钟)并以 TTL 自动释放——被放弃的结账会归还座位而不需人工清理,且保留必须撑得比支付服务商的延迟更久。
05如果一个确认送达两次——连点两下,或支付服务商重试 webhook——扣款和售出仍然必须只发生一次吗?
重试与重复的支付 webhook 绝不能重复收款或重复订票——确认两次的效果必须和确认一次一模一样。
真实面试探得比一份整齐清单深得多。这些范围问句,区分出真正拷问问题的人和只是背诵的人。
把每个估算都当成一种压力,用来合理化一个组件:缓存、队列、分片、副本、worker pool 或退路。
开卖争用
约 1,000 万名用户(来自开卖需求)在同一开卖瞬间争抢假设的约 5 万个座位10,000,000 ÷ 50,000 ≈ 每个座位 200 名用户
几乎所有人都得先进虚拟等候室排队——谁能走到订票这一步,由准入控制的放行机制决定。
预留写入突发
开卖的第一分钟:获准进入的用户各自尝试一次保留准入 50K 用户/分钟 ≈ 对单一活动分区每秒 800 次预留尝试
按活动分片 + 短促的原子状态转换让热分区存活;其他活动不受影响。
浏览读取偏斜
读取密集约 100:1——座位图轮询占主导800 writes/s × 100 ≈ 尖峰每秒 80K 次座位图读取
以缓存/CDN 提供视图,容许几秒的过时——库存数据库永远看不到浏览流量。
保留 TTL vs 付款延迟
外部付款确认的 p95 是几秒;用户填表要几分钟TTL 5-10 min ≫ payment p95 ~3-10 s
宽松的 TTL 避免保留在付款途中过期;过期会自动释放被放弃的座位,不需清理作业。
搜索延迟预算
搜索目标端到端 < 500 ms倒排索引查询 ~50-100 ms + 排名 + 网络 ≈ 远低于 500 ms
需要全文索引(而非 LIKE 扫描);缓存重复查询,并用 CDN 缓存非个性化的结果。
决策示例
想象一下开卖:约 1,000 万人想要约 5 万个座位——每个座位约 200 人。这场争夺的是每个座位的状态,所以对一个座位的写入必须一次一个进行,而所有只是在浏览的人都从缓存获得服务。
我会做三件事。第一,把所有人放进等候室,以受控的速率放行进入订票流程,让座位数据库永远只看到它撑得住的流量。第二,当用户选好座位,就保留 5-10 分钟——在这段时间内付款,否则座位自动重新开卖。第三,把「检查座位是空的 AND 把它标记为 held」做成单一的原子步骤(一个 row lock,或一个只有在中间没人改动座位时才成功的更新),这样两个买家永远不可能都抢到它。
我不会做的事:先读「座位是空的」,再以第二个步骤把它标记为 held。在这两个步骤之间的空隙里,另一个买家可以做同样的读取——两人都以为自己赢了,座位就售出两次。检查与更新必须是一个步骤,而落败的一方应该得到清楚的「有人比你先一步」响应(HTTP 409),而不是无声的失败。
如果场馆是自由入场——没有特定座位,只有一个容量数字——那么逐座位加锁就太过头了。我会维护一个剩余容量的原子计数器,归零时就直接停售。
先看一张完整的图,再把每条路径各自画成一张图 —— 写入路径与读取路径承载不同流量,合理化不同的组件。
完整全貌
订票是主干;浏览与搜索永远不碰座位库存(虚线 = 读取端,在订票热路径之外)。过期的保留会自动归还座位。
路径 1
路径 2
活动页面是预先渲染并由 CDN 缓存的,所以秒杀永远不会为了静态内容打到应用服务器;可用状态可能落后几秒——订票交易才是真相被强制执行的地方。
在优化之前,先让契约可被检视:端点、实体、所有权、重试与状态。
GET/events/{event_id}
响应200 { event, venue, seat_map, availability }
缓存/CDN 优先;可用状态可能落后几秒——订票才是真相被强制执行的地方。
GET/events/search?keyword&date&location
响应200 Event[]
倒排索引(全文检索)——含模糊匹配也在 500 ms 以内。
POST/bookings
请求{ event_id, ticket_ids[] }
响应201 { booking_id, hold_expires_at } · 409 座位已被保留或售出
创建 TTL 保留:座位原子性地翻成 held,否则整个请求失败——不会有部分保留。
POST/bookings/{booking_id}/confirm
请求{ payment_token, idempotency_key }
响应200 已确认 · 402 付款失败(保留继续倒数) · 410 保留已过期
以 booking_id 为单位幂等:支付服务商的重试与重复的 webhook 都是安全的。
核心实体
Eventevent_id (PK) · venue_id · performer · starts_at · on_sale_at
Ticketticket_id (PK) · event_id · section/row/seat · price · status: available/held/booked · version
status + version 驱动乐观并发控制;按 event_id 分片,让一场开卖不会拖垮其他活动。
Bookingbooking_id (PK) · user_id · ticket_ids · total · status: pending/confirmed/failed · idempotency_key
Useruser_id (PK) · email · created_at
在面试最后三分之一挑一条路线。每条路线给你主题、它该回答的面试官问题,以及要避免的失败模式。
带一个座位走过 available → held → sold。到底是什么在翻动每个状态,两个人有没有可能保留同一个座位?
一次原子写入把座位从 available 翻成 held。只有座位还是 available 时它才成功,所以第二个买家直接失败。两个人不可能占到同一个座位,因为检查和翻转是同一步。付款之后 confirm 把 held 变成 sold;TTL 过期则把它退回 available。
没有 TTL 的保留——被放弃的购物车会永远锁住座位。
为什么要在付款前保留座位,如果付款一直没完成,座位会怎样?
先把座位 hold 住,这样买家在输卡号的时候它一直是他的。如果在预订那一刻就标成 sold,每一笔失败或放弃的付款都会卡住一个卖不出去的座位。付款一直没完成时,TTL 让这个 hold 过期,座位自动重新上架——不需要清理任务。
在预留时就把座位标记为售出——付款失败后它就变得无法销售。
Row lock、版本检查(CAS)还是原子计数器——各自如何阻止两个买家赢得同一个座位,在争用下各自的代价是什么?
对号入座就用短的行锁,或者单语句的条件更新,再用 admission control 把争用压下去。行锁把买家排成一列:永远正确,但他们会在热门行上排队。版本检查(CAS)省掉锁,可一旦每个座位挤着约 200 个买家,大多数人重试了还是一直输。原子计数器只适合可互换的座位——就是不对号的 general admission。
秒杀中纯用乐观重试——大多数买家一次又一次失败,而不是公平地排队等候。
1,000 万人涌向一场开卖。你如何让他们公平地等候,而不是把大多数人以错误挡掉?
把这 1000 万人全放进一个候客室,按受控的速率放行。一个 token bucket 每分钟大约放 5 万人进入预订流程,这样座位库存永远只面对它扛得住的流量。其余的人守着一个公平的排队位置,而不是对着错误拼命重试。别的活动完全感觉不到这波冲击。
让整群人都抵达座位库存——等候室的存在就是为了保护它。
确认请求送达两次——一次重试或一次连点两下。你如何确保卡片只被扣款一次、座位只售出一次?
confirm 带一个幂等键、booking_id,服务把第一次尝试的结果存下来。任何带同一个键的重试或重复 webhook,拿回的都是那个存好的结果,而不是再跑一遍。所以不管这个请求到达多少次,卡只扣一次,座位只卖一次。
确认上没有 idempotency key——一次网络重试就会重复收款或重复售出。
把 票务系统 大声讲一遍,让 AI 为你的说明评分。