← 2026-05-07 📂 All Days 2026-05-09 →

🏗️ 系统设计 / System Design — Design a Payment System (Stripe)

Day 38 · Mastery Phase · ~3 min read

想象你在设计...

你被要求为一个电商平台设计支付系统,类似 Stripe。每天需要处理数百万笔交易,支持信用卡、数字钱包等多种支付方式,同时保证资金安全和高可用性。

Imagine you're designing a payment system for an e-commerce platform like Stripe. You need to handle millions of transactions daily, support credit cards and digital wallets, and guarantee both fund security and high availability.


架构图 / Architecture Diagram

Client (Browser/App) │ ▼ [API Gateway] (TLS, auth, rate limit) │ ┌────┴────┐ │ │ ▼ ▼ [Payment [Fraud Service] Detection] │ │ │◄─────────────┘ │ (async scoring) ▼ [Payment Processor] (Stripe, Adyen, Braintree) │ ┌────┴────────────┐ │ │ ▼ ▼ [Ledger DB] [Event Bus] (PostgreSQL (Kafka) + double-entry) │ ├──► [Notification Service] ├──► [Analytics Pipeline] └──► [Reconciliation Service] [Idempotency Store] ←── All write requests keyed here (Redis)

核心设计决策 / Key Design Decisions

1. 幂等性 (Idempotency) — 最关键

支付最怕的是重复扣款。每个支付请求必须携带唯一 idempotency_key,服务端在 Redis 中记录已处理的请求。

The biggest fear in payments is double-charging. Every payment request carries a unique idempotency_key. The service stores processed requests in Redis — if the same key comes again, return the cached result.

POST /v1/charges
Idempotency-Key: user-123-order-456-attempt-1

2. 双重记账 (Double-Entry Ledger)

银行用了几百年的方法。每笔交易产生两条记录:借方 + 贷方,总和必须为 0。这样账目永远对得上。

Banks have used this for centuries. Every transaction creates two entries: debit + credit, always summing to zero. This makes reconciliation trivial and catches bugs immediately.

3. 状态机 (State Machine)

支付状态:`PENDING → PROCESSING → AUTHORIZED → CAPTURED → SETTLED` (or `FAILED`, `REFUNDED`)

每个状态转移必须是原子的,使用数据库事务 + 乐观锁防止并发问题。

Each state transition must be atomic, using DB transactions + optimistic locking to prevent race conditions.

4. 异步欺诈检测 (Async Fraud Detection)

欺诈检测不能阻塞支付流程(延迟太高)。用 Kafka 异步处理:先放行,发现欺诈后退款,同时标记账户。

Fraud detection can't block the payment flow (too slow). Use Kafka for async processing: approve first, refund + flag if fraud detected. This is the industry standard — 100% fraud prevention is impossible anyway.


为什么这样设计? / Why These Choices?

决策原因
Redis 幂等存储快速 O(1) 查找,支付窗口内有效即可
PostgreSQL 账本ACID 事务、强一致性,资金不能用最终一致
Kafka 事件流解耦下游服务,支持重放审计
状态机防止非法状态转移(不能从 FAILED 直接变 CAPTURED)

别踩这个坑 / Common Mistakes

❌ 直接调用支付网关不处理超时 — 网络超时后你不知道是否扣款成功,必须轮询查询最终状态

❌ 数据库金额用 float — 浮点数精度问题会导致 $0.01 的差异。永远用整数存分 (cents)

❌ 忽略退款流程 — 退款和收款一样复杂,需要独立设计

❌ 单点支付网关 — Stripe 也会宕机,需要备用支付通道 (Adyen, Braintree)

✅ 正确做法: 幂等键 + 整数金额 + 状态机 + 多通道降级


📚 参考资料 / References


🧒 ELI5

想象你去超市买东西,把购物清单给收银员(支付请求)。收银员把你的名字写在小本本上(幂等键),这样就算你说了两遍"我要结账",也只收你一次钱。收银员在账本上写"顾客减少 $50,超市增加 $50"(双重记账)。如果你的信用卡被盗,超市会事后发现然后退款——不会为了检查每张卡把所有人都堵在收银台前(异步欺诈检测)。

Imagine buying groceries. The cashier writes your name in a notebook (idempotency key) so even if you say "charge me" twice, you only pay once. They record "customer -$50, store +$50" in the ledger (double-entry). If your card was stolen, the store discovers it later and refunds — they don't block the checkout line to verify every card (async fraud detection).

💻 算法 / Algorithms — #981 Time Based Key-Value Store (Medium)

