Em redes sociais, a funcionalidade de "seguidores em comum" é um recurso essencial para aumentar o engajamento. Utilizar um banco de dados relacionall para calcular interseções de grandes volumes de dados pode ser ineficiente. Para resolver isso, o Redis oferece a estrutura de dados Set, que permite realizar operações de conjunto, como a interseção, de forma extremamente performática.
Estrutura e Lógica de Implementação
A estratégia consiste em espelhar as relações de "seguir" do banco de dados relacional no Redis. Cada usuário possui uma chave do tipo Set contendo os IDs das pessoas que ele segue. Quando o Usuário A segue o Usuário B, o ID de B é adicionado ao Set de A. Para encontrar seguidores comuns entre o Usuário A e o Usuário C, basta realizar a interseção entre follows:A e follows:C.
Definição dos Endpoints
Abaixo, apresentamos a interface do controlador responsável por gerenciar as interações entre usuários e a busca por conexões compartilhadas.
@RestController
@RequestMapping("/relacionamentos")
public class ConexaoController {
@Autowired
private IRelacionamentoService servicoRelacionamento;
@PutMapping("/{idAlvo}/{seguir}")
public Response alterarSeguimento(@PathVariable("idAlvo") Long idAlvo, @PathVariable("seguir") Boolean deveSeguir) {
return servicoRelacionamento.processarSeguimento(idAlvo, deveSeguir);
}
@GetMapping("/status/{idAlvo}")
public Response verificarSeSegue(@PathVariable("idAlvo") Long idAlvo) {
return servicoRelacionamento.checarStatus(idAlvo);
}
@GetMapping("/comuns/{idOutroUsuario}")
public Response listarSeguidoresComuns(@PathVariable("idOutroUsuario") Long idOutroUsuario) {
return servicoRelacionamento.obterIntersecaoSeguidores(idOutroUsuario);
}
}
Implementação da Lógica de Negócio
O serviço abaixo demonstra como sincronizar o estado entre o banco de dados (para persistência) e o Redis (para operações de conjunto).
@Service
public class RelacionamentoServiceImpl extends ServiceImpl<RelacionamentoMapper, Relacionamento> implements IRelacionamentoService {
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public Response processarSeguimento(Long idAlvo, Boolean deveSeguir) {
Long idLogado = ContextoUsuario.getId();
String chaveRedis = "seguindo:" + idLogado;
if (deveSeguir) {
Relacionamento novoViculo = new Relacionamento();
novoViculo.setUsuarioId(idLogado);
novoViculo.setSeguidoId(idAlvo);
boolean gravado = save(novoViculo);
if (gravado) {
// SADD: Adiciona o ID ao conjunto no Redis
redisTemplate.opsForSet().add(chaveRedis, idAlvo.toString());
}
} else {
boolean removido = remove(new QueryWrapper<Relacionamento>()
.eq("usuario_id", idLogado).eq("seguido_id", idAlvo));
if (removido) {
// SREM: Remove o ID do conjunto no Redis
redisTemplate.opsForSet().remove(chaveRedis, idAlvo.toString());
}
}
return Response.ok();
}
@Override
public Response checarStatus(Long idAlvo) {
Long idLogado = ContextoUsuario.getId();
Integer registro = query().eq("usuario_id", idLogado).eq("seguido_id", idAlvo).count();
return Response.ok(registro > 0);
}
@Override
public Response obterIntersecaoSeguidores(Long outroUsuarioId) {
Long idLogado = ContextoUsuario.getId();
String minhaChave = "seguindo:" + idLogado;
String outraChave = "seguindo:" + outroUsuarioId;
// SINTER: Realiza a interseção entre os dois conjuntos
Set<String> idsComuns = redisTemplate.opsForSet().intersect(minhaChave, outraChave);
if (idsComuns == null || idsComuns.isEmpty()) {
return Response.ok(Collections.emptyList());
}
List<Long> listaIds = idsComuns.stream()
.map(Long::valueOf)
.collect(Collectors.toList());
// Busca detalhes dos usuários a partir dos IDs encontrados
List<UserDTO> perfisComuns = userService.listByIds(listaIds)
.stream()
.map(u -> BeanUtil.copyProperties(u, UserDTO.class))
.collect(Collectors.toList());
return Response.ok(perfisComuns);
}
}
Vantagens desta Abordagem
- Velocidade: A operação de interseção no Redis ocorre em memória, sendo ordens de magnitude mais rápida que um JOIN ou subquery em SQL para este propósito.
- Escalabilidade: Conforme a rede cresce, o custo computacional de calcular amigos em comum permanece baixo.
- Simplicidade: O comando
SINTERdo Redis abstrai toda a lógica complexa de comparação de listas que precisaria ser feita manualmente no código da aplicação.
Ao navegar pelo perfil de outro usuário, o sistema agora pode validar instantaneamente quais conexões são compartilhadas, recuperando os IDs via Redis e populando os dados básicos do perfil via banco de dados ou cache de objetos.