Padrões de Aplicação com Redis: Cache, Armazenamento de Sessão e Rate Limiting

Published on 2026-08-20·v1.0

Objective

Aprender o pequeno conjunto de padrões de Redis que de fato aparecem em aplicações web de produção, não a referência de comandos, mas os formatos aos quais as pessoas recorrem repetidamente: um HASH mais um ZSET para armazenamento de sessão e cookie de login, um wrapper cache-aside GET-depois-SET ... EX para linhas de banco de dados e páginas renderizadas, e INCR/sorted sets para contadores, análises e rate limiting. Esses são os padrões que Redis in Action introduz antes mesmo de chegar a uma referência completa de comandos, porque, nas palavras do livro, "cada uma dessas peças pode de fato ser usada com pouca modificação diretamente em suas aplicações." O objetivo é entender não só como cada padrão funciona, mas a troca que ele faz: o que ele te dá em relação a bater direto no banco de dados relacional, e qual novo problema (geralmente invalidação, ou dados obsoletos) ele te devolve.

Use Cases

  • Substituir uma tabela de sessão apoiada em banco de dados relacional por um HASH do Redis para que checagens de login e rastreamento de "vistos recentemente" sobrevivam em escala web: os próprios números do livro: uma tabela de token-cookie que limitaria um banco de dados relacional a "aproximadamente 200 a 2.000 linhas individuais por segundo por servidor de banco de dados" em vez disso sustenta "pelo menos 20.000 visualizações de item por segundo" em um único servidor Redis.
  • Fazer cache de uma página web totalmente renderizada por uma janela fixa (cache: + SET ... EX 300) quando 95% do tráfego bate em páginas que "mudam no máximo uma vez por dia", cortando uma renderização de 20-50ms para "uma ida e volta ao Redis (menos de 1ms para uma conexão local)."
  • Fazer cache de linhas individuais de banco de dados, não páginas inteiras, para conteúdo que muda com frequência demais para cachear por completo, como uma contagem de estoque ao vivo em um item de promoção diária, usando um daemon em segundo plano que relê a linha em uma programação por item em vez de a cada requisição.
  • Construir throttling de requisição ou um limitador de tentativas de login com INCR + EXPIRE (janela fixa) ou um sorted set de timestamps (janela deslizante) em vez de fazer contadores na mão contra uma tabela relacional.
  • Rastrear popularidade de visualização de página com um único ZSET (ZINCRBY) para decidir quais páginas estão quentes o bastante para valer a pena cachear, em vez de cachear tudo e ficar sem memória.
  • Decidir, quando uma sessão de login ou entrada de cache precisa desaparecer, se EXPIRE/TTL é a ferramenta certa ou se um daemon explícito de limpeza limitado por tamanho é: eles não são intercambiáveis, e o livro é explícito sobre o porquê.

Deep Dive

Cache de sessão e cookie de login: o caso de uso canônico do Redis

Redis in Action abre seu capítulo prático com esse padrão por um motivo: é o que quase toda aplicação web precisa, e mapeia quase perfeitamente sobre os tipos de dado do Redis. A configuração: a Fake Web Retailer emite um token cookie aleatório em vez de um cookie assinado ("mais informação para armazenar no servidor", mas evita o modo de falha em que "é fácil esquecer de assinar e/ou verificar dados, permitindo vulnerabilidades de segurança"). O token é uma chave em um HASH do Redis chamado login:, mapeando token para usuário:

plaintext
def check_token(conn, token): return conn.hget('login:', token)

Atualizar uma sessão a cada visita faz três coisas de uma vez, empacotadas para custar uma ida e volta: registra qual usuário é dono do token, marca o timestamp da visita em um ZSET chamado recent: (score = timestamp, então o ZSET está sempre ordenado do mais antigo para o mais novo), e, se o usuário visualizou um item, empurra esse item para um ZSET viewed:<token> por token, aparado para as 25 entradas mais recentes com ZREMRANGEBYRANK ... 0 -26.

