Fazer mais coisas ao mesmo tempo e não repetir trabalho
Hoje vais perceber porque é que uma aplicação fica lenta, como pôr o computador a trabalhar em várias tarefas em simultâneo e como guardar resultados já calculados para responder num piscar de olhos. Tudo explicado com analogias do dia a dia e aplicado ao Spring Boot.
- Explicar o que é uma thread e porque existe paralelismo
- Usar
CompletableFuturee@Asyncno Spring - Configurar pools de threads e virtual threads
- Evitar race conditions em beans Spring
- Aplicar cache com
@Cacheable, Caffeine e Redis - Reconhecer os riscos de uma cache mal feita
Porque é que as aplicações ficam lentas?
Quando abres uma loja online e clicas num produto, o teu pedido viaja até um servidor (um computador noutro sítio), que vai buscar dados a uma base de dados, talvez pergunte o preço a outro serviço, calcula os portes e só depois te responde. Cada um destes passos demora tempo.
A maior parte desse tempo não é gasto a calcular. É gasto à espera: à espera da base de dados, da rede, de outro serviço. O processador está quase parado, de braços cruzados.
Imagina um restaurante com um único empregado. Ele leva o pedido da mesa 1 à cozinha e fica ali, parado, até o prato estar pronto. Só depois atende a mesa 2. As mesas esperam imenso, mas o empregado passou quase todo o tempo sem fazer nada. Há duas formas de melhorar: contratar mais empregados (paralelismo) ou ter pratos já preparados para os pedidos mais comuns (cache). É exatamente isso que vais aprender hoje.
O que vais ver nesta sessão
Três palavras que vais ouvir muito
Threads: os trabalhadores do teu programa
Um programa Java corre dentro de um processo (a JVM). Dentro desse processo, quem executa realmente as instruções são as threads (em português, «fios de execução»). Cada thread segue o código linha a linha, de forma independente das outras.
Quando corres um main, já existe uma thread chamada main. Se criares mais threads, o teu programa passa a fazer várias coisas ao mesmo tempo.
O processo é o restaurante; as threads são os empregados. Todos partilham a mesma cozinha (a memória). Isto é ótimo, porque todos veem os mesmos dados, mas também é perigoso: dois empregados podem tentar mexer na mesma panela ao mesmo tempo.
Concorrência vs. paralelismo
| Conceito | O que é | Analogia |
|---|---|---|
| Concorrência | Várias tarefas em curso, que se vão alternando no mesmo processador. | Um cozinheiro que mexe a sopa, vira o bife e volta à sopa. |
| Paralelismo | Várias tarefas a correr literalmente ao mesmo tempo, em processadores (núcleos) diferentes. | Três cozinheiros, cada um no seu fogão. |
Criar uma thread à mão
O () -> { … } é uma lambda: um bloco de código que se passa como se fosse um valor. Aqui diz à thread «quando arrancares, faz isto». O start() cria a thread; o join() espera que ela acabe.
Corre este programa várias vezes e a ordem das mensagens pode mudar. Quem decide que thread corre em cada momento é o sistema operativo, não tu. Esta imprevisibilidade é a principal fonte de dificuldade na programação concorrente.
Vê a diferença: um a um ou todos de uma vez
Imagina que o teu serviço tem de pedir dados a três sistemas: o catálogo, o stock e os preços. Muda os tempos e vê o que acontece quando os pedidos são feitos em sequência ou em paralelo.
Em sequência, o tempo total é a soma dos tempos. Em paralelo, é o tempo da tarefa mais lenta. Só podes paralelizar tarefas que não dependem umas das outras.
Platform threads vs. virtual threads
Até ao Java 21, cada thread Java correspondia a uma thread do sistema operativo, a que chamamos platform thread. São «caras»: cada uma reserva cerca de 1 MB de memória e o sistema operativo tem de gerir a troca entre elas (context switching). Um servidor normal aguenta alguns milhares, não muito mais.
O Java 21 trouxe as virtual threads (Projeto Loom): threads leves geridas pela própria JVM. Quando uma virtual thread fica à espera (de uma base de dados, por exemplo), a JVM «tira-a» da thread real e põe lá outra a trabalhar. Podes ter milhões delas.
Uma platform thread é um empregado contratado a tempo inteiro: caro. Uma virtual thread é um talão de pedido: o empregado pega num talão, leva-o à cozinha, pousa-o enquanto o prato é feito e pega logo noutro talão. Poucos empregados conseguem gerir milhares de talões.
Em paralelo, esperas pela mais lenta (500 ms). Em sequência somavas tudo: 1000 ms. O paralelismo poupou metade do tempo.
Executores e CompletableFuture
Criar threads à mão com new Thread() é como contratar um empregado novo para cada cliente e despedi-lo no fim. Na prática usamos um pool de threads: um grupo fixo de threads que vão pegando em tarefas de uma fila.
ExecutorService: a agência de trabalho
| Ferramenta | Para que serve |
|---|---|
Executors.newFixedThreadPool(n) | Pool com n threads fixas. Bom ponto de partida. |
Executors.newVirtualThreadPerTaskExecutor() | Uma virtual thread nova por tarefa (Java 21+). Ideal para I/O. |
ForkJoinPool | Divide um problema grande em pedaços e junta os resultados. É o que está por trás dos parallel streams. |
CompletableFuture: promessas que se encadeiam
Um Future simples obriga-te a ficar parado no get(). O CompletableFuture permite dizer «quando isto acabar, faz aquilo», sem bloquear. É como encomendar online: recebes um número de encomenda e só és avisado quando chega.
| Método | Em linguagem simples |
|---|---|
supplyAsync(() -> …) | Começa uma tarefa noutra thread e promete um resultado. |
thenApply(r -> …) | Quando o resultado chegar, transforma-o. |
thenCompose(r -> …) | Quando chegar, começa outra tarefa assíncrona que depende dele. |
thenCombine(outro, (a,b) -> …) | Junta dois resultados independentes. |
allOf(f1, f2, f3) | Espera que todas acabem. |
exceptionally(e -> …) | Se der erro, devolve um valor alternativo. |
orTimeout(2, SECONDS) | Desiste se demorar demasiado. |
Experimenta: muda o tempo de cada serviço e simula uma falha no serviço de preços.
lista.parallelStream().map(…) divide o trabalho por vários núcleos automaticamente. Funciona bem para cálculos pesados em listas grandes. Mas usa um pool partilhado por toda a JVM (o ForkJoinPool.commonPool()): se puseres lá chamadas à base de dados ou HTTP, bloqueias esse pool para toda a aplicação. Em código Spring de servidor, raramente é a ferramenta certa.
Quando as threads se atropelam
As threads partilham memória. Se duas mexem na mesma variável ao mesmo tempo, os resultados podem ficar errados. Estes problemas são traiçoeiros porque nem sempre acontecem: o programa funciona 99 vezes e falha à centésima.
Race condition: a corrida pela mesma variável
A instrução contador++ parece uma só, mas o computador faz três passos: ler o valor, somar 1, escrever. Se duas threads leem o mesmo valor antes de qualquer uma escrever, uma das somas perde-se.
Agora em grande escala: várias threads, cada uma a somar muitas vezes. Compara as três versões.
| Solução | Como funciona | Quando usar |
|---|---|---|
synchronized | Põe um «cadeado»: só uma thread entra no bloco de cada vez. | Proteger várias operações que têm de acontecer juntas. |
AtomicInteger, AtomicLong | O processador faz ler-somar-escrever num só passo indivisível. | Contadores e valores simples. |
ConcurrentHashMap | Um mapa preparado para muitas threads. | Sempre que um mapa é partilhado. |
| Imutabilidade | Objetos que nunca mudam (ex.: record) não podem ser corrompidos. | Sempre que possível. É a solução mais simples. |
Deadlock e starvation
Adquire os cadeados sempre pela mesma ordem em todo o código, mantém os blocos synchronized pequenos e prefere ferramentas de alto nível (ConcurrentHashMap, CompletableFuture) em vez de cadeados manuais.
O perigo escondido no Spring: beans singleton
Por defeito, o Spring cria uma única instância de cada @Service, @Component ou @RestController (âmbito singleton). Essa instância é usada por todos os pedidos ao mesmo tempo, cada um na sua thread. Logo, um campo dentro de um serviço é partilhado por todos os utilizadores!
Se a Ana e o Bruno adicionarem itens ao mesmo tempo, o item da Ana pode acabar no carrinho do Bruno.
Variáveis locais vivem na pilha de cada thread e nunca são partilhadas. Regra prática: serviços Spring não devem ter campos que mudam; só dependências injetadas (repositórios, outros serviços) e constantes.
O controller é singleton: todos os pedidos partilham o mesmo campo. pedidosHoje++ não é atómico, por isso perdem-se somas. Usa AtomicInteger ou, melhor, uma métrica do Micrometer (sessão 2).
Execução assíncrona com @Async
Às vezes não precisas da resposta de uma tarefa para responder ao utilizador. Exemplo: depois de uma encomenda, enviar o email de confirmação. O cliente não deve ficar à espera do servidor de email. Com @Async, o Spring corre esse método noutra thread e o pedido continua.
Passo 1: ligar a funcionalidade
Passo 2: marcar os métodos
Repara nos nomes entre parênteses retos: o pedido HTTP foi tratado pela thread http-nio-8080-exec-3 do Tomcat; o email foi enviado pela thread task-1, do executor do Spring. O cliente recebeu a resposta 2 segundos antes de o email sair.
Como funciona por dentro: o proxy
O Spring não altera a tua classe. Cria um proxy: um objeto «intermediário» que se põe à frente do teu serviço. Quando chamas o método, quem recebe a chamada é o proxy, que a entrega a outra thread.
enviarEmail()task-1Se um método da mesma classe chamar o método @Async com this.enviarEmail(), a chamada não passa pelo proxy. Resultado: corre na mesma thread, de forma síncrona, sem qualquer aviso. O mesmo acontece com @Transactional e @Cacheable. Solução: põe o método assíncrono noutro bean e injeta-o.
E se o método assíncrono falhar?
Num método @Async que devolve void, a exceção não volta a quem chamou (já seguiu caminho). Por defeito só aparece no log. Para a tratares, regista um AsyncUncaughtExceptionHandler. Nos métodos que devolvem CompletableFuture, a exceção fica dentro da promessa e tratas com exceptionally.
As anotações do Spring funcionam através de proxies. Uma chamada interna (this.metodo()) vai diretamente ao objeto real e o @Async é ignorado em silêncio.
Configurar o pool: quantos trabalhadores e quanta fila?
O executor que corre os métodos @Async é um ThreadPoolTaskExecutor. Tem quatro parâmetros que tens mesmo de perceber, porque decidem como a tua aplicação se comporta sob pressão.
| Parâmetro | Significado | No restaurante |
|---|---|---|
corePoolSize | Threads sempre disponíveis. | Empregados do quadro. |
queueCapacity | Quantas tarefas podem esperar na fila. | Lugares na sala de espera. |
maxPoolSize | Máximo de threads, criadas só quando a fila enche. | Empregados temporários chamados em dias de enchente. |
| Política de rejeição | O que fazer quando tudo está cheio. | O que dizer ao cliente quando não cabe mais ninguém. |
As threads extra (acima do corePoolSize) só são criadas quando a fila está cheia. Se a fila for enorme (ou ilimitada), nunca passas do core. Experimenta no simulador abaixo.
A configuração no Spring Boot
É a forma mais simples: o Spring Boot cria o executor por ti com estes valores.
Útil quando queres pools separados por tipo de trabalho. Assim, se o servidor de email ficar lento, não bloqueia a geração de faturas. Chama-se a isto bulkhead (como os compartimentos estanques de um navio).
Políticas de rejeição
| Política | O que faz | Na prática |
|---|---|---|
AbortPolicy (defeito) | Lança TaskRejectedException. | Falha rápido e às claras. |
CallerRunsPolicy | Quem submeteu corre a tarefa ele próprio. | Abranda naturalmente quem está a produzir trabalho a mais (backpressure). |
DiscardPolicy | Deita fora a tarefa em silêncio. | Perigoso: perdes trabalho sem saber. |
DiscardOldestPolicy | Descarta a tarefa mais antiga da fila. | Só para dados em que só o mais recente interessa. |
Não perder o contexto: TaskDecorator
Algumas informações vivem «agarradas» à thread: o utilizador autenticado (SecurityContext), o identificador do pedido nos logs (MDC), o trace. Quando o trabalho salta para outra thread, essa informação fica para trás. Um TaskDecorator copia-a.
Para o utilizador autenticado existe o DelegatingSecurityContextAsyncTaskExecutor. Para o tracing (sessão 2), o Spring Boot 3 tem ContextPropagatingTaskDecorator, que propaga tudo o que o Micrometer conhece.
Virtual threads, agendamento e WebFlux
Ligar virtual threads no Spring Boot
Com Java 21 e Spring Boot 3.2 ou superior, basta uma linha:
Com isto, o Tomcat trata cada pedido HTTP numa virtual thread, e os @Async e @Scheduled também passam a usá-las. Não precisas de mudar mais nada no código.
Simula um serviço que recebe muitos pedidos, cada um à espera 1 segundo de outro sistema. Compara um pool de 200 platform threads (o defeito do Tomcat) com virtual threads.
No Java 21 a 23, se uma virtual thread fizer uma espera dentro de um bloco synchronized, fica pinned (presa) à thread real, e perdes a vantagem. Isto foi corrigido no Java 24. Em Java 21, prefere ReentrantLock em código com esperas e confirma com -Djdk.tracePinnedThreads=full. Outro cuidado: com milhares de pedidos em simultâneo, o limite passa a ser a base de dados. As virtual threads não criam ligações à base de dados do nada (vês isto na sessão 2).
Tarefas agendadas com @Scheduled
Por defeito, todas as tarefas agendadas partilham uma única thread. Se uma demorar, as outras atrasam. Aumenta com spring.task.scheduling.pool.size=4 ou ativa as virtual threads.
Paralelismo em processamento em lote (Spring Batch)
Spring MVC com virtual threads ou Spring WebFlux?
O Spring WebFlux é a alternativa reativa: em vez de uma thread por pedido, poucas threads tratam tudo com eventos, e o código usa os tipos Mono e Flux.
| Spring MVC + virtual threads | Spring WebFlux | |
|---|---|---|
| Estilo de código | Normal, linha a linha, fácil de ler e depurar. | Encadeamento de operadores; curva de aprendizagem maior. |
| Bibliotecas | Funciona com JPA, JDBC e tudo o que já existe. | Exige drivers reativos (R2DBC, WebClient…). |
| Streaming e backpressure | Limitado. | Excelente: fluxos contínuos, SSE, WebSockets. |
| Recomendação | A escolha por defeito para APIs novas em 2026. | Quando precisas de streaming ou já tens uma base reativa. |
Laboratório 1: paralelizar chamadas a serviços externos
Objetivo: a página de detalhe de uma encomenda chama três serviços, um a seguir ao outro. Vais torná-la mais rápida com CompletableFuture e depois comparar com virtual threads.
Ponto de partida (lento)
Repara que clientes e fidelizacao precisam do clienteId, que vem da encomenda. A transportadora só precisa do id. Há dependências, por isso não se pode paralelizar tudo às cegas.
Versão otimizada
Desafios laboratório
- Mede os tempos com um
StopWatchdo Spring e regista-os no log. - Troca o executor por
Executors.newVirtualThreadPerTaskExecutor(). Os tempos mudam com poucos pedidos? E com 500 pedidos em simultâneo? - Simula a transportadora em baixo (lança exceção). Garante que a página continua a responder com
exceptionally.
Cache: não fazer duas vezes o mesmo trabalho
Uma cache é um local de armazenamento rápido onde guardas o resultado de uma operação lenta, para o reutilizar da próxima vez. Na segunda vez, em vez de ires à base de dados (lento), vais à memória (muito rápido).
Ligas para as informações para saber o número do canalizador. Demora 3 minutos. Escreves o número num post-it e colas no monitor. Da próxima vez, demoras 2 segundos. O post-it é a cache. Mas atenção: se o canalizador mudar de número, o post-it está desatualizado. Este é o grande desafio das caches.
Vocabulário essencial
- Hit
- O valor estava na cache. Resposta rápida. 🎯
- Miss
- Não estava. Tem de se ir à fonte (lenta) e depois guardar na cache.
- Hit ratio
- Percentagem de hits. Uma boa cache tem tipicamente mais de 80%.
- Evicção
- Remover entradas da cache, porque está cheia ou porque o valor mudou.
- TTL
- Time To Live: tempo máximo que uma entrada pode viver na cache.
Padrões de utilização
| Padrão | Como funciona | Vantagem / risco |
|---|---|---|
| Cache-aside ⭐ | A aplicação pergunta à cache; se não houver, vai à base de dados e guarda na cache. | Simples e o mais usado. É o que o @Cacheable faz. |
| Read-through | A aplicação só fala com a cache; a cache vai à base de dados sozinha. | Código mais limpo; precisa de uma cache que saiba carregar dados (ex.: LoadingCache do Caffeine). |
| Write-through | Ao gravar, escreve na cache e na base de dados ao mesmo tempo. | Cache sempre atualizada; escritas mais lentas. |
| Write-behind | Escreve na cache e a base de dados é atualizada mais tarde, em lote. | Escritas rapidíssimas; risco de perder dados se a cache cair. |
Quando a cache enche: políticas de evicção
Experimenta uma cache por dentro
Clica nos produtos para os pedir. O primeiro pedido de cada um é um miss (800 ms); os seguintes são hits (1 ms), até a entrada sair da cache. Muda a capacidade e a política.
Cache local ou distribuída?
| Local (Caffeine) | Distribuída (Redis) | |
|---|---|---|
| Onde vive | Na memória de cada instância da aplicação. | Num servidor à parte, partilhado por todas as instâncias. |
| Velocidade | Nanossegundos. | Sub-milissegundo (há uma ida à rede). |
| Consistência | Cada instância tem a sua cópia: podem divergir. | Todas veem o mesmo valor. |
| Sobrevive a reinícios? | Não. | Sim. |
| Ideal para | Dados pequenos, muito lidos, que mudam pouco (configurações, catálogos). | Sessões, dados partilhados, aplicações com várias instâncias. |
Cache no Spring em quatro anotações
O Spring tem uma abstração de cache: tu marcas os métodos com anotações e ele trata do resto. O melhor é que o código não depende da tecnologia: podes trocar Caffeine por Redis sem mudar uma linha do serviço.
Passo 1: dependência e ativação
Passo 2: as anotações
| Anotação | O que faz |
|---|---|
@Cacheable("produtos") | Antes de correr o método, procura na cache. Se existir, devolve logo e o método nem corre. Se não, corre e guarda o resultado. |
@CachePut("produtos") | Corre sempre o método e atualiza a cache com o resultado. Para gravações. |
@CacheEvict("produtos") | Remove da cache. Com allEntries = true limpa tudo. |
@Caching(…) | Junta várias das anteriores no mesmo método. |
Testa a API. Repara no tempo de resposta e nas mensagens do servidor por baixo.
Controlar as chaves e as condições
A chave identifica a entrada na cache. Por defeito usa os parâmetros do método. Com key, condition e unless escreves regras em SpEL (Spring Expression Language), uma mini-linguagem que começa com #.
| Atributo | Avaliado | Significado |
|---|---|---|
condition | Antes de correr o método | Se for falso, ignora a cache por completo. |
unless | Depois de correr o método | Se for verdadeiro, não guarda o resultado. Pode usar #result. |
Numa cache local, o objeto devolvido é o mesmo que está guardado. Se alguém fizer produto.setPreco(0) no objeto que recebeu, alterou a cache para todos! Usa record ou DTOs imutáveis.
@CachePut corre sempre o método e guarda o resultado. @Cacheable não correria o método se o produto já estivesse em cache. @CacheEvict também seria válido, mas obrigava a ir à base de dados no próximo pedido.
Escolher a tecnologia: Caffeine, Redis ou as duas
Sem configuração, o Spring usa um simples ConcurrentHashMap, que nunca expira e nunca liberta memória. Serve para aprender, nunca para produção. Escolhe um fornecedor a sério:
maximumSize limita o número de entradas; expireAfterWrite é o TTL; recordStats ativa as estatísticas de hits e misses (para veres no Actuator).
Para configurar cada cache de forma diferente:
O Redis guarda bytes, por isso os objetos têm de ser serializados (convertidos em texto ou binário). O defeito usa a serialização Java, que é frágil e ilegível. Prefere JSON:
Em aplicações com várias instâncias e muito tráfego, combinam-se as duas: uma cache local pequena e rapidíssima à frente de uma cache Redis partilhada.
O Spring não traz isto pronto. Implementa-se com um CacheManager próprio ou usando bibliotecas como a JetCache. O problema a resolver é a invalidação: quando um produto muda, todas as instâncias têm de limpar a sua L1, normalmente através de mensagens pub/sub do Redis.
Cache na camada de persistência (Hibernate)
| Nível | Âmbito | Notas |
|---|---|---|
| 1.º nível | Dentro de uma transação (EntityManager). | Sempre ligado. Se pedires a mesma entidade duas vezes na mesma transação, só há uma consulta. |
| 2.º nível | Partilhado entre transações. | Opcional. Guarda entidades por ID. Bom para tabelas de referência (países, categorias). |
| Query cache | Resultados de consultas. | Invalidado sempre que qualquer tabela envolvida muda. Raramente compensa. |
Para a maioria dos casos, @Cacheable nos serviços é mais simples, mais visível e cacheia DTOs prontos (e não entidades com ligações lazy). Usa a cache de 2.º nível para entidades de referência muito lidas.
Cache HTTP: deixar o browser trabalhar
A cache mais rápida é a que nem chega ao servidor. Com os cabeçalhos certos, o browser (ou uma CDN) guarda as respostas.
200 + ETag: "7"If-None-Match: "7"304 Not Modified, sem corpo. Poupa largura de banda.O filtro ShallowEtagHeaderFilter gera ETags automaticamente a partir do conteúdo da resposta. Poupa rede, mas o servidor continua a calcular a resposta toda.
Quando a cache se volta contra ti
«Há só duas coisas difíceis em informática: invalidar caches e dar nomes às coisas.» A frase é antiga, mas continua verdadeira. Conhece os problemas clássicos:
Solução:
sync = true, TTL com variação aleatória (jitter) ou renovar antes de expirar (refreshAfterWrite do Caffeine)./produtos/999999) nunca ficam em cache e batem sempre na base de dados. Pode ser um ataque. Solução: cachear o «não existe» com TTL curto ou usar um filtro de Bloom.
Solução: TTL adequado ao negócio,
@CacheEvict/@CachePut em todas as escritas.OutOfMemoryError). Solução: definir sempre
maximumSize. Vais ver isto na sessão 2.Invalidação com várias instâncias
Com 3 instâncias da aplicação e caches locais, um @CacheEvict só limpa a instância que tratou o pedido. As outras duas continuam com o valor antigo até o TTL expirar. Opções: aceitar a desatualização durante um TTL curto, usar Redis (cache única) ou difundir eventos de invalidação.
Medir: a cache está mesmo a ajudar?
Com o Actuator e o recordStats, o Spring Boot publica métricas de cada cache automaticamente:
Checklist de decisão
| Pergunta | Se a resposta for «sim»… |
|---|---|
| A tarefa passa o tempo à espera de I/O e há várias independentes? | Paraleliza (CompletableFuture, virtual threads). |
| A tarefa não precisa de resposta imediata? | @Async ou uma fila de mensagens. |
| Os mesmos dados são lidos muitas vezes e mudam pouco? | Cache. |
| Os dados têm de estar 100% atualizados (saldo bancário, stock no checkout)? | Não faças cache, ou usa TTL muito curto. |
| O problema é uma consulta lenta por falta de índice ou N+1? | Nem paralelismo nem cache: corrige a consulta (sessão 2). |
| Não mediste ainda onde está a lentidão? | Mede primeiro. Otimizar às cegas é perder tempo. |
Paralelismo e cache acrescentam complexidade e novos tipos de erros. Usa-os onde a medição mostrar que fazem diferença, e não por precaução.
Exercícios
1. Contador de visitas seguro fácil
Um @RestController conta visitas com um campo int. Corrige-o para funcionar com muitos pedidos em simultâneo.
Ver solução
2. Email assíncrono com pool próprio médio
Cria um executor emailExecutor com 2 threads de core, 4 de máximo, fila de 50 e CallerRunsPolicy. Usa-o num @Async que simula um envio de 2 segundos.
Ver solução
3. Cache de taxas de câmbio médio
Um serviço CambioService.taxa(String moeda) chama uma API externa (1 s). As taxas podem ter até 60 segundos de atraso. Configura uma cache Caffeine adequada, não guardes resultados nulos e evita o stampede.
Ver solução
Nota: com sync = true o atributo unless não é suportado em algumas versões do Spring. Se o arranque falhar, remove o unless e garante que o método nunca devolve null (lança exceção).
4. Projeto: medir antes e depois projeto
No projeto do curso (catálogo de encomendas), aplica Caffeine ao ProdutoService.buscar e Redis ao CategoriaService.listar. Faz 100 pedidos seguidos com curl num ciclo e compara o tempo total com e sem cache. Regista o hit ratio no Actuator. Guarda os números: vais usá-los na sessão 3.
Resumo da sessão
O essencial em 10 pontos
- A maior parte do tempo de uma aplicação web é gasto à espera de I/O, não a calcular.
- Em paralelo, o tempo total é o da tarefa mais lenta; em sequência, é a soma.
CompletableFutureencadeia e combina tarefas assíncronas sem bloquear.- Beans Spring são singletons: nada de campos mutáveis; usa variáveis locais,
Atomic*ou objetos imutáveis. @Asyncfunciona via proxy: chamadas internas (this.) ignoram-no.- No
ThreadPoolTaskExecutor, threads extra só nascem quando a fila enche. spring.threads.virtual.enabled=trueliga virtual threads (Java 21+), ideais para I/O.@Cacheable,@CachePute@CacheEvictcontrolam a cache sem prender o código à tecnologia.- Caffeine para cache local rápida; Redis para cache partilhada entre instâncias.
- Define sempre tamanho máximo e TTL, e mede o hit ratio.
Glossário
- Thread
- Fio de execução independente dentro de um programa.
- Virtual thread
- Thread leve gerida pela JVM (Java 21+).
- Pool de threads
- Grupo de threads reutilizáveis que executam tarefas de uma fila.
- Race condition
- Erro causado por threads que acedem aos mesmos dados ao mesmo tempo.
- Deadlock
- Threads bloqueadas para sempre à espera umas das outras.
- Proxy
- Objeto intermediário que o Spring põe à frente dos teus beans para aplicar anotações.
- Cache
- Armazenamento rápido de resultados para os reutilizar.
- Hit / Miss
- Encontrado / não encontrado na cache.
- TTL
- Tempo de vida de uma entrada na cache.
- Evicção
- Remoção de entradas da cache.
- Stampede
- Avalanche de pedidos à fonte quando uma entrada popular expira.
Trabalho de casa
Agregador de meteorologia. Cria um endpoint GET /tempo?cidades=Lisboa,Porto,Faro que, para cada cidade, chama um serviço simulado que demora entre 200 e 800 ms (usa Thread.sleep com valor aleatório). Requisitos: chamadas em paralelo, timeout de 1 segundo por cidade, cache de 5 minutos por cidade e um log com o tempo total de cada pedido.
Leitura: documentação Spring Boot, secções «Task Execution and Scheduling» e «Caching».
Na próxima sessão: vais aprender a descobrir onde está a lentidão, com métricas, profilers e debugging.