← 2026-05-06 📂 All Days 2026-05-08 →

🏗️ 系统设计 / System Design — Day 37

主题 / Topic: Design a Ticket Booking System (Ticketmaster)

难度 / Difficulty: Advanced | 阶段 / Phase: Mastery


想象你在设计... / Imagine You're Designing...

你在设计一个像 Ticketmaster 的票务平台。Taylor Swift 巡演票刚开放预售,10 万人同时抢票,每张票库存只有一张。系统必须保证:不能超卖、不能同一张票卖给两个人、同时支撑几十万并发请求。

You're building a ticket booking platform like Ticketmaster. Taylor Swift presale just dropped — 100K users rushing simultaneously, and each seat has exactly one ticket. The system must guarantee: no overselling, no double-booking, while handling massive concurrent load.


架构图 / Architecture Diagram

┌─────────────┐ │ CDN / WAF │ (static assets, DDoS protection) └──────┬──────┘ │ ┌─────────▼──────────┐ │ API Gateway │ │ (rate limiting, │ │ auth, routing) │ └────┬──────────┬────┘ │ │ ┌───────────▼──┐ ┌────▼──────────────┐ │ Queue Service │ │ Booking Service │ │ (virtual │ │ (stateless, │ │ waiting │ │ horizontally │ │ room) │ │ scaled) │ └───────────────┘ └────┬──────────────┘ │ ┌───────────────────────▼────────────────────┐ │ Redis Cluster │ │ (seat locks: SETNX, TTL=10min, │ │ available seat inventory) │ └───────────────────────┬────────────────────┘ │ ┌───────────────────────▼────────────────────┐ │ PostgreSQL (Primary) │ │ tickets table: seat_id, status, user_id │ │ orders table: order_id, payment_status │ │ SELECT FOR UPDATE → serialized writes │ └────────────────────────────────────────────┘ │ ┌───────────▼──────────┐ │ Payment Service │ │ (Stripe/PayPal, │ │ async webhook) │ └──────────────────────┘

核心设计决策 / Key Design Decisions

1. 座位锁定 (Seat Locking) — 乐观锁 vs 悲观锁

两阶段流程 / Two-phase flow:

Phase 1: Soft Lock (用户选座时) Redis SETNX seat:{id}:lock {user_id} EX 600 → 成功:进入支付流程 → 失败:座位已被锁,提示用户选其他位置 Phase 2: Hard Commit (支付成功后) BEGIN TRANSACTION SELECT * FROM tickets WHERE seat_id = ? AND status = 'available' FOR UPDATE UPDATE tickets SET status = 'sold', user_id = ? WHERE seat_id = ? INSERT INTO orders (...) COMMIT → 同时 DEL Redis lock

为什么用两阶段? 纯 DB 锁撑不住 10 万并发;纯 Redis 锁在支付成功后还需要 DB 原子提交。两阶段各司其职。

2. 虚拟等候室 (Virtual Waiting Room)

高热度演唱会开售前,用户先进等候室队列(SQS/Kafka),按 FIFO 逐批放入系统,避免瞬间流量将数据库打垮。这是 Ticketmaster、12306 都在用的方案。

3. 防超卖 (Oversell Prevention)

-- DB level constraint
ALTER TABLE tickets ADD CONSTRAINT unique_seat UNIQUE (event_id, seat_id);
-- + Redis atomic decrement for inventory
WATCH inventory:{event_id}
MULTI
  DECR inventory:{event_id}  -- atomic, fails if 0
EXEC

为什么这样设计?/ Why This Design?

方案优点缺点
纯 DB 行锁强一致性10万并发把 DB 打死
纯 Redis高性能宕机丢数据,支付后仍需 DB
Redis + DB 两阶段性能 + 一致性复杂度高,需处理锁过期

