0Pricing
DSA Interview Prep · 课时

缓存、CDN 与负载均衡

添加 Redis 缓存层,将静态资源推送到 CDN,并使用轮询和一致性哈希负载均衡器将流量分配到各个副本。

缓存、CDN 与负载均衡 是 CoddyKit 上的免费 DSA Interview Prep 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 DSA Interview Prep 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 DSA Interview Prep 课程共包含 4 节课。

缓存为何是大规模系统的必需品

缓存会将频繁访问的数据副本存储在更快的存储层中,从而无需访问较慢的后端存储(数据库、外部接口)即可处理后续请求。在大规模场景下,少量热门项目会收到绝大多数请求——通常适用二八法则(帕累托法则):20% 的项目会带来 80% 的流量。

能够将热门的 20% 数据放入内存的缓存,可以吸收 80% 的数据库负载。因此,增加 Redis 缓存通常可以将数据库 CPU 使用率降低 70–90%,并将缓存命中时的 p99 延迟从 10ms 降至 1ms 以下,同时无需大幅修改数据库或应用逻辑。

# Demonstrating the 80/20 caching benefit
import random

# Simulate 1000 requests to 100 items with Zipf-like distribution
def zipf_sample(n_items, n_requests):
    access_counts = {}
    weights = [1.0 / (i + 1) for i in range(n_items)]  # Zipf: item 0 most popular
    total = sum(weights)
    probs = [w / total for w in weights]
    for _ in range(n_requests):
        item = random.choices(range(n_items), weights=probs)[0]
        access_counts[item] = access_counts.get(item, 0) + 1
    return access_counts

random.seed(42)
counts = zipf_sample(100, 10000)
top_20_items = sorted(counts, key=counts.get, reverse=True)[:20]
top_20_requests = sum(counts[i] for i in top_20_items)
print(f'Top 20% of items ({20} of 100) handle {top_20_requests/100:.1f}% of requests')

旁路缓存模式(延迟加载)

旁路缓存模式(也称为延迟加载)是最常见的缓存策略。应用代码负责管理缓存:读取时先检查缓存。缓存命中时立即返回。缓存未命中时,从数据库获取数据,写入缓存,然后返回。写入时,更新数据库,并对缓存条目执行失效处理(delete),以便下一次读取时刷新该条目。

这种模式可以确保缓存只保存实际被请求的数据(不会不必要地预加载),并通过失效机制与数据库保持一致。其权衡是:缓存未命中后的首次访问需要承担完整的数据库开销(冷启动)。

# Cache-aside pattern in Python
class CacheAsideService:
    def __init__(self, db, cache):
        self.db = db
        self.cache = cache   # e.g., Redis client

    def get_user(self, user_id):
        cache_key = f'user:{user_id}'
        # 1. Check cache
        cached = self.cache.get(cache_key)
        if cached:
            return cached    # cache hit
        # 2. Cache miss: fetch from DB
        user = self.db.query('SELECT * FROM users WHERE id=%s', user_id)
        # 3. Write to cache with TTL
        self.cache.set(cache_key, user, ttl=3600)  # 1 hour TTL
        return user

    def update_user(self, user_id, data):
        # 1. Write to DB
        self.db.execute('UPDATE users SET ... WHERE id=%s', user_id, data)
        # 2. Invalidate cache (delete, not update)
        self.cache.delete(f'user:{user_id}')
        # Next read will re-populate cache from DB

print('Cache-aside: READ from cache, miss? load from DB + write cache')
print('         WRITE to DB, then DELETE from cache (invalidate)')

写穿透与写后缓存

写穿透缓存:每次写入时同步更新数据库和缓存。缓存始终包含最新数据。权衡是:写入速度较慢(需要执行两项操作),并且缓存可能会填充一些之后再也不会读取的数据。

写后缓存(回写):写入时仅更新缓存,随后异步刷新到数据库。这会使写入速度极快,但如果缓存尚未刷新就发生故障,可能导致数据丢失。这种方式适用于写密集型负载,且场景允许丢失部分数据(例如浏览量计数器、分析数据)。