A decisão de design interessante é como sessões antigas são limpas, e é aqui que o livro ganha seu rótulo de "guia prático" em vez de só ensinar comandos. Existem duas formas de limitar o armazenamento de sessão:

  1. Um daemon de limpeza limitado por tamanho. Manter as 10 milhões de sessões mais recentes por loop: checar ZCARD('recent:'), e assim que ultrapassar o limite, puxar os 100 tokens mais antigos da frente do ZSET recent: (barato, porque já está ordenado por timestamp) e apagar suas entradas HASH e ZSETs viewed:.
  2. EXPIRE em cada chave. Mais simples de escrever, e o livro sinaliza isso explicitamente como uma alternativa real: "poderíamos omitir o ZSET recent, armazenar tokens de login como pares chave-valor simples, e usar o EXPIRE do Redis para definir uma data ou hora futura para limpar tanto sessões quanto nossos ZSETs de vistos recentemente."

O livro escolhe o daemon em vez do EXPIRE por um motivo que vale a pena internalizar, não só uma preferência estilística: "usar EXPIRE nos impede de limitar explicitamente nossa informação de sessão a 10 milhões de usuários, e nos impede de realizar análise de carrinho abandonado durante a expiração da sessão, se necessário no futuro." EXPIRE te dá armazenamento limitado por tempo; um ZSET com score mais um loop de limpeza te dá armazenamento limitado por contagem, e só o segundo te deixa dar uma última olhada em uma sessão antes que ela desapareça. Essa é a troca por baixo de uma decisão que, à primeira vista, parece "qual comando eu chamo."

Carrinhos de compra reaproveitam o mesmo token de sessão como chave em um segundo HASH (cart:<session>, ID de item para quantidade): deliberadamente simples, com validação deixada para a camada de aplicação: "vamos deixar a aplicação web lidar com a validação da contagem de itens, então só precisamos atualizar as contagens no carrinho conforme mudam." O ganho de colocar carrinho e sessão sob um único identificador é que o daemon de limpeza que expira sessões antigas consegue apagar o carrinho na mesma passada, e, porque os dois já estão no Redis, o varejista agora consegue computar "pessoas que olharam esse item acabaram comprando esse item X% das vezes" sem uma única escrita extra no banco de dados.

Cache-aside: páginas, linhas, e o problema de invalidação que você herda

O cache de página web está perto da definição de livro-texto de cache-aside: checar o cache, e só fazer o trabalho caro em um miss.

plaintext
def cache_request(conn, request, callback): if not can_cache(conn, request): return callback(request) page_key = 'cache:' + hash_request(request) content = conn.get(page_key) if not content: content = callback(request) conn.setex(page_key, content, 300) return content

(O conn.setex(page_key, content, 300) do livro é a ordem de argumento do cliente Python; o comando bruto equivalente é SETEX page_key 300 content: chave, segundos, valor.) Um TTL fixo de 5 minutos é a estratégia de invalidação inteira aqui, e funciona precisamente porque o dado subjacente é um em que dados obsoletos são baratos: "95% das páginas web que eles servem mudam no máximo uma vez por dia." Expiração baseada em TTL é a ferramenta certa exatamente quando você consegue limitar o quão errada uma resposta obsoleta tem permissão de ser, e por quanto tempo.

O cache de linha de banco de dados é a mesma ideia de cache-aside aplicada a algo que não pode tolerar cinco minutos de dados obsoletos: a contagem de estoque ao vivo de um item de promoção diária, onde mostrar uma quantidade obsoleta arrisca vender além do disponível. Em vez de cachear em um TTL disparado por requisição, o livro vira para um modelo push: uma chamada schedule_row_cache() registra um ID de linha em dois ZSETs (delay:, com que frequência, em segundos, essa linha deveria ser atualizada, e schedule:, quando é a próxima vez que ela vence) e um daemon cache_rows() separado continuamente puxa a linha que vence mais cedo, a relê do banco de dados, e escreve SET inv:<row_id> <json>. Definir o delay de uma linha como zero ou menos é o sinal para parar de cacheá-la e apagar o valor cacheado por completo.

Isso torna o problema de invalidação concreto: um cache TTL simples responde "por quanto tempo isso pode ficar obsoleto?" com um único número global, mas uma linha cuja frequência de escrita varia ("provavelmente faz sentido atualizar a linha cacheada a cada poucos segundos se houver muitos compradores. Mas se o dado não muda com frequência... pode fazer sentido só atualizar o cache a cada minuto") precisa de uma programação de atualização por chave, não por cache. O par de ZSETs schedule/delay é uma solução pequena e autocontida para "quais entradas estão obsoletas e precisam de atualização agora", que é a metade mais difícil de qualquer design de cache-aside; a metade fácil (ler cache, recorrer à fonte) é um GET.