❌ 别踩这个坑 / Common Mistakes

  1. 忘记锁过期处理 — 用户选了座位但放弃支付,10分钟后 Redis TTL 到期,座位自动释放。必须用 expiry + cleanup job 双保险。
  2. 在 Redis 里扣库存但不做 DB 回写 — Redis 挂了库存就没了,一定要 DB 是最终 source of truth。
  3. 不做幂等 — 支付回调可能重复触发,order_id 要做唯一性校验,防止重复出票。
  4. 没有 Circuit Breaker — 支付服务超时时要快速失败,释放座位锁,别让用户无限等待。

📐 规模估算 / Scale Estimation

Peak: 100K concurrent users
Seat lock ops: ~100K/s (Redis handles this easily)
DB writes (final purchase): ~1K-5K/s (much lower, filtered by Redis)
Read throughput: CDN + Redis cache handles 95% of seat map reads
Storage: 1M events × 50K seats = 50B rows (partitioned by event_id)

📚 References

🧒 ELI5

就像抢限量球鞋:你先抢到一张"号码牌"(Redis 锁),然后拿着号码牌去收银台付钱(DB 写入),付完才算真的买到。号码牌有过期时间,10 分钟不来就还给别人。

Like getting a numbered ticket at the deli counter. You grab a ticket (Redis lock), then bring it to the cashier (DB write) to actually complete the purchase. If you don't show up in 10 minutes, your number expires and goes back in the pool.

💻 算法 / Algorithms — Day 37

题目 / Problem: #33 Search in Rotated Sorted Array 🟡 Medium

模式 / Pattern: 二分搜索 Binary Search (5/7)

LeetCode: https://leetcode.com/problems/search-in-rotated-sorted-array/

NeetCode: https://neetcode.io/problems/find-target-in-rotated-sorted-array


🧩 Binary Search (5/7) — 在模板基础上变化

Building on the template from Day 32 (Binary Search block, problem 1: #704).

回顾模板 / Template Recap:

left, right = 0, len(arr) - 1
while left <= right:
    mid = (left + right) // 2
    if arr[mid] == target: return mid
    elif arr[mid] < target: left = mid + 1
    else: right = mid - 1

今天的变化 / Today's twist: 数组是"旋转"过的排序数组 — 标准二分不能直接用,因为整体不再单调递增。需要先判断哪半边是有序的,再决定往哪边搜。

The array is a rotated sorted array — standard binary search breaks because the array isn't fully monotonic. Key insight: even in a rotated array, at least one half is always sorted.


现实类比 / Real-World Analogy

想象一叠按年份排好的文件,有人把后半段搬到了前面。比如原来是 [1,2,3,4,5,6,7],变成了 [4,5,6,7,1,2,3]。虽然整体乱了,但每次切开,至少一边是连续有序的。你用这个性质来决定往哪边找。

Like a sorted stack of papers where someone moved the back half to the front. Though the whole stack is "rotated", when you split it in half, at least one side is always in order. Use that to decide which direction to search.


问题分析 / Problem Breakdown

Input: nums = [4,5,6,7,0,1,2], target = 0
Output: 4 (index of 0)

Input: nums = [4,5,6,7,0,1,2], target = 3
Output: -1

Constraints:
- All values unique
- Time: O(log n)

映射到模板 / Mapping to Template

标准二分用 arr[mid] vs target 决定方向。旋转数组要先确认有序半边,再用范围检查决定方向。

Standard:  is target LEFT or RIGHT of mid?
Rotated:   WHICH HALF is sorted? Is target IN that sorted half?

两个判断 / Two checks:

  1. 左半边有序?nums[left] <= nums[mid]
  2. Target 在左半边范围内?nums[left] <= target < nums[mid]

Python 解法 + Trace / Solution with Trace

def search(nums: list[int], target: int) -> int:
    left, right = 0, len(nums) - 1
    
    while left <= right:
        mid = (left + right) // 2
        
        if nums[mid] == target:
            return mid
        
        # Check if LEFT half is sorted
        if nums[left] <= nums[mid]:
            # target in left sorted range?
            if nums[left] <= target < nums[mid]:
                right = mid - 1  # go left
            else:
                left = mid + 1   # go right
        else:
            # RIGHT half must be sorted
            # target in right sorted range?
            if nums[mid] < target <= nums[right]:
                left = mid + 1   # go right
            else:
                right = mid - 1  # go left
    
    return -1

# Trace: nums=[4,5,6,7,0,1,2], target=0
# Iteration 1: left=0, right=6, mid=3 → nums[3]=7, not target
#   Left sorted? nums[0]=4 <= nums[3]=7 ✅
#   target=0 in [4,7)? NO → left = mid+1 = 4
# Iteration 2: left=4, right=6, mid=5 → nums[5]=1, not target
#   Left sorted? nums[4]=0 <= nums[5]=1 ✅
#   target=0 in [0,1)? YES → right = mid-1 = 4
# Iteration 3: left=4, right=4, mid=4 → nums[4]=0 == target ✅
# Return 4

时间复杂度 / Complexity:

  • Time: O(log n) — 每次排除一半
  • Space: O(1) — 原地操作

与同模式题目对比 / Pattern Comparison

题目变化点关键差异
#704 Binary Search标准排序数组直接比较
#153 Find Minimum in Rotated找旋转点不需要 target,只找最小值
#33 Search in Rotated找 target先判断有序半边
#981 Time Based KV (下一题)时间戳 + 字符串二分 + 条件判断

核心洞察: #153 找的是旋转点(pivot),#33 在知道有旋转的情况下找 target。两题都利用了"至少一边有序"这个性质。


举一反三 / Generalization

同类变形:

  • 数组有重复元素时 → nums[left] == nums[mid] 时无法判断哪边有序,需要 left++ 跳过 (LeetCode #81)
  • 旋转后找最小值 → 不用 target,只看旋转点 (#153)
  • 2D Matrix 搜索 → 先二分找行,再二分找列 (#74)

📝 Quiz

问题: 对于旋转数组 [6,7,1,2,3,4,5],mid=3(值为2),target=7,下一步应该:

A) right = mid - 1

B) left = mid + 1

C) return mid

D) return -1