# Write-through vs Write-behind comparison
strategies = {
    'Cache-aside (Lazy)': {
        'read':  'Check cache; miss => DB + populate cache',
        'write': 'Write DB; delete from cache (invalidate)',
        'consistency': 'Strong (invalidation ensures freshness)',
        'write_latency': 'Fast (one DB write)',
        'risk': 'Cache stampede on popular key expiry',
    },
    'Write-through': {
        'read':  'Always check cache; miss => DB',
        'write': 'Write DB AND cache atomically',
        'consistency': 'Strong (cache always has latest)',
        'write_latency': 'Slower (two writes per operation)',
        'risk': 'Cache polluted with rarely-read data',
    },
    'Write-behind': {
        'read':  'Check cache; miss => DB',
        'write': 'Write cache only; async flush to DB',
        'consistency': 'Eventual (flush may be delayed)',
        'write_latency': 'Very fast (cache write only)',
        'risk': 'Data loss if cache crashes before flush',
    },
}
for name, info in strategies.items():
    print(f'\n{name}:')
    for k, v in info.items(): print(f'  {k}: {v}')

缓存淘汰策略

缓存已满时,淘汰策略会决定要移除哪个条目。最常见的策略包括:

  • LRU(最近最少使用):淘汰最长时间未被访问的条目。适用于具有时间局部性的负载。Redis 默认使用该策略。
  • LFU(最不经常使用):淘汰访问次数最少的条目。对于某些项目长期热门、而 LRU 无法捕捉这种特征的负载,该策略表现更好。
  • FIFO:淘汰最早插入的条目。实现简单,但对于典型网络负载性能较差。
  • 随机:淘汰随机条目。在超大缓存中,实际表现出人意料地接近 LRU,竞争力很强。
# Implementing LRU cache
from collections import OrderedDict

class LRUCache:
    def __init__(self, capacity):
        self.capacity = capacity
        self.cache = OrderedDict()  # maintains insertion/access order

    def get(self, key):
        if key not in self.cache:
            return -1
        self.cache.move_to_end(key)   # mark as recently used
        return self.cache[key]

    def put(self, key, value):
        if key in self.cache:
            self.cache.move_to_end(key)
        self.cache[key] = value
        if len(self.cache) > self.capacity:
            self.cache.popitem(last=False)   # evict LRU (oldest)

cache = LRUCache(3)
for k, v in [('a',1),('b',2),('c',3)]:
    cache.put(k, v)
print('Get a:', cache.get('a'))   # 1 (a now most recently used)
cache.put('d', 4)                  # evicts 'b' (LRU)
print('Get b:', cache.get('b'))   # -1 (evicted)
print('Get c:', cache.get('c'))   # 3

内容分发网络(CDN)

CDN 是一种按地理位置分布的边缘服务器网络(存在点,PoPs),用于将静态和动态内容缓存在靠近终端用户的位置。用户的请求不必都跨越网络前往同一个数据中心中的源站服务器,而是由距离最近的 PoP 边缘节点提供内容,从而将延迟从约 200 毫秒(跨大陆)降低到约 5 毫秒(附近 PoP)。

CDN 对以下内容至关重要:静态资源(图像、CSS、JS)、视频流媒体(HLS 分片),以及日益增多的接口响应和服务器渲染的 HTML。CDN 会检查其边缘缓存;缓存未命中时,它会从源站获取内容,并将其缓存起来供后续请求使用。

# CDN architecture flow
cdn_flow = [
    'User requests https://example.com/image.jpg',
    'DNS resolves to the nearest CDN PoP (e.g., Frankfurt for EU users)',
    'CDN edge checks its local cache:',
    '  HIT:  Return cached image directly (5ms latency)',
    '  MISS: Fetch from origin server (e.g., AWS S3 in us-east-1)',
    '        Cache image at edge with Cache-Control: max-age=86400',
    '        Future requests for this image served from edge (HIT)',
    'Cache-Control headers control CDN behaviour:',
    '  max-age=31536000 s-maxage=31536000  -- cache 1 year',
    '  no-cache                             -- always revalidate',
    '  private                              -- CDN must not cache (user-specific)',
]
for step in cdn_flow:
    print(step)