Contadores, análises e rate limiting

A adição de análise mais simples do Capítulo 2 é uma única linha extra no caminho de atualização de sessão: conn.zincrby('viewed:', item, -1), transformando viewed: em um ZSET global de todo item já visto, ranqueado por contagem de visualização (incrementos negativos para que o item mais visualizado pouse no rank 0: um truque de ordenação de ZSET que vale a pena notar por si só). Um daemon o reescala a cada cinco minutos: descarta tudo fora do top 20.000, depois reduz pela metade todo score restante com ZINTERSTORE('viewed:', {'viewed:': .5}), o que mantém itens recentemente populares competitivos contra itens mais antigos que acumularam uma contagem grande uma vez e depois esfriaram. can_cache() então se torna uma única checagem de ZRANK: só cacheie uma página se seu item estiver no top 10.000 por contagem de visualização. Esse é o loop de feedback inteiro que decide quais páginas ganham uma entrada de cache em primeiro lugar, construído a partir de um sorted set e um job de reescalonamento.

O update_counter() do Capítulo 5 generaliza isso em métricas de série temporal de verdade: para cada uma de várias precisões (1 segundo, 5 segundos, 1 minuto, ... até 1 dia), ele faz um ZADD('known:', hash, 0) para registrar a existência do contador e um HINCRBY para incrementar a contagem do bucket de tempo atual, tudo em um pipeline. O ZSET known: (todo score fixo em 0, então o Redis recorre a ordenar por nome de membro) existe puramente para que um daemon de limpeza consiga enumerar todo contador já escrito sem um scan KEYS. O livro é explícito sobre por que isso não simplesmente usa EXPIRE por bucket: "uma limitação do comando EXPIRE é que ele só se aplica a chaves inteiras; não conseguimos expirar partes de chaves": e um contador aqui é um HASH guardando todos os buckets de tempo para uma dada precisão, então expirar a chave inteira jogaria fora buckets que ainda não venceram.

Livro vs. hoje: rate limiting especificamente. Nenhum dos dois capítulos constrói um limitador de throttling de requisição diretamente; o análogo mais próximo é o contador HINCRBY em bucket de tempo acima, que é um contador de janela fixa em espírito. Checando isso contra a orientação atual: a documentação oficial de rate limiter do Redis confirma que contagem de janela fixa ainda é um padrão de primeira classe e recomendado: "INCR e EXPIRE te dão contadores atômicos de janela fixa com limpeza automática de janela de tempo": então o instinto do livro de recorrer a um contador incrementando com um limite de tempo estava correto e continua atual. O que foi adicionado desde 2013 é o log de janela deslizante, construído sobre um sorted set em vez de um hash: registre o timestamp de toda requisição como um membro de ZSET, e a cada nova tentativa, descarte tudo mais antigo do que a janela com ZREMRANGEBYSCORE, depois checa ZCARD contra o limite antes de dar ZADD na nova entrada. Essa estrutura é a mesma ferramenta que o livro já usa para recent: e viewed: (um ZSET com score de timestamp), só que mirada em uma chave menor, por cliente, em vez de uma global. O Redis também agora documenta rate limiting de token bucket construído com Lua/EVAL para manter o ciclo ler-decidir-atualizar atômico, o que importa pelo exato mesmo motivo de condição de corrida que o livro aponta para seu próprio código de limpeza de contador. Nada disso contradiz o livro; são as mesmas primitivas (ZSET, INCR, EXPIRE) recombinadas para uma garantia mais estrita (nenhum pico acima de N em qualquer janela deslizante, não só por bucket fixo).

Livro vs. hoje: SETEX e SET ... EX. SETEX key seconds value ainda funciona exatamente como o livro o usa; não está obsoleto. A documentação atual do Redis o descreve como simplesmente equivalente a SET key value EX seconds, e essa unificação já era verdade no Redis 2.6.12 (2013), antes deste livro. Não há lacuna real aqui: SETEX continua bom de usar, embora SET ... EX seja a forma moderna mais idiomática porque o mesmo comando também compõe com NX/XX/GET.