<details><summary>答案 / Answer</summary>

A) right = mid - 1

Left half: [6,7,1,2] — nums[left]=6 > nums[mid]=2,左半边不是有序的。

Right half: [2,3,4,5] — 右半边有序。

target=7 在右半边范围 (2,5] 内吗?不在。

所以 target 在左半边 → right = mid - 1,往左搜。

</details>


📚 References

🧒 ELI5

把书架上的书按字母排好,然后把后半段搬到前面。要找某本书:先看中间那本,判断左边还是右边是"整齐的",再判断要找的书在不在那个整齐的那半里,决定往哪边找。每次排除一半,找到为止。

Like a shelf of books sorted A-Z, then someone moved the Z-M books to the front. To find a book: look at the middle, figure out which half is "in order", check if your book would be in that ordered half, then search there. Eliminate half each time.

🗣️ 软技能 / Soft Skills — Day 37

题目 / Question: Tell me about a time you over-engineered something. What did you learn?

类别 / Category: Failure & Growth | 级别 / Level: Senior/Staff


为什么面试官问这个 / Why Interviewers Ask This

这题考察的不是失败本身,而是你的工程判断力和自我认知。Senior 工程师必须知道:简单够用的方案 > 技术上完美的方案。能坦诚承认过度设计,说明你对"工程权衡"有成熟的理解。

This question isn't about failure — it's about engineering judgment and self-awareness. Senior engineers must internalize: simple and sufficient > technically perfect. Honestly acknowledging over-engineering shows maturity in understanding tradeoffs.


STAR 拆解 / STAR Breakdown

Situation(情境)

"In my previous role, we were building an internal analytics dashboard used by ~20 people. I proposed — and built — a full event-sourcing + CQRS architecture with Kafka, separate read/write models, and eventual consistency."

Task(任务)

"My goal was to make the system 'future-proof' for scale. I estimated we might grow to 10K users eventually."

Action(行动)

"I spent 6 weeks building the infrastructure: Kafka topics, event store, two separate databases, replay logic. The actual feature work took another 4 weeks on top of that."

Result(结果)

