Implementando Funcionalidade de Seguidores Comuns com Redis Sets

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 SINTER do 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.

Tags: Redis Spring Boot Data Structures backend java

Publicado em 6-19 01:34