print('\nCDN providers: Cloudflare, AWS CloudFront, Fastly, Akamai')

负载均衡:分配流量

负载均衡器 会将传入请求分配到多个后端服务器,避免任何单台服务器成为瓶颈。它还提供高可用性:如果一台服务器发生故障,负载均衡器会自动将流量路由到健康的服务器(每 5–30 秒执行一次健康检查)。

负载均衡器可以运行在不同的 OSI 层:第 4 层(传输层——按 IP/端口路由,速度非常快)和第 7 层(应用层——按网址路径、标头和 Cookie 路由,从而实现更智能的流量路由)。AWS ALB、Nginx 和 HAProxy 是常见的第 7 层负载均衡器。AWS NLB 是第 4 层负载均衡器。

# Load balancing algorithms
algorithms = {
    'Round Robin': {
        'how': 'Rotate through servers in sequence',
        'best_for': 'Stateless servers with similar capacity',
        'weakness': 'Does not account for server load or response time',
    },
    'Weighted Round Robin': {
        'how': 'Round robin but servers with more capacity get more requests',
        'best_for': 'Heterogeneous server fleet',
        'weakness': 'Static weights; does not adapt to runtime load',
    },
    'Least Connections': {
        'how': 'Send to server with fewest active connections',
        'best_for': 'Long-lived connections (WebSocket, streaming)',
        'weakness': 'More complex tracking of connection state',
    },
    'Consistent Hashing': {
        'how': 'Hash request key (user_id, session) to server',
        'best_for': 'Sticky sessions, cache locality per server',
        'weakness': 'Uneven distribution if hash space is not balanced',
    },
    'Random': {
        'how': 'Choose server at random',
        'best_for': 'Simple stateless workloads',
        'weakness': 'No guarantee of load balance in short windows',
    },
}
for alg, info in algorithms.items():
    print(f'{alg}: {info["how"]}')

一致性哈希:添加和移除节点

一致性哈希解决了添加或移除服务器时重新分配缓存键的问题。在朴素的取模哈希中(server = hash(key) % n),改变 n 会导致几乎所有键都被重新映射,从而造成缓存击穿。一致性哈希会将键和服务器都映射到一个环上;每个键都由顺时针方向距离最近的服务器提供服务。添加一台服务器时,只有新服务器与其前驱服务器之间的键需要重新映射,约占全部键的 1/n。

虚拟节点可以改善负载分布:每台物理服务器在环上分配多个位置,因此即使服务器数量较少,键也能更加均匀地分布。

import hashlib
import bisect

class ConsistentHashRing:
    def __init__(self, replicas=100):
        self.replicas = replicas      # virtual nodes per server
        self.ring = {}
        self.sorted_keys = []

    def add_server(self, server):
        for i in range(self.replicas):
            key = int(hashlib.md5(f'{server}:{i}'.encode()).hexdigest(), 16)
            self.ring[key] = server
            bisect.insort(self.sorted_keys, key)

    def remove_server(self, server):
        for i in range(self.replicas):
            key = int(hashlib.md5(f'{server}:{i}'.encode()).hexdigest(), 16)
            del self.ring[key]
            self.sorted_keys.remove(key)

    def get_server(self, item):
        key = int(hashlib.md5(item.encode()).hexdigest(), 16)
        idx = bisect.bisect(self.sorted_keys, key) % len(self.sorted_keys)
        return self.ring[self.sorted_keys[idx]]

ring = ConsistentHashRing()
for s in ['server-1', 'server-2', 'server-3']:
    ring.add_server(s)
for item in ['user:1', 'user:2', 'product:abc', 'session:xyz']:
    print(f'{item} => {ring.get_server(item)}')

缓存击穿及其解决方案

缓存击穿(也称惊群效应)发生在热门缓存条目过期时:大量并发请求同时无法命中缓存,将同一个查询一齐发送到数据库,造成数据库负载骤增。解决方案包括:

  • 互斥锁:只有一个请求计算该值,其他请求等待
  • 概率提前过期:在 TTL 到期前稍早的时候,由请求随机决定是否刷新缓存,避免缓存同时过期
  • 提供旧内容并重新验证:立即提供旧内容,同时异步刷新缓存
  • 后台刷新:由独立进程在热门键过期前刷新它们