Day 38 · Mastery Phase · ~4 min read
🧩 二分搜索模式 (6/7) — 在上一题 Search in Rotated Sorted Array 基础上进阶

🧩 模式回顾 / Pattern Recap (6/7)

Building on the template from earlier in this block:

# 通用二分模板 / Universal Binary Search Template
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 Variation: 不是在连续数组中搜索,而是在时间戳列表中找"最大的 ≤ target 的值"(upper bound 变体)。核心洞察不变:排除一半搜索空间。

Not searching a plain array — searching a list of timestamps for the largest value ≤ target. Same core insight: eliminate half the search space each time.


题目 / Problem

🔗 LeetCode #981 🟡 Medium · 📹 NeetCode

设计一个时间版本的 key-value 存储,支持:

  • set(key, value, timestamp) — 存储 key 在某时间戳的值
  • get(key, timestamp) — 获取 key 在 ≤ timestamp 的最新值

Design a time-versioned key-value store supporting set(key, value, timestamp) and get(key, timestamp) which returns the value at the largest timestamp ≤ the given timestamp.


现实类比 / Real-World Analogy

Git 的版本历史!git log 给你所有提交(按时间排序),git checkout 让你回到"某时间点之前最新的"那次提交。这道题就是实现这个机制。

Git version history! git log gives you all commits (sorted by time), and git checkout lets you restore the most recent commit before a given time. This problem implements exactly that.


解题思路 / Approach

  1. 用 defaultdict(list) 存储每个 key 的 (timestamp, value) 列表
  2. set 时直接 append(题目保证时间戳递增,所以自然有序)
  3. get 时对时间戳列表做二分搜索,找最大的 ≤ target 的时间戳

Use defaultdict(list) to store (timestamp, value) pairs per key. set simply appends (timestamps are guaranteed increasing = naturally sorted). get binary searches the timestamp list for the largest timestamp ≤ target.


Python 解法 / Solution

from collections import defaultdict
import bisect

class TimeMap:
    def __init__(self):
        # key -> list of (timestamp, value), sorted by timestamp
        self.store = defaultdict(list)
    
    def set(self, key: str, value: str, timestamp: int) -> None:
        self.store[key].append((timestamp, value))
    
    def get(self, key: str, timestamp: int) -> str:
        if key not in self.store:
            return ""
        
        values = self.store[key]  # [(ts1, val1), (ts2, val2), ...]
        
        # Binary search: find rightmost timestamp <= target
        # bisect_right on timestamps
        left, right = 0, len(values) - 1
        result = ""
        
        while left <= right:
            mid = (left + right) // 2
            if values[mid][0] <= timestamp:
                result = values[mid][1]  # candidate — try to find closer
                left = mid + 1           # go right (want larger timestamp ≤ target)
            else:
                right = mid - 1          # too large, go left
        
        return result

# Trace example:
# set("foo", "bar", 1)  → store["foo"] = [(1, "bar")]
# set("foo", "bar2", 4) → store["foo"] = [(1, "bar"), (4, "bar2")]
# get("foo", 4) → binary search [1,4] for ≤4 → ts=4 ✓ → "bar2"
# get("foo", 3) → binary search [1,4] for ≤3 → ts=1 ✓ → "bar"
# get("foo", 0) → ts=1 > 0, right=-1 → result stays "" → ""

时间/空间复杂度:

  • set: O(1) 均摊
  • get: O(log n) — n 为该 key 的版本数
  • Space: O(n) — n 为总 set 调用次数

模板映射 / Template Mapping

标准模板中: 这道题中: arr[mid] == target → values[mid][0] == timestamp (exact match) arr[mid] < target → values[mid][0] <= timestamp → result = value; left = mid + 1 arr[mid] > target → values[mid][0] > timestamp → right = mid - 1

关键变化: 不是找精确值,而是找"最右边满足条件的"。用 result 变量记录候选答案,继续向右搜索。

Key difference: not finding exact match but the rightmost satisfying condition. Use a result variable to track the best candidate, keep searching right.


举一反三 / Pattern Connection

这个 block 中的 6 道题都是二分的不同变体:

题目变体
#704 Binary Search基础:精确查找
#74 Search a 2D Matrix把矩阵当 1D 数组
#875 Koko Eating Bananas二分答案(不是数组)
#153 Find Min in Rotated找边界点
#33 Search in Rotated判断哪半段有序
#981 Time Based KV找最右满足条件的值
#4 Median of Two Sorted下次见 😈

📚 参考资料 / References


