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

🏗️ 系统设计 Day 16 / System Design Day 16

场景 / Scenario

想象你在设计一个公共 API(比如 Twitter API 或 OpenAI API)。如果某个用户或恶意脚本在一秒内发送了 10,000 个请求,你的服务器可能会崩溃,其他正常用户也会受到影响。你需要一个“限流器”(Rate Limiter)来控制每个用户在特定时间内的请求数量。

Imagine you are designing a public API (like Twitter API or OpenAI API). If a user or malicious script sends 10,000 requests in one second, your server might crash, affecting other legitimate users. You need a "Rate Limiter" to control the number of requests each user can make within a specific time window.

架构图 / Architecture Diagram

[ Client ] 
    │ (1) Request
    ▼
[ API Gateway / Load Balancer ]
    │ (2) Check Rate Limit
    ├──► [ Rate Limiter Cache (Redis) ]
    │      ◄── (3) Allow / Deny
    ▼
(4) If Allow: [ Backend API Servers ]
(4) If Deny:  Return 429 Too Many Requests

核心权衡 / Key Tradeoffs

为什么这样设计? / Why design it this way?

  1. Where to put it? (放在哪里?): 通常放在 API Gateway(网关),而不是后端应用服务器。这样可以在流量到达核心业务逻辑之前将其拦截,节省资源。
  2. Storage (存储): 使用像 Redis 这样的内存缓存(In-memory Cache),而不是关系型数据库。因为限流需要极低的延迟(Low Latency),并且 Redis 支持原子操作(如 INCR 和 EXPIRE)。
  3. Algorithms (算法选择):

- Token Bucket (令牌桶): 最常用(Stripe, Amazon 使用)。允许一定程度的突发流量(Burst traffic)。

- Sliding Window (滑动窗口): 更精确,但内存占用稍高(Cloudflare 使用)。

常见误区 / Common Mistakes

  • ❌ 使用单机内存 (Using local memory in a distributed system): 如果你有 5 台网关服务器,每台自己记录限流,那么用户总共可以发送 5 倍的请求。必须使用集中式缓存(如 Redis)。
  • ❌ 同步阻塞请求 (Synchronous blocking): 检查限流的过程必须极快,否则限流器本身会成为性能瓶颈。

📚 延伸阅读 / References