Vale saber: cache do lado do cliente. O padrão de cache-aside acima (fazer polling em um TTL, aceitar dados obsoletos até esse TTL) ainda é exatamente como a maioria das aplicações faz cache hoje. O que é novo desde o Redis 6.0 (2020) é cache do lado do cliente via RESP3: um cliente consegue pedir ao Redis para rastrear as chaves que lê, e o Redis empurra uma mensagem de invalidação no instante em que outro cliente modifica uma dessas chaves: "toda vez que o valor de uma chave cacheada é modificado no banco de dados, o Redis empurra uma mensagem de invalidação para todos os clientes que estão cacheando a chave, dizendo aos clientes para descartar o valor cacheado localmente da chave." Essa é uma ferramenta genuinamente diferente de qualquer coisa nesses capítulos: em vez de um cliente chutar um TTL, o servidor diz a ele exatamente quando sua cópia cacheada ficou obsoleta. Isso não substitui o padrão de cache-aside de página/linha acima (esse ainda é o formato certo para cache armazenado no Redis), mas vale a pena saber sobre isso para cache armazenado na memória da aplicação, algo que esses capítulos não cobrem de jeito nenhum.

Trade-offs

  • Expiração baseada em TTL (EXPIRE/SETEX) troca precisão por simplicidade. Um número limita dados obsoletos para toda chave a que é aplicado; é trivial de raciocinar e grátis de implementar, mas não consegue expressar "atualize essa linha a cada poucos segundos enquanto a demanda estiver alta, a cada minuto do contrário": isso precisa do padrão de ZSET schedule/delay, que é mais código por um ganho real de controle de frescor em chaves quentes.
  • Limpeza limitada por contagem (um ZSET com score mais um daemon) troca simplicidade por controle. É estritamente mais código do que EXPIRE, mas é a única das duas que te deixa limitar armazenamento diretamente ("manter as 10 milhões de sessões mais recentes") e a única que te dá uma última olhada nos dados antes da exclusão: o próprio motivo do livro para escolhê-la para limpeza de sessão. Recorra a EXPIRE quando dados obsoletos são o que você está limitando; recorra a um ZSET-e-daemon quando contagem ou "fazer algo na saída" é o que você está limitando.
  • Cache-aside empurra a decisão de invalidação para você, permanentemente. O caminho de leitura (GET, recorrer à fonte, SET ... EX em um miss) é a metade fácil e parece igual para um cache de página e um cache de linha. A metade difícil (decidir quando o valor cacheado de uma chave específica está errado) é diferente para toda carga de trabalho: um TTL fixo para conteúdo que "muda no máximo uma vez por dia", uma programação de atualização por linha para uma contagem de estoque ao vivo, um DEL na escrita para qualquer coisa em que dados obsoletos são inaceitáveis mesmo brevemente. Acertar o caminho de leitura sem nunca decidir uma estratégia de invalidação é como caches ficam obsoletos silenciosamente.
  • Um contador de janela fixa (INCR + EXPIRE, ou o HINCRBY em bucket de precisão do livro) é barato e pode ser burlado na fronteira da janela. Um cliente consegue mandar N requisições no último milissegundo de uma janela e mais N no primeiro milissegundo da próxima, dobrando a taxa efetiva bem na costura: aceitável para dashboards de análise, uma lacuna real para qualquer coisa sensível à segurança como throttling de tentativas de login. O padrão de log de janela deslizante (sorted set de timestamps, podado com ZREMRANGEBYSCORE) fecha essa lacuna ao custo de um membro de sorted set por requisição em vez de um inteiro por janela: mais memória e CPU por uma garantia real.
  • Colocar sessão, carrinho e itens vistos sob um único ID de token é conveniente até virar um único ponto de raio de impacto. O livro se apoia nisso deliberadamente: uma única passada de limpeza apaga login:, cart:<token> e viewed:<token> juntos, mas isso também significa que um bug na construção de chave do daemon de limpeza toca três formatos de dado de uma vez em vez de um. Vale a pena nomear como o outro lado do design de "pequeno número de chaves fortemente acopladas" que este capítulo inteiro ensina.
  • A velocidade do Redis aqui é uma consequência de tirar escritas do banco de dados relacional, não uma substituição dele. Todo padrão neste conceito assume que o banco de dados relacional (ou outro sistema de registro) ainda existe em algum lugar para dados que precisam sobreviver a um restart do Redis sem persistência ajustada para durabilidade, ou que precisam de flexibilidade de consulta relacional. O Redis está fazendo as partes do trabalho (buscas de sessão, contadores de caminho quente, caches de curta duração) que um banco de dados relacional é comparativamente ruim sob carga, não o substituindo como fonte de verdade.

Documentation Links