"We shipped 10 weeks late. The team struggled to understand the codebase. Two engineers needed me to explain the system before any change. Three months later, the dashboard was deprecated because the product direction changed. The actual peak load was 20 concurrent users — a SQLite database would have been fine."

❌ Bad Answer vs ✅ Good Answer

❌ 避免 / Avoid:

  • "I once wrote very detailed documentation" ← 这不是过度设计
  • "Everything turned out fine" ← 没有 growth
  • Making it sound like a humble-brag ("I was TOO thorough")

✅ 好的回答要包含 / Good answer includes:

  1. 具体技术细节 — 说清楚用了什么技术,为什么是 overkill
  2. 业务代价 — 延期、复杂度、维护成本
  3. 根因分析 — "我当时的假设是...但实际是..."
  4. 改变了什么 — 之后如何做决策不同了

根因 & 教训 / Root Cause & Lessons

过度设计往往源于:

  • 担心被批评不够"高级" — 想证明自己懂分布式系统
  • 为假设的未来优化 — YAGNI (You Aren't Gonna Need It)
  • 缺乏问题约束条件 — 没有先问"这个系统的 SLO 是什么?用户规模多大?"

改变后的决策框架 / Post-lesson decision framework:

Before architecting, ask:
1. What's the actual current load? (not hoped-for future load)
2. What's the maintenance cost if I leave?
3. Can we start simple and migrate later?
4. What's the cost of being wrong?

Senior/Staff 加分 / Senior/Staff Tips

加分回答: 聊到你如何把这个教训传递给团队。

"After this, I started running 'complexity audits' before kick-off: we write down our load assumptions, then ask 'what's the simplest thing that could work?' We only add complexity when we've hit a measurable constraint."

Staff 级别加分: 谈 organizational impact — 过度设计不只是你的问题,它是 team velocity 的杀手。


关键要点 / Key Takeaways

  1. YAGNI 原则 — 不需要的功能就不要设计,即使你觉得"以后会用到"
  2. 复杂度是负债 — 每个系统决策都有维护成本,要收益 > 成本
  3. 先问约束条件 — 用户规模、SLO、团队规模、系统寿命
  4. 成熟 = 选简单 — 能用简单方案解决问题,才是真正的技术能力

📚 References

🧒 ELI5

就像你要烤一个生日蛋糕,结果造了一个商业烘焙工厂。工厂确实可以做蛋糕,但造工厂花了3个月、花了100万,蛋糕的生日早过了。

Like building a commercial bakery when someone asked for a birthday cake. The bakery can definitely make cakes — but it took 3 months and a million dollars, and the birthday was last week.

🎨 前端 / Frontend — Day 37

主题 / Topic: Virtualization — Rendering 10K Items Without Dying

类别 / Category: Performance | 阶段 / Phase: Mastery


真实场景 / Real Scenario

你在做一个后台 dashboard,需要展示一个有 10,000 条记录的交易列表。产品说"不要分页,用户要能滚动看所有数据"。你直接 items.map(item => <Row />) 之后,浏览器直接卡死……

You're building an admin dashboard that needs to show 10,000 transaction rows. Product says "no pagination — users should scroll through everything." You naively items.map(item => <Row />) and the browser hangs.

问题根源: DOM 中存在 10,000 个节点,即使大多数在视口外,浏览器仍然需要布局、样式计算、内存分配全部节点。


什么是虚拟化 / What is Virtualization?

核心思路: 只渲染视口内(及附近缓冲区)的节点,其余用占位符撑满滚动高度。

Without virtualization: ┌─────────────────┐ ← viewport (visible) │ Row 1 (DOM) │ │ Row 2 (DOM) │ │ Row 3 (DOM) │ └─────────────────┘ │ Row 4 (DOM) │ ← not visible, but still in DOM! │ ... │ │ Row 10000 (DOM) │ ← all 10K nodes exist With virtualization: ┌─────────────────┐ ← viewport │ Row 47 (DOM) │ ← only ~20 rows in DOM │ Row 48 (DOM) │ │ Row 49 (DOM) │ └─────────────────┘ (spacer div) ← fakes scroll height for all 10K rows

代码示例 / Code Example

使用 react-window (推荐 / Recommended)

import { FixedSizeList } from 'react-window';

interface Transaction {
  id: string;
  amount: number;
  date: string;
}

const Row = ({ index, style, data }: {
  index: number;
  style: React.CSSProperties;
  data: Transaction[];
}) => (
  // Must apply style prop! It positions the row absolutely
  <div style={style} className="flex px-4 items-center border-b">
    <span>{data[index].date}</span>
    <span className="ml-auto">${data[index].amount}</span>
  </div>
);

export function TransactionList({ transactions }: { transactions: Transaction[] }) {
  return (
    <FixedSizeList
      height={600}        // viewport height
      itemCount={transactions.length}
      itemSize={48}       // row height in px (must be fixed for FixedSizeList)
      width="100%"
      itemData={transactions}
    >
      {Row}
    </FixedSizeList>
  );
}

变高行 / Variable Height Rows → 用 react-virtual

import { useVirtualizer } from '@tanstack/react-virtual';

function VariableList({ items }: { items: string[] }) {
  const parentRef = useRef<HTMLDivElement>(null);
  
  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 48,  // initial estimate, measured after render
    overscan: 5,             // extra rows above/below viewport
  });

  return (
    <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
      {/* Total height spacer */}
      <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
        {virtualizer.getVirtualItems().map((virtualItem) => (
          <div
            key={virtualItem.key}
            style={{
              position: 'absolute',
              top: virtualItem.start,
              width: '100%',
            }}
          >
            {items[virtualItem.index]}
          </div>
        ))}
      </div>
    </div>
  );
}