🧒 ELI5

想象一本日记本,每页都有日期和内容。你想找"4月3日或之前最新的那页日记"。笨方法是从头翻到第4月3日。聪明方法是先翻到中间——如果中间那页是4月10日,说明太新了,往前翻;如果是3月20日,可以用它但继续往后翻找更新的。每次都排除一半,很快就找到了!

Imagine a diary with dated pages. You want "the most recent entry on or before April 3rd." Dumb way: read every page. Smart way: open to the middle — if it's April 10th, it's too new so go earlier; if it's March 20th, it's a candidate but keep looking later. Eliminate half each time — that's binary search!

🗣️ 软技能 / Soft Skills — How Do You Approach System Design Trade-offs?

Day 38 · Mastery Phase · ~2 min read

为什么这很重要 / Why This Matters

系统设计面试从来没有"标准答案"。面试官真正在评估的是:你怎么思考,不是你背了多少。能清晰表达权衡(trade-off)的工程师,才是 Senior/Staff 工程师的思维模式。

System design interviews never have "correct" answers. Interviewers assess how you think, not what you've memorized. Engineers who can articulate trade-offs clearly demonstrate Senior/Staff-level thinking.


核心框架 / Core Framework

用 PAST 框架 组织你的权衡讨论:

  • Problem: 先明确核心约束(读多写少?强一致性必须?延迟 vs 吞吐?)
  • Alternatives: 列出 2-3 个方案,不要直接跳到答案
  • Selection: 说明为什么选这个方案(基于约束)
  • Trade-offs: 主动说出你的选择放弃了什么

STAR 拆解 / STAR Breakdown

问题题干: "How do you approach system design trade-offs?"

Situation(情境):

"在设计 X 系统时,我们面临 A 和 B 两个选择..."

Task(任务):

"我需要在强一致性和高可用性之间做出决定(CAP theorem),同时满足 <1s 的 p99 延迟要求..."

Action(行动):

"我的方法是:先问清楚业务优先级——如果是金融交易,宁可不可用也不能数据不一致;如果是社交 feed,最终一致性完全可以接受。然后我会画出两种方案的架构,估算延迟和成本..."

Result(结果):

"通过明确权衡,团队在 30 分钟内达成共识,避免了后来可能的重构..."


❌ 差回答 vs ✅ 好回答

❌ Bad:

"我会选 microservices,因为它更好扩展。"

(没有说为什么,没有权衡,没有基于约束)

✅ Good:

"这取决于几个维度:团队规模、一致性要求、和现有基础设施。如果团队只有 5 人,微服务的运维复杂度会拖累交付速度;如果有强 ACID 要求,分布式事务会很痛苦。在这个场景下,我会先从 modular monolith 开始,等扩展瓶颈出现后再拆分。"

Senior/Staff 进阶技巧 / Senior/Staff Tips

  1. 主动引导对话:"我想先明确几个约束,这会决定我的方向——DAU 是多少?数据一致性要求是什么?预算约束呢?"
  1. 用数字锚定讨论:"如果写入 QPS 超过 10K,单机 MySQL 就到瓶颈了,我们需要 sharding 或 NoSQL"
  1. 预判面试官的 follow-up:"我选择了 eventual consistency,这意味着用户可能看到短暂的不一致——我们可以用 client-side cache 或 read-your-write consistency 缓解这个问题"
  1. 承认不确定性:"这我不确定,但我会这样评估..." — 这比假装确定更加分

关键要点 / Key Takeaways

  • 权衡不是背答案,是展示思维过程
  • 先问约束,再提方案——顺序很重要
  • 主动说出"我选择 X 意味着放弃 Y",比等面试官问更加分
  • Staff 工程师的标志:能把技术决策和业务影响连接起来

📚 参考资料 / References


🧒 ELI5

如果你妈问你"今晚想吃什么",说"随便"是最差的回答。好的回答是:"如果你不想花太多时间,我们吃面;如果想吃得健康,可以做沙拉;如果今天你很累,我来做简单的炒饭。你最看重哪个?"这就是系统设计权衡——先搞清楚优先级,然后给出基于约束的方案。

If your mom asks "what do you want for dinner?", "anything" is the worst answer. A good answer: "If you don't want to spend much time, noodles; if you want healthy, salad; if you're tired, I'll make easy fried rice. Which matters most?" That's system design trade-offs — clarify priorities first, then propose based on constraints.

🎨 前端 / Frontend — Code Splitting & Lazy Loading — Ship Less JS

Day 38 · Mastery Phase · ~2 min read