🧒 ELI5 (Explain Like I'm 5)

限流器就像游乐园的检票员。如果你买的是“每小时只能玩 3 次”的票,检票员会记录你玩的次数。如果你这小时已经玩了 3 次还想玩,检票员会让你在旁边等(返回 429 错误),直到下一个小时重新开始。

A rate limiter is like a ticket checker at an amusement park. If your ticket says "3 rides per hour", the checker tallies your rides. If you try to ride a 4th time in that hour, they tell you to wait (returning a 429 error) until the next hour starts.

💻 算法 Day 17 / Algorithms Day 17 — #424 Longest Repeating Character Replacement (Medium) — Sliding Window

🧩 滑动窗口模式 (3/6) — building on the template from Day 16

今天我们将继续使用滑动窗口模式。这道题的特别之处在于,窗口的“有效性”不是由简单的总和决定的,而是由“窗口内最多出现的字符”和“允许替换的次数”共同决定的。

Today we continue with the Sliding Window pattern. The twist here is that the window's "validity" isn't determined by a simple sum, but by the "most frequent character in the window" combined with the "allowed replacements".

🔗 题目链接 / Links

💡 现实场景 / Real-world Analogy

想象你在玩拼字游戏,你有一排字母,并且你有 k 张“万能牌”(可以变成任何字母)。你想弄出最长的一段完全相同的字母。你会怎么做?你会找一段本身就有很多相同字母的区域,然后把里面少数不同的字母用万能牌替换掉。

Imagine playing Scrabble. You have a row of letters and k "wildcards" (can be any letter). You want to make the longest contiguous sequence of the same letter. What do you do? You find a section that already has a lot of the same letter, and use your wildcards to replace the few different ones.

🧠 映射到模版 / Map to Pattern Template

  • 窗口扩张 (Expand): 右指针 right 向右移动,将新字符加入计数器。
  • 违规条件 (Condition Violated): 窗口长度 - 窗口内出现次数最多的字符的数量 > k。这意味着即使把所有其他字符都替换掉,替换次数 k 也不够用了。
  • 窗口收缩 (Shrink): 左指针 left 向右移动,将移出窗口的字符从计数器中减去。

💻 Python 实现 / Python Solution

class Solution:
    def characterReplacement(self, s: str, k: int) -> int:
        count = {}
        res = 0
        left = 0
        maxf = 0 # Track the count of the most frequent character
        
        for right in range(len(s)):
            # Expand window
            count[s[right]] = 1 + count.get(s[right], 0)
            # Optimization: we only care about the historical max frequency
            maxf = max(maxf, count[s[right]])
            
            # Condition violated: replacements needed > k
            while (right - left + 1) - maxf > k:
                # Shrink window
                count[s[left]] -= 1
                left += 1
                
            res = max(res, right - left + 1)
            
        return res

⏱️ 复杂度 / Complexity

  • Time: $O(N)$。右指针遍历一次字符串,左指针最多遍历一次。
  • Space: $O(26) = O(1)$。哈希表最多存储 26 个大写英文字母。

🔄 举一反三 / Connect to Pattern

这道题与 #3 Longest Substring Without Repeating Characters 类似,都是在窗口不满足条件时移动左指针。区别在于 #3 的条件是“有重复字符”,而本题的条件是“需要替换的字符超过了 k”。

📚 延伸阅读 / References

🧒 ELI5 (Explain Like I'm 5)

你想用积木搭一座纯色的塔,但你只有 k 块可以变色的魔法积木。你圈出一堆积木(窗口),数数里面哪种颜色最多。如果总积木数减去最多颜色的积木数,比你的魔法积木 k 还要多,说明魔法不够用了,你就得把圈子左边的积木扔出去,直到魔法够用为止。

🗣️ 软技能 Day 16 / Soft Skills Day 16

❓ 面试问题 / The Question

"Tell me about a time your team had a production outage. What was your role?"

(告诉我一次你们团队遇到生产环境故障的经历。你的角色是什么?)

💡 为什么问这个? / Why this matters

面试官想看你的危机处理能力(Crisis Management)、责任心(Ownership)以及事后复盘(Post-mortem/Blameless Culture)的态度。高级/Staff 工程师不仅要能修 Bug,还要能稳住阵脚、协调沟通,并确保问题不再发生。

The interviewer wants to see your crisis management skills, ownership, and attitude towards post-mortems (blameless culture). Senior/Staff engineers don't just fix bugs; they stabilize the situation, coordinate communication, and ensure the issue doesn't recur.

📝 STAR 拆解 / STAR Breakdown

  • Situation (情境): 描述故障的严重程度(例如:支付网关宕机,影响了 20% 的用户)。
  • Task (任务): 你需要做什么(快速止血、定位根本原因、通知利益相关者)。
  • Action (行动): 你的具体贡献。你是否主导了回滚(Rollback)?你是否在 Slack 上组织了沟通?你是否查看了 Datadog/Splunk 日志?
  • Result (结果): 故障恢复时间(MTTR)。更重要的是,事后复盘(Post-mortem)得出了什么 Action Items(例如:增加监控告警、改进 CI/CD 流程)。

❌ 错误示范 vs ✅ 正确示范

  • ❌ Bad: "服务器挂了,我查了半天日志发现是别人的代码有问题,然后我让他改了。" (Blaming others, no systemic improvement / 甩锅,没有系统性改进)
  • ✅ Good: "我们在发布新版本后 CPU 飙升导致服务不可用。我立刻提议先回滚(Rollback)止血。恢复后,我主导了排查,发现是一个低效的正则引起的。事后我组织了无指责复盘(Blameless Post-mortem),并推动在 CI 中加入了性能回归测试。" (Focus on mitigation first, blameless culture, systemic fix / 强调先止血、无指责文化、系统性修复)

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

高级工程师在故障中通常扮演 "Incident Commander"(故障指挥官)的角色。强调你如何隔离沟通噪音(让排查的人专心排查,你负责向管理层汇报进度),以及你如何推动架构层面的容灾设计(Resiliency)。

🔑 核心收获 / Key Takeaways

  1. Mitigation over Investigation (先止血,后排查): 回滚永远是第一选择。
  2. Blameless Culture (对事不对人): 故障是系统性问题,不是个人的错。
  3. Actionable Post-mortems (可执行的复盘): 故障的价值在于改进系统。

📚 延伸阅读 / References

🧒 ELI5 (Explain Like I'm 5)

就像厨房突然起火了。初级厨师可能会慌张地找是谁弄倒了油瓶;高级主厨会立刻拿灭火器把火扑灭(止血),然后安抚外面的客人(沟通),最后大家坐下来讨论怎么把灶台改得更安全(复盘),而不是开除那个弄倒油瓶的人。

🎨 前端 Day 16 / Frontend Day 16

💡 场景 / Scenario

你在做一个 Dashboard,里面有图表组件、用户信息组件和列表组件。如果图表组件因为后端返回了脏数据而抛出 JavaScript 错误,整个页面都会变成白屏!你需要一种方法,让报错的组件只显示一个“加载失败”的提示,而不影响页面上其他正常的组件。同时,对于加载慢的组件,你想优雅地显示一个 Loading 骨架屏。

You're building a dashboard with charts, user info, and lists. If the chart component throws a JS error due to bad backend data, the entire page goes blank! You need a way to catch the error locally so only the chart shows an "Error" state, keeping the rest of the app alive. Also, for slow components, you want to elegantly show a loading skeleton.

💻 核心概念 / Core Concept: Error Boundaries & Suspense

React 提供了两个强大的边界组件来处理异步加载和错误状态:

  1. Error Boundary (错误边界): 捕获子组件树中的 JS 错误,渲染备用 UI。
  2. Suspense (悬念): 在子组件准备好(例如加载代码分割的 chunk 或获取数据)之前,渲染后备 UI(如 Loading Spinner)。
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary'; // 推荐使用这个库

// 懒加载重型组件
const HeavyChart = React.lazy(() => import('./HeavyChart'));

function Dashboard() {
  return (
    <div className="dashboard">
      <Sidebar />
      <main>
        {/* 错误边界:如果 Chart 崩溃,只在这里显示错误,不影响 Sidebar */}
        <ErrorBoundary fallback={<div className="error">图表加载失败</div>}>
          {/* Suspense:在 Chart 代码下载完成前显示 Loading */}
          <Suspense fallback={<div className="skeleton-loader">Loading Chart...</div>}>
            <HeavyChart />
          </Suspense>
        </ErrorBoundary>
      </main>
    </div>
  );
}

🧠 猜猜输出什么? / Quiz

如果 HeavyChart 内部的 useEffect 里面有一个 setTimeout,在 2 秒后抛出了一个错误,ErrorBoundary 能捕获到吗?

A) 能

B) 不能

(答案在底部 / Answer at the bottom)

❌ 常见误区 vs ✅ 正确做法

  • ❌ Bad: 在每个组件内部写大量的 try/catch 和 if (loading) 逻辑,导致 UI 逻辑和数据逻辑严重耦合。
  • ✅ Good: 使用声明式的 <Suspense> 和 <ErrorBoundary> 将加载状态和错误处理提升到父级,让业务组件只关注快乐路径(Happy Path)。

⚖️ 何时使用 / When to use

  • 使用 Error Boundaries: 在应用的顶层(捕获致命错误),以及在主要的布局区块(如侧边栏、主内容区)周围,防止局部崩溃导致全局白屏。
  • 使用 Suspense: 配合 React.lazy 进行代码分割(Code Splitting),或者在使用 Next.js 13+ App Router 时处理服务器组件的数据获取。

📚 延伸阅读 / References

🧒 ELI5 (Explain Like I'm 5)

Suspense 就像餐厅的叫号器。你点了一份需要做很久的大餐(重型组件),服务员先给你一个叫号器(Loading 状态),等菜做好了再给你端上来。

Error Boundary 就像船上的水密舱。如果船底破了一个洞(组件报错),水密舱门会关上,把水限制在一个小房间里,这样整艘船(整个网页)就不会沉没。


Quiz Answer: B) 不能。Error Boundary 只能捕获渲染期间、生命周期方法和子组件构造函数中的错误。它不能捕获事件处理器(Event Handlers)、异步代码(如 setTimeout 或 fetch 回调)中的错误。对于异步错误,你需要手动 catch 并设置状态,或者抛出给 Error Boundary。

🤖 AI Day 18

🧠 核心概念 / Core Concept: Vector Databases (向量数据库)

传统数据库(如 MySQL、PostgreSQL)擅长精确匹配(Exact Match):"找到名字叫 'Alice' 的用户"。

但在 AI 时代,我们需要语义搜索(Semantic Search):"找到和 '一只可爱的橘猫' 意思相近的图片或文本"。

这就是向量数据库(Vector Databases)的用武之地。它们专门用于存储和快速检索高维向量(Embeddings)。

Traditional databases excel at exact matches. In the AI era, we need semantic search. Vector databases are purpose-built to store and quickly retrieve high-dimensional vectors (embeddings).

⚙️ 它是如何工作的? / How it works

  1. Embedding: 将文本、图像或音频通过 AI 模型(如 OpenAI 的 text-embedding-ada-002)转换为一串数字(向量)。
  2. Storage: 向量数据库存储这些数字串。
  3. Query: 当你搜索时,将你的搜索词也变成向量。
  4. Similarity Search: 数据库计算搜索向量与存储向量之间的“距离”(如余弦相似度 Cosine Similarity)。距离越近,语义越相似。

为了在数百万个向量中快速找到最相似的,向量数据库使用了近似最近邻(ANN, Approximate Nearest Neighbor)算法,如 HNSW (Hierarchical Navigable Small World)。

🛠️ 常见工具 / Common Tools

  • Pinecone: 完全托管的云原生向量数据库,闭源,极其易用。
  • Weaviate / Milvus / Qdrant: 开源的向量数据库,支持自托管。
  • pgvector: PostgreSQL 的一个扩展插件。如果你已经在使用 Postgres,加上这个插件就能直接获得向量搜索能力,无需引入新的基础设施。

💻 Python 实战 / Runnable Python Snippet

这里使用内存中的轻量级向量库 ChromaDB 来演示:

# pip install chromadb
import chromadb

# 1. Initialize local memory client
client = chromadb.Client()
collection = client.create_collection("my_documents")

# 2. Add documents (Chroma automatically handles embedding under the hood)
collection.add(
    documents=["The cat is sleeping on the sofa", "Dogs love to play fetch in the park", "I like to eat pizza"],
    metadatas=[{"source": "cat_book"}, {"source": "dog_book"}, {"source": "food_blog"}],
    ids=["id1", "id2", "id3"]
)

# 3. Query by semantics
results = collection.query(
    query_texts=["Tell me about pets"],
    n_results=2
)

print(results["documents"]) 
# Output will likely prioritize the cat and dog sentences over the pizza one!

📚 延伸阅读 / References

🧒 ELI5 (Explain Like I'm 5)

想象一个巨大的图书馆,传统数据库是按字母顺序排列书名的目录柜。如果你拼错了一个字母,就找不到书。

向量数据库就像一个超级聪明的图书管理员。你对他说:“我想找一本关于太空旅行和外星人的小说,风格有点像星球大战。” 他不需要精确的书名,就能直接把你带到最符合你描述的书架前。