APIs Úteis
Utilize LocalDateTime para manipular datas. Exemplo para obter a data atual menos cinco dias:
LocalDateTime limiteTemporal = LocalDateTime.now().minusDays(5);
Date dataLimite = Date.from(limiteTemporal.atZone(ZoneId.systemDefault()).toInstant());
Filtragem e ordenação com Stream API:
List<ArtigoPopularVO> artigosDoCanal = listaCompletaArtigos.stream()
.filter(artigo -> artigo.getCanalId().equals(canalAtual.getId()))
.collect(Collectors.toList());
artigosDoCanal.sort(Comparator.comparingDouble(ArtigoPopularVO::getPontuacao).reversed());
Truncagem segura de listas para evitar exceções:
int limiteMaximo = 30;
if (artigosDoCanal.size() > limiteMaximo) {
artigosDoCanal = new ArrayList<>(artigosDoCanal.subList(0, limiteMaximo));
}
Tratamento de respostas de chamadas remotas (Feign/RestTemplate):
if (respostaAPI.getStatus() == HttpStatus.OK.value()) {
List<CanalMidia> canais = objectMapper.convertValue(
respostaAPI.getCorpo(), new TypeReference<>() {}
);
// processar lista de canais
}
Para evitar dados duplicados entre cache Redis e consultas ao banco, filtre os IDs presentes no cache nas consultas subsequentes.
Requisitos e Fluxo
Artigos populares serão cacheados no Redis por canal. O cálculo da popularidade ocorre periodicamente via XXL-JOB. A leitura prioritária usa o cache, enuqanto operações de "carregar mais/novo" consultam o banco com exclusão dos IDs já cacheados.
Pontuação por Comportamento do Usuário
Pesos: Leitura = 1, Curtida = 3, Comentário = 5, Favorito = 8.
Cálculo da Popularidade
1. Consultar Artigos Recentes
Mapper com parâmetro de data:
@Select("SELECT a.* FROM artigo a " +
"INNER JOIN config_artigo c ON a.id = c.artigo_id " +
"WHERE c.ativo = 1 AND a.data_publicacao >= #{dataLimite}")
List<Artigo> listarArtigosRecentes(@Param("dataLimite") Date dataLimite);
Obtenção de canais via cliente Feign:
@FeignClient(name = "servico-midia", path = "/canais")
public interface ClienteMidia {
@GetMapping
List<Canal> listarTodos();
}
2. Calcular Pontuação
public List<ArtigoPopularVO> calcularPontuacoes(List<Artigo> artigos) {
return artigos.stream().map(artigo -> {
ArtigoPopularVO vo = new ArtigoPopularVO();
vo.setId(artigo.getId());
vo.setCanalId(artigo.getCanalId());
double pontuacao = (artigo.getCurtidas() != null ? artigo.getCurtidas() * 3 : 0) +
(artigo.getLeituras() != null ? artigo.getLeituras() : 0) +
(artigo.getComentarios() != null ? artigo.getComentarios() * 5 : 0) +
(artigo.getFavoritos() != null ? artigo.getFavoritos() * 8 : 0);
vo.setPontuacao(pontuacao);
return vo;
}).collect(Collectors.toList());
}
3. Cachear Top 30 por Canal
public void atualizarCache(List<ArtigoPopularVO> artigos, List<Canal> canais) {
String chavePrefixo = "popular:canal:";
for (Canal canal : canais) {
List<ArtigoPopularVO> filtrados = artigos.stream()
.filter(a -> a.getCanalId().equals(canal.getId()))
.sorted(Comparator.comparingDouble(ArtigoPopularVO::getPontuacao).reversed())
.limit(30)
.collect(Collectors.toList());
redisTemplate.opsForValue().set(chavePrefixo + canal.getId(), filtrados);
}
// Cache geral (recomendações)
List<ArtigoPopularVO> topGeral = artigos.stream()
.sorted(Comparator.comparingDouble(ArtigoPopularVO::getPontuacao).reversed())
.limit(30)
.collect(Collectors.toList());
redisTemplate.opsForValue().set(chavePrefixo + "recomendacoes", topGeral);
}
Agendamento com XXL-JOB
Configuração do Executor
Adicionar dependência e configuração:
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.3.0</version>
</dependency>
Configuração do executor:
@Configuration
public class ConfiguracaoXxlJob {
@Value("${xxl.job.admin.enderecos}")
private String enderecosAdmin;
@Value("${xxl.job.executor.nome-app}")
private String nomeApp;
@Value("${xxl.job.executor.porta}")
private int porta;
@Bean
public XxlJobSpringExecutor criarExecutor() {
XxlJobSpringExecutor executor = new XxlJobSpringExecutor();
executor.setAdminAddresses(enderecosAdmin);
executor.setAppname(nomeApp);
executor.setPort(porta);
return executor;
}
}
Propriedades em application.yml:
xxl:
job:
admin:
enderecos: http://endereco-admin:8080/xxl-job-admin
executor:
nome-app: executor-artigos-populares
porta: 9999
Definição da tarefa agendada:
@Component
public class TarefaArtigosPopulares {
private final ServicoPopularidade servicoPopularidade;
@XxlJob("calcularArtigosPopulares")
public void executar() {
servicoPopularidade.calcularEAtualizarCache();
}
}
Modificar Consulta de Artigos
Controlador com lógica prioritária para cache:
@RestController
@RequestMapping("/artigos")
public class ControladorArtigos {
private final ServicoArtigo servicoArtigo;
@PostMapping("/carregar")
public ResponseEntity<List<ArtigoVO>> carregar(@RequestBody FiltroArtigo filtro) {
return ResponseEntity.ok(servicoArtigo.carregarComPrioridadeCache(filtro));
}
}
Serviço com fallback para banco:
@Service
public class ServicoArtigoImpl implements ServicoArtigo {
private final CacheService cache;
private final RepositorioArtigo repositorio;
@Override
public List<ArtigoVO> carregarComPrioridadeCache(FiltroArtigo filtro) {
String chaveCache = "popular:canal:" + filtro.getCanalId();
List<ArtigoPopularVO> cacheados = cache.obter(chaveCache);
if (cacheados != null && !cacheados.isEmpty()) {
return cacheados;
}
return carregarDoBanco(filtro);
}
private List<ArtigoVO> carregarDoBanco(FiltroArtigo filtro) {
Set<Long> idsExcluidos = cache.obterIdsCacheados(filtro.getCanalId());
return repositorio.buscarExcluindoIds(idsExcluidos, filtro.getLimite());
}
}
Prevenção de OOM com XXL-JOB e Sharding
Estratégia: Dividir Carga com Sharding Broadcast
Configurar a tarefa no XXL-JOB com estratégia de roteamento "Sharding Broadcast".
Implementação da tarefa com processamento em lotes:
@XxlJob("calcularPopularidadeDistribuida")
public void executarSharding() {
int shardIndex = XxlJobHelper.getShardIndex();
int shardTotal = XxlJobHelper.getShardTotal();
long offset = 0;
final int TAMANHO_LOTE = 1000;
while (true) {
List<Artigo> lote = repositorio.buscarPorShard(shardIndex, shardTotal, offset, TAMANHO_LOTE);
if (lote.isEmpty()) break;
List<ArtigoPopularVO> resultados = calcularPontuacoes(lote);
salvarNoRedis(resultados);
offset += TAMANHO_LOTE;
}
}
Consulta SQL otimizada para sharding:
SELECT id, titulo, curtidas, leituras, comentarios, favoritos
FROM artigo
WHERE MOD(id, #{totalShards}) = #{shardIndex}
AND id >= #{offset}
ORDER BY id
LIMIT #{limite}
Processamento Streaming (Alternativa)
try (Connection conn = dataSource.getConnection()) {
PreparedStatement stmt = conn.prepareStatement(sql,
ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
stmt.setFetchSize(Integer.MIN_VALUE); // Habilita streaming
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
Artigo artigo = mapearArtigo(rs);
double pontuacao = calcularPontuacao(artigo);
atualizarRedis(artigo.getId(), pontuacao);
}
}
Agregação Final
Após execução de todos os shards, qualquer instância pode recuperar o top 30 global:
Set<String> top30 = redisTemplate.opsForZSet()
.reverseRange("ranking:artigos:popular", 0, 29);
Resumo das práticas para prevenção de OOM:
- Sharding broadcast divide dados entre executores
- Sharding por módulo de ID distribui carga uniformemente
- Processamento em lotes ou straeming evita carga total em memória
- Redis Sorted Set agrega resultados de todos os shards
- Escalabilidade horizonatl adicionando mais instâncias executoras