真实场景 / Real Scenario

你在做一个 SaaS dashboard,包含主页、分析报表、设置页、和管理员页面。用户打开首页,却要等待所有页面的 JavaScript 加载完——包括他们根本不会用的管理员功能。这就是需要 Code Splitting 的典型场景。

You're building a SaaS dashboard with home, analytics, settings, and admin pages. Users open the home page but wait for ALL pages' JavaScript to load — including admin features they'll never use. This is the classic code splitting use case.


核心概念 / Core Concept

Code Splitting(代码分割) = 把 JS bundle 拆成多个小文件,按需加载

Lazy Loading(懒加载) = 用户访问某页面/组件时才加载对应代码

React 内置支持:React.lazy() + Suspense


代码示例 / Code Examples

基础用法 / Basic Usage

import React, { lazy, Suspense } from 'react';

// ❌ 同步导入 — 打包进主 bundle
// import AdminPage from './AdminPage';

// ✅ 懒加载 — 只在用户访问时才下载
const AdminPage = lazy(() => import('./AdminPage'));
const AnalyticsPage = lazy(() => import('./AnalyticsPage'));

function App() {
  return (
    // Suspense 必须包裹懒加载组件 — 显示 loading 状态
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/admin" element={<AdminPage />} />
        <Route path="/analytics" element={<AnalyticsPage />} />
      </Routes>
    </Suspense>
  );
}

Next.js 中的写法 / In Next.js

import dynamic from 'next/dynamic';

// Next.js 的 dynamic() = React.lazy() 的增强版
const HeavyChart = dynamic(() => import('./HeavyChart'), {
  loading: () => <p>Loading chart...</p>,
  ssr: false,  // 禁用 SSR(对只在浏览器运行的库有用,如 D3)
});

// 也可以按条件懒加载
const AdminPanel = dynamic(() => import('./AdminPanel'), {
  ssr: false,
});

猜猜输出 / Quiz

const LazyComp = lazy(() => import('./HeavyComponent'));

function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(true)}>Show</button>
      {show && (
        <Suspense fallback={<p>Loading...</p>}>
          <LazyComp />
        </Suspense>
      )}
    </>
  );
}

首次点击 "Show" 按钮时会发生什么?

A) HeavyComponent 立即显示

B) 先显示 "Loading...",然后显示组件

C) 报错,因为 Suspense 没在 Router 里

D) 什么都不发生,需要再次点击

答案:B — React 触发网络请求下载 HeavyComponent 的 chunk,期间显示 fallback,加载完成后渲染组件。


❌ 错误做法 vs ✅ 正确做法

// ❌ 在组件内部定义 lazy — 每次渲染都重新创建
function Parent() {
  const LazyChild = lazy(() => import('./Child')); // BUG!
  return <Suspense fallback={null}><LazyChild /></Suspense>;
}

// ✅ 在模块顶层定义 — 只创建一次
const LazyChild = lazy(() => import('./Child'));

function Parent() {
  return <Suspense fallback={null}><LazyChild /></Suspense>;
}
// ❌ 忘记 Suspense boundary — 会崩溃
function App() {
  return <LazyComp />; // Error: A React component suspended without Suspense
}

// ✅ 始终包裹 Suspense
function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <LazyComp />
    </Suspense>
  );
}

什么时候用 / When to Use

✅ 适合懒加载:

  • 路由级别的页面组件(最大收益)
  • 大型图表库(ECharts、D3、recharts)
  • 富文本编辑器(Quill、TipTap)
  • 弹窗/Modal(用户不一定会打开)
  • 管理员/权限功能

⛔ 不适合懒加载:

  • 首屏必须显示的内容(会加重 LCP)
  • 非常小的组件(拆分开销 > 收益)
  • 频繁切换的 Tab 内容(每次切换都重新加载)

📚 参考资料 / References


🧒 ELI5

想象一家餐厅,菜单上有 100 道菜。笨方法是开门就把所有菜都做好放着等(用户打开网页就加载所有 JS)。聪明方法是只备好常见菜,等有人点特殊菜再做(懒加载)。这样开门快,大多数客人不用等,偶尔点特殊菜的人稍等片刻——总体体验更好。

Imagine a restaurant with 100 dishes. Dumb way: cook everything when you open (load all JS at page load). Smart way: prep common dishes, cook special ones when ordered (lazy loading). Faster opening, most customers don't wait, occasional special orders take a moment — overall better experience.

🤖 AI — Diffusion Models — How Image Generation Works

Day 38 · Mastery Phase · ~2 min read

