0Pricing
DSA Interview Prep · Aula

Cache, CDNs e balanceamento de carga

Adicione camadas de cache com Redis, envie ativos estáticos para uma CDN e distribua o tráfego entre réplicas com balanceadores de carga de round-robin e de hashing consistente.

Cache, CDNs e balanceamento de carga é uma aula grátis de DSA Interview Prep no CoddyKit. Esta é a aula 3 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de DSA Interview Prep, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de DSA Interview Prep inclui 4 aulas no total.

Por que o armazenamento em cache é essencial em grande escala

O armazenamento em cache guarda cópias de dados acessados com frequência em uma camada de armazenamento mais rápida, para que solicitações futuras possam ser atendidas sem acessar o armazenamento subjacente mais lento (banco de dados, API externa). Em grande escala, um pequeno número de itens populares recebe a grande maioria das solicitações — a regra 80/20 (princípio de Pareto) costuma se aplicar: 20% dos itens respondem por 80% do tráfego.

Uma camada de armazenamento em cache que mantenha os 20% mais acessados na memória pode absorver 80% da carga do banco de dados. É por isso que adicionar um cache Redis costuma reduzir em 70–90% o uso de CPU do banco de dados e diminuir a latência p99 de 10 ms para menos de 1 ms nos acertos de cache — sem alterar significativamente o banco de dados ou a lógica da aplicação.

# 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')

Padrão de consulta ao cache (carregamento sob demanda)

O padrão de consulta ao cache (também chamado de carregamento sob demanda) é a estratégia de armazenamento em cache mais comum. O código da aplicação é responsável por gerenciar o cache: durante uma leitura, verifique primeiro o cache. Em caso de acerto no cache, retorne imediatamente. Em caso de falha no cache, busque os dados no banco de dados, grave-os no cache e depois retorne. Ao gravar, atualize o banco de dados e invalide (delete) a entrada do cache para que a próxima leitura a atualize.

Esse padrão garante que o cache contenha apenas dados que foram realmente solicitados (sem pré-carregamento desnecessário) e permaneça consistente com o banco de dados por meio da invalidação. O compromisso é que o primeiro acesso após uma falha no cache arca com o custo total do banco de dados (inicialização a frio).

# 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)')

Armazenamento em cache com gravação simultânea e posterior

Gravação simultânea: a cada gravação, atualize o banco de dados e o cache de forma síncrona. O cache sempre contém dados atualizados. Compromisso: as gravações são mais lentas (duas operações), e o cache é preenchido com dados que talvez nunca sejam lidos novamente.

Gravação posterior (gravação retroativa): ao gravar, atualize somente o cache; envie os dados ao banco de dados de forma assíncrona mais tarde. Isso torna as gravações extremamente rápidas, mas cria risco de perda de dados se o cache falhar antes do envio. É usada em cargas com muitas gravações nas quais alguma perda de dados é aceitável (por exemplo, contadores de visualizações e análises).

# 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}')

Políticas de remoção do cache

Quando o cache está cheio, a política de remoção decide qual entrada remover. As políticas mais comuns são:

  • LRU (Menos Recentemente Utilizado): remove a entrada que não é acessada há mais tempo. Apresenta bom desempenho em cargas com localidade temporal. É usada pelo Redis por padrão.
  • LFU (Menos Frequentemente Utilizado): remove a entrada acessada menos vezes. É melhor para cargas nas quais alguns itens são permanentemente populares, mas o LRU não capturaria essa característica.
  • FIFO: remove a inserção mais antiga. É simples, mas apresenta desempenho ruim em cargas de trabalho web típicas.
  • Aleatória: remove uma entrada aleatória. Na prática, é surpreendentemente competitiva com o LRU em caches muito grandes.
# 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

Redes de distribuição de conteúdo (CDNs)

Uma CDN é uma rede geograficamente distribuída de servidores de borda (pontos de presença, PoPs) que armazena em cache conteúdo estático e dinâmico próximo aos usuários finais. Em vez de a solicitação de cada usuário viajar até um servidor de origem em um único centro de dados, os nós de borda da CDN fornecem o conteúdo a partir do PoP mais próximo — reduzindo a latência de aproximadamente 200 ms (entre continentes) para aproximadamente 5 ms (em um PoP próximo).

As CDNs são essenciais para: assets estáticos (imagens, CSS, JS), transmissão de vídeo (segmentos HLS) e, cada vez mais, respostas de API e HTML renderizado no servidor. A CDN verifica seu cache de borda; quando ocorre uma falha de cache, busca o conteúdo na origem e o armazena em cache para solicitações futuras.

# 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')

Balanceamento de carga: distribuindo o tráfego

Um balanceador de carga distribui as solicitações recebidas entre vários servidores de back-end, impedindo que um único servidor se torne um gargalo. Ele também oferece alta disponibilidade: se um servidor falhar, o balanceador de carga encaminha automaticamente o tráfego para servidores saudáveis (com verificações de integridade a cada 5–30 segundos).

Os balanceadores de carga operam em diferentes camadas do OSI: camada 4 (transporte — encaminha por IP/porta, com muita rapidez) e camada 7 (aplicação — encaminha por caminho da URL, cabeçalhos e cookies, permitindo um roteamento mais inteligente). AWS ALB, Nginx e HAProxy são balanceadores de carga comuns da camada 7. AWS NLB é um balanceador de carga da camada 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"]}')

Hashing consistente: adicionando e removendo nós

O hashing consistente resolve o problema de redistribuir chaves de cache quando servidores são adicionados ou removidos. No hashing ingênuo por módulo (server = hash(key) % n), alterar n remapeia quase todas as chaves — causando uma stampede de cache. O hashing consistente mapeia tanto as chaves quanto os servidores em um anel; cada chave é atendida pelo servidor mais próximo no sentido horário. Adicionar um servidor remapeia apenas as chaves entre o novo servidor e seu predecessor — aproximadamente 1/n de todas as chaves.