猜猜输出 / What Does This Code Do?

// Without style prop:
const Row = ({ index }: { index: number }) => (
  <div className="row">{index}</div>  // ← missing style prop!
);

A) 正常渲染,只是没有虚拟化

B) 所有行重叠在 top:0 的位置

C) 什么都不显示

D) 报错 TypeError

<details><summary>答案 / Answer</summary>

B) 所有行重叠在 top: 0

react-window 通过 style prop 给每行设置 position: absolute; top: Npx; height: 48px。不传 style 的话所有行都默认 top: 0,全部堆叠在列表顶部。这是使用 react-window 最常见的 bug。

</details>


❌ vs ✅ 对比 / Common Mistakes

// ❌ Forgetting the style prop
const Row = ({ index, data }) => (
  <div>{data[index].name}</div>  // rows pile up at top!
);

// ✅ Always apply the style prop
const Row = ({ index, style, data }) => (
  <div style={style}>{data[index].name}</div>
);

// ❌ Inline function as children (re-creates on every render)
<FixedSizeList>
  {({ index, style }) => <Row ... />}
</FixedSizeList>

// ✅ Define Row component outside (stable reference)
const Row = ({ index, style, data }) => ...;
<FixedSizeList itemData={items}>{Row}</FixedSizeList>

// ❌ Using index as key when list can reorder
{items.map((item, i) => <Row key={i} />)}

// ✅ Use stable unique id
{items.map((item) => <Row key={item.id} />)}

何时用 / 何时不用 / When to Use / Not Use

✅ 用虚拟化 / Use❌ 不用 / Skip
列表 > 200 条列表 < 100 条
每行有复杂 DOM静态表格,服务端渲染
移动端性能敏感需要 ctrl+F 搜索页面内容
无限滚动行高非常不规则且无法估算

注意: 虚拟化使 Ctrl+F(浏览器查找)无法搜索未渲染的行,SEO 也会受影响。


库对比 / Library Comparison

库特点适用场景
react-window轻量, 固定/变高行大多数列表场景
@tanstack/react-virtual最灵活, 支持 Grid复杂布局
react-virtuoso自动测量高度社交 feed、聊天
AG Grid企业级数据表格复杂 spreadsheet

📚 References

🧒 ELI5

你在看一本 1000 页的书,但你只能看到当前打开的那两页。聪明的电脑只"印刷"你正在看的那两页,其余的页只是撑着书的厚度。你翻页时,它才印下一页,同时把之前的页"回收"。这就是虚拟化。