直觉理解 / Intuition

你有没有想过,Midjourney 和 DALL-E 怎么从文字生成图片?

它们用的是扩散模型(Diffusion Models)。核心思路出乎意料地简单:先把图片弄乱,再学怎么恢复。

Ever wonder how Midjourney and DALL-E generate images from text? They use Diffusion Models. The core idea is surprisingly simple: first learn to destroy an image, then learn to reverse that destruction.


工作原理 / How It Works

两个阶段 / Two Phases

前向扩散(加噪)/ Forward Process (Noising):

清晰图片 → [加一点噪声] → [再加一点] → ... → 纯随机噪声 Cat photo → slightly noisy → more noisy → ... → pure Gaussian noise

这个过程是固定的,不需要学习,只需要逐步加高斯噪声。经过 ~1000 步后,图片变成纯噪声。

Fixed, no learning needed. Just gradually add Gaussian noise over ~1000 steps until the image is pure noise.

反向扩散(去噪)/ Reverse Process (Denoising):

纯噪声 → [预测并去掉噪声] → ... → 清晰图片 Noise → [predict & remove noise] → ... → clean image

神经网络学习每一步应该去掉多少噪声。训练完成后,从随机噪声出发,就能生成全新图片。

The neural network learns "how much noise to remove at each step." After training, start from random noise → generate new images.


架构进化 / Architecture Evolution

早期 (2020-2021) 现在 (2024-2025) U-Net → DiT (Diffusion Transformer) 像素空间 → 潜在空间 (Latent Space) 纯噪声输入 → 文字/图像条件控制 数百步生成 → 1-4步 (Consistency Models)

关键进步 — 潜在扩散 (Latent Diffusion):

Stable Diffusion 的秘诀:先用 VAE 把图片压缩到更小的潜在空间(512×512 → 64×64),再在潜在空间做扩散。计算量减少 64 倍!

Stable Diffusion's secret: compress image to latent space first (512×512 → 64×64), then do diffusion there. 64x less computation!


可运行代码 / Runnable Python

# pip install diffusers transformers torch accelerate
from diffusers import StableDiffusionPipeline
import torch

# Load Stable Diffusion (uses latent diffusion internally)
pipe = StableDiffusionPipeline.from_pretrained(
    "runwayml/stable-diffusion-v1-5",
    torch_dtype=torch.float16
).to("cuda")  # use "cpu" if no GPU

# Generate from text prompt
image = pipe(
    prompt="a futuristic city at sunset, photorealistic",
    negative_prompt="blurry, low quality",
    num_inference_steps=20,  # fewer steps = faster but lower quality
    guidance_scale=7.5,      # higher = more prompt-adherent, less creative
).images[0]

image.save("output.png")
print("Done! Check output.png")

文字引导的秘密 / The Secret of Text Guidance

纯扩散模型生成随机图片。CLIP + Classifier-Free Guidance 让它生成"符合描述的"图片:

训练时,模型同时学习"有文字条件"和"无文字条件"的去噪。生成时:

最终噪声预测 = 无条件预测 + guidance_scale × (有条件预测 - 无条件预测)

guidance_scale 越大,图片越贴近文字描述,但多样性降低。

CLIP + Classifier-Free Guidance: during generation, blend conditional and unconditional predictions. Higher guidance_scale = more prompt-adherent but less diverse.


应用 / Applications

  • 🎨 图像生成:Midjourney、DALL-E 3、Stable Diffusion
  • 🎬 视频生成:Sora、Runway Gen-3
  • 🧬 蛋白质结构预测(RoseTTAFold Diffusion)
  • 💊 药物分子设计
  • 🗣️ 语音合成:Voicebox (Meta)

📚 参考资料 / References


🧒 ELI5

想象你要学画猫。聪明的方法:找1000张猫的照片,把每张都逐渐涂上白色(加噪声),直到完全看不出来。然后训练自己"从白色一点点还原出猫"(去噪)。练熟了之后,给你一张纯白纸,你就能凭空画出一只猫!扩散模型就是这么工作的——它学会了"从混乱中创造秩序"。

Imagine learning to draw cats. Smart approach: take 1000 cat photos, gradually paint them white (add noise) until unrecognizable. Then train yourself to "restore the cat from white" (denoise). Once mastered, give you a blank white page — you can draw a cat from scratch! Diffusion models work exactly this way — they learn to create order from chaos.

Attention Heatmap

When the model processes it, it attends most strongly to animal.

Theanimaldidntcrossthestreetbecauseitwastootired
it →