import time, threading, random

# Probabilistic early expiry (XFetch algorithm)
class ProbabilisticCache:
    def __init__(self):
        self._cache = {}

    def get(self, key, ttl, recompute_fn, beta=1.0):
        if key in self._cache:
            value, expiry, delta = self._cache[key]
            # XFetch: decide to refresh early with probability proportional to delta/TTL
            remaining = expiry - time.time()
            if remaining > 0:
                early_refresh_score = delta * beta * (-1) * (remaining / ttl)
                if random.random() > (1 - early_refresh_score):  # simplified
                    pass  # could trigger async refresh here
                return value
        # Cache miss or expired
        start = time.time()
        value = recompute_fn()
        delta = time.time() - start          # computation time
        expiry = time.time() + ttl
        self._cache[key] = (value, expiry, delta)
        return value

print('XFetch: refresh probabilistically before expiry based on computation cost')
print('High-cost computations => refresh earlier to avoid stampede')
print('Low-cost computations => refresh closer to TTL')

CDN 缓存失效

缓存失效一直是出了名的难题:“计算机科学中只有两个难题:缓存失效和命名。”当源站的内容发生变化时,CDN 边缘节点必须提供新版本。常用策略包括:

  • 基于 TTL 的过期:让内容自然过期(简单,但会存在旧内容窗口)
  • 网址版本化:将内容哈希嵌入网址(例如 main.a3f2b.js);新内容使用新网址,无需执行失效操作
  • CDN 接口清除:部署后通过接口调用显式清除网址缓存(速度快,但需要集成 CDN 接口)
# Cache invalidation strategies for CDN/browser
strategies = [
    {
        'name': 'Long TTL + URL versioning (best for static assets)',
        'example': '<script src="/app.a3f2b1c.js"></script>',
        'ttl': 'Cache-Control: max-age=31536000 (1 year)',
        'how': 'Content hash in filename; new deploy = new URL; old URL cached forever (OK)',
    },
    {
        'name': 'Short TTL (for frequently changing content)',
        'example': '/api/v1/config',
        'ttl': 'Cache-Control: max-age=60 (1 minute)',
        'how': 'Simple; content is at most 60s stale; no invalidation needed',
    },
    {
        'name': 'CDN API purge (for news / social media)',
        'example': '/news/breaking-story.html',
        'ttl': 'Cache-Control: s-maxage=3600',
        'how': 'On publish, call CDN.purge(url); edge serves new version immediately',
    },
]
for s in strategies:
    print(f'{s["name"]}:')
    print(f'  Example: {s["example"]}')
    print(f'  TTL: {s["ttl"]}')
    print(f'  Strategy: {s["how"]}\n')

架构:整合所有部分

一个充分扩展的 Web 应用层会同时使用这三种技术:负载均衡分配流量,CDN 吸收静态请求和可缓存的接口请求,而 Redis 缓存动态数据。数据库只会接收到缓存未命中的请求,通常仅占全部请求的 5%–20%。

对于读请求占主导的接口,典型请求流程如下:用户 → DNS → CDN 边缘节点(缓存命中:立即提供)→ CDN 未命中 → 负载均衡器 → 应用服务器池 → Redis 缓存(命中:1 毫秒响应)→ Redis 未命中 → 数据库(10–50 毫秒)→ 将响应缓存到 Redis,并可选择性地缓存到 CDN → 用户。每一层都会显著降低数据库负载。