Like reading a 1000-page book, but only the open pages exist in your hands. The smart printer only prints the 2 pages you're reading, using blank fillers for thickness. When you turn a page, it prints the next one and "recycles" the previous. That's virtualization.

🤖 AI 新闻 / AI News — Day 37

日期 / Date: 2026-05-07 | 模式 / Mode: NEWS


今日 AI 速递 / Today's AI Digest

📰 Story 1: 中国 Moonshot AI 完成 20 亿美元融资

来源: TechCrunch — May 7, 2026

中国 AI 公司 Moonshot AI(旗下有 Kimi 大模型)据报道完成了 20 亿美元融资,估值达 200 亿美元。此轮融资背后,是全球对开源 AI 需求的快速增长,以及中国 AI 生态系统在与美国模型竞争中持续发力。

Chinese AI startup Moonshot AI, maker of the Kimi large language model, reportedly closed a $2B fundraising round at a $20B valuation. The round is said to be driven by skyrocketing demand for open-source AI, and underscores the growing competitiveness of China's AI ecosystem against US models.

为什么重要 / Why it matters: 开源 AI 的货币化路径越来越清晰——API 收入 + 订阅双轮驱动。Moonshot 的崛起也是"去中心化 AI"趋势的缩影:世界不再只有一两个 AI 提供商。


📰 Story 2: Spotify 押注 AI 个人化音频

来源: TechCrunch — May 7, 2026

Spotify 宣布希望成为 AI 生成个人化音频内容的"大本营"。用户据报道将能够导入由 AI 工具(如 Codex 或 Claude)生成的播客内容并在 Spotify 上播放,平台也在加大对 AI DJ 功能的投入,已扩展到法语、德语、意大利语和葡萄牙语。

Spotify announced ambitions to become the home for AI-generated personal audio. Users will reportedly be able to import AI-generated podcast content (created with tools like Codex or Claude) into Spotify. The platform is also expanding its AI DJ feature to French, German, Italian, and Brazilian Portuguese.

为什么重要 / Why it matters: 内容创作的 AI 化正在加速。当 AI 可以生成播客、配音、音乐,传统内容平台的护城河从"内容独家"转向"分发渠道"。Spotify 在提前布局。


📰 Story 3: xAI 变身"新云"公司?

来源: TechCrunch — May 6, 2026

Elon Musk 的 xAI 据分析正在向"neocloud"(新型 AI 云服务商)转型。其真正的商业模式可能不只是训练 Grok 大模型,而是通过数据中心基础设施向外部客户提供 GPU 算力租赁。这与 CoreWeave、Lambda Labs 等 AI 云的模式类似。

Elon Musk's xAI appears to be pivoting toward a "neocloud" model — its real business may be less about training Grok and more about leasing GPU compute capacity from its massive data centers to external clients, similar to CoreWeave and Lambda Labs.

为什么重要 / Why it matters: AI 基础设施即服务(AIaaS)正成为巨大商机。算力稀缺时代,"卖铲子"(卖 GPU 算力)有时比"淘金"(训练模型)更稳。这也说明 AI 竞争已经从模型层下沉到算力层。


🔗 本周 AI 趋势总结 / Weekly Trends

  1. 开源 AI 融资热 — 资本押注非 OpenAI 玩家,生态多元化
  2. AI × 内容平台 — 传统平台开始拥抱 AI 生成内容,抢占分发渠道
  3. 算力经济 — GPU 云和 AI 基础设施成为新的"石油"生意

🧒 ELI5

AI 今天像三件事:一家中国 AI 公司拿到了很多钱继续做他们的智能助手;Spotify 想让你可以听 AI 帮你做的播客;马斯克的 AI 公司可能主要是在"租电脑"给别人用,不只是做自己的 AI。

Today in AI: a Chinese AI company raised a lot of money to keep building their assistant; Spotify wants you to listen to AI-made podcasts; and Musk's AI company might mostly be in the business of renting out computers, not just building AI.