Nós virtuais (vnodes) melhoram a distribuição da carga: cada servidor físico recebe várias posições no anel, fazendo com que as chaves sejam distribuídas de maneira mais uniforme, mesmo com um número pequeno de servidores.

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)}')

Stampede de cache e soluções

Uma stampede de cache (ou efeito manada) ocorre quando uma entrada popular do cache expira e muitas solicitações simultâneas não encontram a entrada no cache ao mesmo tempo, inundando o banco de dados com a mesma consulta. Soluções:

  • Mutex/bloqueio: apenas uma solicitação calcula o valor; as outras aguardam
  • Expiração antecipada probabilística: pouco antes do TTL, uma solicitação decide aleatoriamente atualizar o cache, evitando uma expiração simultânea
  • Servir conteúdo antigo enquanto revalida: fornece o conteúdo antigo imediatamente enquanto atualiza o cache de forma assíncrona
  • Atualização em segundo plano: um processo separado atualiza as chaves populares antes que expirem
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')

Invalidação de cache da CDN

A invalidação de cache é famosa por ser difícil: 'Há apenas dois problemas difíceis na ciência da computação: invalidação de cache e dar nomes às coisas.' Quando o conteúdo muda na origem, os nós de borda da CDN precisam fornecer a nova versão. Estratégias:

  • Expiração baseada em TTL: deixar o conteúdo expirar naturalmente (simples, mas com uma janela de conteúdo antigo)
  • Versionamento de URL: incorporar o hash do conteúdo à URL (por exemplo, main.a3f2b.js); conteúdo novo = URL nova, sem necessidade de invalidação
  • Purga pela API da CDN: invalidar explicitamente as URLs por meio de uma chamada à API após a implantação (rápido, mas exige integração com a API da 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')

Arquitetura: reunindo tudo

Uma camada de aplicação web totalmente escalada usa as três técnicas em conjunto: o balanceamento de carga distribui o tráfego, a CDN absorve solicitações de assets estáticos e de API que podem ser armazenadas em cache, e o Redis armazena dados dinâmicos em cache. O banco de dados só recebe as falhas de cache — normalmente 5–20% das solicitações.

Um fluxo típico de solicitação para uma API com predominância de leitura é: usuário → DNS → borda da CDN (acerto de cache: fornecido imediatamente) → falha na CDN → balanceador de carga → conjunto de servidores de aplicação → cache do Redis (acerto: resposta em 1 ms) → falha de cache no Redis → banco de dados (10–50 ms) → resposta armazenada em cache no Redis + CDN opcional → usuário. Cada camada reduz significativamente a carga do banco de dados.

# 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%')

Dicas para entrevistas: cache e balanceamento de carga

Ao discutir cache em uma entrevista de design de sistemas, aborde sempre: o que armazenar em cache (dados muito acessados, cálculos dispendiosos), onde armazenar em cache (navegador, CDN, aplicação, cache de consultas do banco de dados), quando invalidar (na escrita, na expiração do TTL ou por meio de uma atualização em segundo plano) e quais garantias de consistência são aceitáveis. Um cache introduz uma janela de inconsistência — seja explícito a respeito dela.

Quanto ao balanceamento de carga, mencione a escolha do algoritmo, as verificações de integridade, as sessões persistentes (se necessário) e se é possível fazer a escalabilidade horizontal de servidores de aplicação sem estado. Se a aplicação tiver estado (conexões WebSocket, sessões), explique como esse estado é gerenciado entre as réplicas.

# 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}')

Verificação rápida

Teste sua compreensão dos conceitos de Estruturas de Dados e Algoritmos — Preparação para Entrevistas de Programação desta lição.

Recapitulação da lição

Nesta lição, você aprendeu que: o cache armazena dados muito acessados em camadas de memória rápida (Redis, CDN) para absorver a maioria das leituras e reduzir a carga do banco de dados, cache-aside é o padrão mais comum — uma falha significa carregar do banco de dados, um acerto significa retornar imediatamente e uma escrita significa excluir do cache e o hashing consistente distribui as chaves de cache entre os nós, de modo que adicionar ou remover nós remapeia apenas aproximadamente 1/n das chaves, em vez de remapear tudo. Em seguida, vamos projetar um limitador de taxa e um fluxo do Twitter para aplicar todos os conceitos de design de sistemas em problemas completos, de ponta a ponta.

Perguntas Frequentes

A aula “Cache, CDNs e balanceamento de carga” é grátis?

Sim — o texto completo de “Cache, CDNs e balanceamento de carga” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de DSA Interview Prep, atualize para CoddyKit PRO. O curso de DSA Interview Prep inclui 4 aulas no total.

O que vou aprender em “Cache, CDNs e balanceamento de carga”?

Adicione camadas de cache com Redis, envie ativos estáticos para uma CDN e distribua o tráfego entre réplicas com balanceadores de carga de round-robin e de hashing consistente. Você pratica DSA Interview Prep com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.

Preciso ter experiência prévia para começar DSA Interview Prep?

Nenhuma experiência prévia é necessária. DSA Interview Prep no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 3 de 4.

Quanto tempo leva a aula “Cache, CDNs e balanceamento de carga”?

A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.

Posso escrever e executar código nesta aula de DSA Interview Prep?

Sim. Cada aula de DSA Interview Prep inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.

Todas as aulas deste curso

  1. A estrutura da entrevista de projeto de sistemas
  2. Armazenamento de dados escalável: SQL vs NoSQL
  3. Cache, CDNs e balanceamento de carga
  4. Projetar um limitador de taxa e um feed do Twitter
← Voltar para DSA Interview Prep