# Request flow with cache hit rates
request_flow = [
    ('Browser Cache',       '10%',  '0ms',   'Browser caches GET responses per Cache-Control'),
    ('CDN Edge Cache',      '60%',  '5ms',   'CloudFront/Fastly caches cacheable API responses'),
    ('Load Balancer',        None,  '1ms',   'Routes to healthy app server replica'),
    ('App Server',           None,  '2ms',   'Business logic, auth check'),
    ('Redis Cache',         '25%',  '1ms',   'Caches computed data, hot DB rows'),
    ('Database Read Replica','5%',  '10ms',  'Cache miss: query read replica'),
    ('Database Primary',    '0.1%', '15ms',  'Cache+replica miss: query primary (rare for reads)'),
]
print(f'{'Layer':30s} {'Hit Rate':10s} {'Latency':10s} {'Notes'}')
print('-'*80)
for layer, hit_rate, latency, note in request_flow:
    hr = hit_rate if hit_rate else '-'
    print(f'{layer:30s} {hr:10s} {latency:10s} {note}')
print('\nResult: DB sees ~5% of requests; Redis sees ~25%; CDN absorbs 60%; browser 10%')

面试技巧:缓存与负载均衡

在系统设计面试中讨论缓存时,务必说明:缓存什么(热门数据、计算成本高的结果)、在哪里缓存(浏览器、CDN、应用层、数据库查询缓存)、何时使缓存失效(写入时、TTL 到期时或通过后台刷新),以及可以接受哪些一致性保证。缓存会引入一致性窗口,请明确说明这一点。

讨论负载均衡时,请提及算法选择、健康检查、粘性会话(如果需要),以及是否可以对无状态应用服务器进行横向扩展。如果应用具有状态(WebSocket 连接、会话),请说明如何在各个副本之间管理这些状态。

# Caching design questions checklist
cache_checklist = [
    'What data to cache? (read-heavy, expensive to compute, rarely updated)',
    'Cache layer: client-side / CDN / app-level / DB query cache?',
    'Cache invalidation strategy: TTL / event-driven / write-through?',
    'Eviction policy: LRU / LFU?',
    'Cache key design: ensure uniqueness, avoid hotspots',
    'Consistency window: acceptable staleness in seconds?',
    'Cache stampede prevention: mutex / stale-while-revalidate?',
    'Cache capacity: how much RAM needed for hot set?',
]
lb_checklist = [
    'Layer 4 vs Layer 7: routing by IP or by URL/headers?',
    'Algorithm: round robin / least-connections / consistent hashing?',
    'Health checks: interval, failure threshold, recovery',
    'Session stickiness: needed? Use cookie-based affinity or external session store',
    'Auto-scaling: scale out when CPU > 70%; scale in when < 30%',
]
print('Cache checklist:')
for item in cache_checklist: print(f'  [ ] {item}')
print('\nLoad balancer checklist:')
for item in lb_checklist: print(f'  [ ] {item}')

快速检查

测试您对本课中数据结构与算法——编程面试准备相关概念的理解。

课程回顾

本课您学到了:缓存会将热门数据存储在快速内存层(Redis、CDN)中,从而吸收大多数读取请求并降低数据库负载;旁路缓存是最常见的模式——未命中意味着从数据库加载,命中意味着立即返回,写入意味着删除缓存;以及一致性哈希会在节点之间分配缓存键,因此添加或移除节点时只需重新映射约 1/n 的键,而不必重新映射全部键。接下来,我们将设计限流器和 Twitter 信息流,在端到端问题中应用所有系统设计概念。

常见问题解答

「缓存、CDN 与负载均衡」课时是免费的吗?

是的 — 「缓存、CDN 与负载均衡」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 DSA Interview Prep 课程的其余内容,请升级到 CoddyKit PRO。 DSA Interview Prep 课程共包含 4 节课。

「缓存、CDN 与负载均衡」这节课中我会学到什么?

添加 Redis 缓存层,将静态资源推送到 CDN,并使用轮询和一致性哈希负载均衡器将流量分配到各个副本。 你通过在浏览器中直接运行的动手代码来练习 DSA Interview Prep,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 DSA Interview Prep 需要有经验吗?

无需任何先前经验。CoddyKit 上的 DSA Interview Prep 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「缓存、CDN 与负载均衡」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 DSA Interview Prep 课中编写并运行代码吗?

能。每节 DSA Interview Prep 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 系统设计面试框架
  2. 可扩展数据存储:SQL 与 NoSQL
  3. 缓存、CDN 与负载均衡
  4. 设计限流器与 Twitter 信息流
← 返回 DSA Interview Prep