【2023】Um resumo das maneiras como o Redis implementa filas de mensagens e implementação de código

Prefácio

Muitas vezes ouço muitas pessoas discutindo a questão de "se é apropriado usar o Redis como uma fila".

Algumas pessoas concordam: elas acham que o Redis é muito leve e conveniente para usar como fila. Algumas pessoas se opõem, pensando que o Redis “perderá” dados e é melhor usar middleware de fila “profissional” para ser mais seguro.

Este artigo discutirá se é apropriado usar o Redis como fila. Iremos guiá-lo passo a passo, do simples ao complexo, para resolver os detalhes e mostrar os métodos de implementação comumente usados.

Antes que você comece

1. Adicione dependências

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
                <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
                <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
                <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.4.0</version>
        </dependency>

2. Adicione beans configurados

Evite a inconveniência de usar software para visualizar dados armazenados

    /**
     * redisTemplate 序列化使用的jdkSerializeable, 存储二进制字节码, 所以自定义序列化类
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    
    
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        // 使用Jackson2JsonRedisSerialize 替换默认序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        // 设置value的序列化规则和 key的序列化规则
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //jackson2JsonRedisSerializer就是JSON序列号规则,
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

Implementação

1. Comece com o mais simples: listar fila

Primeiro, vamos começar com o cenário mais simples.

Se as necessidades do seu negócio são bastante simples e você deseja usar o Redis como fila, a primeira coisa que vem à mente é usar o tipo de dados Lista.

Como a implementação subjacente de List é uma "lista vinculada", a complexidade de tempo dos elementos operacionais no início e no final é O(1), o que significa que é muito consistente com o modelo de fila de mensagens.

Se você tratar List como uma fila, poderá usá-la assim.

Código

O lado do produtor lê:

@RestController
@RequestMapping("/redis01")
public class RedisTest1 {
    
    

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;


	//LPUSH 发布消息
    @GetMapping("/set")
    public void set(String code){
    
    
        redisTemplate.opsForList().leftPush("code",code);
    }
    // RPOP 拉取消息
    @GetMapping("/get1")
    public String get1(String key){
    
    
        Object code =  redisTemplate.opsForList().rightPop(key);
        if (code!=null){
    
    
            return code.toString();
        }
        return "redis中没数据!";
    }

Modelo de implementação:
Este modelo também é muito simples e fácil de entender.Insira a descrição da imagem aqui

Mas há um pequeno problema aqui: quando não há mensagem na fila, o consumidor retornará NULL ao executar o RPOP.
Insira a descrição da imagem aqui
Geralmente, ao escrever um consumidor, um loop infinito é adotado. Este método de implementação consiste em extrair dados continuamente da fila.

    @GetMapping("/get2")
    public String get2(String key) throws InterruptedException {
    
    
        while (true){
    
    
            Object code = redisTemplate.opsForList().rightPop(key);
            System.out.println(code);
 //            读取到消息,退出,没读到继续循环
            if (code!=null){
    
    
                return code.toString();
            }
        }
    }

Se a fila estiver vazia neste momento, o consumidor ainda puxará mensagens com frequência, o que causará "inatividade da CPU", que não apenas desperdiça recursos da CPU, mas também pressiona o Redis.

Como resolver este problema?

Também é muito simples, quando a fila está vazia podemos "dormir" um pouco e depois tentar extrair a mensagem. O código pode ser modificado assim:

    @GetMapping("/get2")
    public String get2(String key) throws InterruptedException {
    
    
        while (true){
    
    
            Object code = redisTemplate.opsForList().rightPop(key);
            System.out.println(code);
 //            读取到消息,退出,没读到继续循环
            if (code!=null){
    
    
                return code.toString();
            }
            Thread.sleep(2000);
        }
    }

Isso resolve o problema de inatividade da CPU.

Embora esse problema seja resolvido, ele traz outro problema: quando uma nova mensagem chega enquanto o consumidor está dormindo e esperando, haverá um “atraso” no processamento da nova mensagem pelo consumidor.

Supondo que o tempo de sleep definido seja de 2s, haverá um atraso máximo de 2s para novas mensagens.

Para diminuir esse atraso, você só pode reduzir o tempo de sono. No entanto, quanto menor o tempo de suspensão, maior a probabilidade de a CPU ficar ociosa.

Você não pode ter as duas coisas.

Então, como podemos processar novas mensagens em tempo hábil e evitar a ociosidade da CPU?

O Redis tem esse mecanismo: se a fila estiver vazia, o consumidor irá "bloquear e esperar" ao extrair mensagens. Assim que uma nova mensagem chegar, meu consumidor será notificado para processar a nova mensagem imediatamente?

Felizmente, o Redis fornece comandos de "bloqueio" para extrair mensagens: BRPOP / BLPOP , onde B se refere a Bloquear.
Insira a descrição da imagem aqui
Também foi encapsulado em java. Ao chamar o método pop, basta definir um tempo de expiração diretamente.

    @GetMapping("/get3")
    public String get3(String key) throws InterruptedException {
    
    
        Object code = redisTemplate.opsForList().rightPop(key,0, TimeUnit.SECONDS);
        if (code==null){
    
    
            return "数据读取超时!";
        }
        return code.toString();
    }

Ao usar o método de bloqueio do BRPOP para extrair mensagens, ele também suporta a passagem de um "período de tempo limite". Se definido como 0, significa que o tempo limite não será definido e não retornará até que haja uma nova mensagem. Caso contrário, NULL será retornado após o período de tempo limite especificado.

Esta solução é boa, pois não só leva em consideração a eficiência, mas também evita o problema de ociosidade da CPU, matando dois coelhos com uma cajadada só.

Nota: Se o tempo limite for definido muito longo e a conexão não estiver ativa por muito tempo, ela poderá ser considerada uma conexão inválida pelo Redis Server e, em seguida, o Redis Server forçará o cliente a ser colocado offline
. Portanto, utilizando esta solução, o cliente deve possuir um mecanismo de reconexão.

Depois de resolver o problema do processamento tardio de mensagens, você pode pensar novamente: quais são as deficiências desse modelo de fila?

Vamos analisar juntos:

  1. O consumo repetido não é suportado : depois que o consumidor extrai a mensagem, a mensagem é excluída da lista e não pode ser consumida novamente por outros consumidores, ou seja, vários consumidores não têm suporte para consumir o mesmo lote de dados.
  2. Perda de mensagem : Depois que o consumidor extrai a mensagem, se ocorrer um tempo de inatividade anormal, a mensagem será perdida. O primeiro
    problema é funcional. Usar Lista como fila de mensagens suporta apenas o mais simples, um grupo de produtores. Correspondente a um grupo de consumidores , não consegue atender aos cenários de negócios de múltiplos grupos de produtores e consumidores.

O segundo problema é mais difícil, porque depois que uma mensagem é POP da Lista, a mensagem será imediatamente excluída da lista vinculada. Em outras palavras, não importa se o consumidor a processa com sucesso ou não, esta mensagem não pode ser consumida novamente.

Isso também significa que se o consumidor travar de forma anormal durante o processamento da mensagem, a mensagem será equivalente a ser perdida.

Como resolver esses dois problemas? Vamos examiná-los um por um.

2. Modelo de publicação e assinatura: Pub/Sub

Como você pode perceber pelo nome, este módulo foi projetado pelo Redis especificamente para o modelo de fila "publicar/assinar".

Pode resolver exatamente o primeiro problema mencionado anteriormente: o consumo repetido.

Ou seja, no cenário de múltiplos grupos de produtores e consumidores, vamos ver como isso se faz.

O Redis fornece comandos PUBLISH/SUBSCRIBE para concluir as operações de publicação e assinatura.
Insira a descrição da imagem aqui
Continue usando o anterior para dependências.

1. Use RedisMessageListenerContainer para implementar assinatura

  • Processe mensagens recebidas implementando a interface MessageListener. Isso permite que você lide com mensagens de uma forma mais avançada em seu aplicativo Spring, como usando injeção de dependência e outros recursos do Spring. Ele também suporta ouvintes de mensagens baseados em anotações, tornando o processamento de mensagens mais conciso e flexível.
  • Este método é fornecido pela biblioteca Spring Data Redis para usar as funções de publicação e assinatura do Redis em aplicativos Spring. Requer a criação de um objeto MessageListenerContainer e a adição de um ouvinte de mensagem chamando o método addMessageListener.
  • Adicione monitores para monitorar canais.
    Para facilitar um teste de comparação mais intuitivo, adicionei dois
/**
 * @author zhengfuping
 * @version 1.0
 * @description: TODO  配置监控器
 * @date 2023/7/28 17:10
 */
@Component
public class RedisMessaeListener1 implements MessageListener {
    
    
    @Override
    public void onMessage(Message message, byte[] pattern) {
    
    

        String channel = new String(message.getChannel());
        String body = new String(message.getBody());
        System.out.println("监听器1号:消息: " + body + " 通道QQ: " + channel);
    }
}

/*########################*/

/**
 * @author zhengfuping
 * @version 1.0
 * @description: TODO 配置监控器1
 * @date 2023/8/2 11:24
 */
@Component
public class RedisMessaeListener2 implements MessageListener {
    
    
    @Override
    public void onMessage(Message message, byte[] pattern) {
    
    
        String channel = new String(message.getChannel());
        String body = new String(message.getBody());
        System.out.println("监听器2号:消息: " + body + " 通道QQ: " + channel);
    }
}

  • As assinaturas de configuração podem ser únicas ou múltiplas
    para vinculação de tópicos (canais) e ouvintes. O envio é
/**
 * @author zhengfuping
 * @version 1.0
 * @description: TODO 使用RedisMessageListenerContainer直接注入到bean进行监听
 * @date 2023/7/28 15:43
 */
@Configuration
public class RedisPubSubExample {
    
    

    @Autowired
    private  RedisMessaeListener1 redisMessaeListener1;
    @Autowired
    private  RedisMessaeListener2 redisMessaeListener2;

    /**
     * 订阅三个频道
     * @author zhengfuping
     * @date 2023/8/2 11:19
     * @param redisConnectionFactory redis线程工厂
     * @return RedisMessageListenerContainer
     */
//    @Bean
    public RedisMessageListenerContainer subscribeToChannel(RedisConnectionFactory redisConnectionFactory){
    
    
        RedisMessageListenerContainer listenerContainer = new RedisMessageListenerContainer();

        listenerContainer.setConnectionFactory(redisConnectionFactory);
        List<Topic> list = new ArrayList<>();
        list.add(new PatternTopic("TEST01"));
        list.add(new PatternTopic("TEST02"));
        list.add(new PatternTopic("TEST03"));
        /*
         *  redisMessaeListener  消息监听器
         *  list  订阅的主题(可以单个和多个)
         */
        listenerContainer.addMessageListener(redisMessaeListener1,list);
        listenerContainer.addMessageListener(redisMessaeListener2,new PatternTopic("TEST01"));
        return listenerContainer;
    }
}

  • Enviar mensagem para canal especificado
    /**
     *  PUBLISH 发送消息到指定频道
     * @author zhengfuping
     * @date 2023/8/2 11:14
     * @param channel 通道
     * @param name  数据
     * @param age
     * @return Object
     */
    @GetMapping("/pub")
    public Object pub(String channel,String name,Integer age) {
    
    
        User user = new User(name, age);
        redisTemplate.convertAndSend(channel,user);
        return user;
    }

Insira a descrição da imagem aqui

2. Você também pode usar redisTemplate para implementar assinatura

  • Este método é um método do cliente Redis e é usado para usar diretamente a função de publicação e assinatura em um cliente Redis independente. Requer a criação de um objeto de conexão Redis e a assinatura de um ou mais canais chamando o método subscribe.

  • Se você estiver usando apenas o recurso publicar-assinar em um cliente Redis independente e não precisar usar outros recursos do Spring, poderá escolher connection.subscribe

 /**
 * 自行添加订阅
 */
    @GetMapping("/sub")
    public void sub(String channel) {
    
    
        RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();

        /*
         *  MessageListener:监听器,直接使用内部类实现绑定监听可以把数据传递出去
         *  channel 订阅频道
         */
        connection.subscribe((message, pattern) -> {
    
    
            String channel1 = new String(message.getChannel());
            String body = new String(message.getBody());
            System.out.println("subscribe方式监听:消息: " + body + " 通道QQ: " + channel1);
        }, channel.getBytes());

//            connection.close();
    }

Insira a descrição da imagem aqui

Envie uma mensagem

    @GetMapping("/pub")
    public Object pub(String channel,String name,Integer age) {
    
    
        User user = new User(name, age);
        redisTemplate.convertAndSend(channel,user);
        return user;
    }

Insira a descrição da imagem aqui

O resultado final do monitoramento
Insira a descrição da imagem aqui

3. Filas com tendência a amadurecer: Stream

Vamos ver como o Stream resolve os problemas acima.

Vamos ainda ir do simples ao complexo e ver como o Stream é processado ao fazer filas de mensagens?

Primeiro, o Stream completa o modelo mais simples de produção e consumo por meio de XADD e XREAD:

  • O produtor publica 2 mensagens:
// *表示让Redis自动生成消息ID
127.0.0.1:6379> XADD queue * name zhangsan
"1618469123380-0"
127.0.0.1:6379> XADD queue * name lisi
"1618469127777-0"
  • Os consumidores extraem mensagens:
// 从开头读取5条消息,0-0表示从开头读取
127.0.0.1:6379> XREAD COUNT 5 STREAMS queue 0-0
1) 1) "queue"
   2) 1) 1) "1618469123380-0"
         2) 1) "name"
            2) "zhangsan"
      2) 1) "1618469127777-0"
         2) 1) "name"
            2) "lisi"

fluxograma
Insira a descrição da imagem aqui

Implementação específica de código java:

  • Primeiro configure a classe da mensagem de escuta
@Slf4j
@Component
public class ListenerMessage implements StreamListener<String, MapRecord<String, String, String>> {
    
    

    @Override
    public void onMessage(MapRecord<String, String, String> entries) {
    
    
        log.info("接受到来自redis的消息");
        System.out.println("message id "+entries.getId());
        System.out.println("stream "+entries.getStream());
        System.out.println("body "+entries.getValue());
    }
}
  • Adicionar classe de ferramenta para implementar a inicialização
@Component
@Slf4j
public class RedisStreamUtil {
    
    
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;


    /**
     * @author zhengfuping 添加数据
     * @param streamKey
     * @param map
     * @return RecordId
     */
    public RecordId addStream(String streamKey,Map<String, Object> map){
    
    
        RecordId recordId = redisTemplate.opsForStream().add(streamKey, map);
        return recordId;
    }

    /**
     * 用来创建绑定流和组
     */
    public void addGroup(String key, String groupName){
    
    
        redisTemplate.opsForStream().createGroup(key,groupName);
    }

    /**
     * 用来判断key是否存在
     */
    public boolean hasKey(String key){
    
    
        if(key==null){
    
    
            return false;
        }else{
    
    
            return redisTemplate.hasKey(key);
        }

    }
    /**
     * 用来删除掉消费了的消息
     */
    public void delField(String key,String recordIds){
    
    
        redisTemplate.opsForStream().delete(key,recordIds);
    }

    /**
     * 用来初始化 实现绑定
     */
    public void initStream(String key, String group){
    
    
        //判断key是否存在,如果不存在则创建
        boolean hasKey = hasKey(key);
        if(!hasKey){
    
    
            Map<String,Object> map = new HashMap<>();
            map.put("field","value");
            RecordId recordId = addStream(key, map);
            addGroup(key,group);   //把Stream和gropu绑定
            delField(key,recordId.getValue());
            log.info("stream:{}-group:{} initialize success",key,group);
        }
    }
}
  • Adicione classe de configuração e configure Stream
/**
 * @author zhengfuping
 * @version 1.0
 * @description: TODO  添加配置类,配置Stream
 */
@Configuration
@Slf4j
public class RedisStreamConfig {
    
    

    @Autowired
    private RedisStreamUtil redisStream;
    @Autowired
    private ListenerMessage listenerMessage;


    @Bean
    public Subscription subscription(RedisConnectionFactory factory){
    
    
//        代码中的var是使用了Lombok的可变局部变量。主要是为了方便
//        StreamMessageListenerContainer: 消息侦听容器,不能在外部实现。创建后,StreamMessageListenerContainer可以订阅Redis流并使用传入的消息
        var options = StreamMessageListenerContainer
                .StreamMessageListenerContainerOptions
                .builder()
                .pollTimeout(Duration.ofSeconds(1))
                .build();
        redisStream.initStream("mystream","mygroup");	//调用初始化
        var listenerContainer = StreamMessageListenerContainer.create(factory,options);

        /*
         *  注意这里接受到消息后会被自动的确认,如果不想自动确认请使用其他的创建订阅方式
         * 消费组 consumer group ,它不能为null (Consumer类型)
         * stream offset ,stream的偏移量(StreamOffset 类型)
         * listener 不能为null (StreamListener<K,V> 类型)
         */
        var subscription = listenerContainer.receiveAutoAck(Consumer.from("mygroup","huhailong"),
                StreamOffset.create("mystream", ReadOffset.lastConsumed()),listenerMessage);

        listenerContainer.start();
        return subscription;
    }
}

  • Teste de chamada
/**
 * @author zhengfuping
 * @version 1.0
 * @description: TODO
 * @date 2023/8/2 16:06
 */
@RestController
@RequestMapping("/redisStream")
public class RedisStreamTest {
    
    

    @Autowired
    private RedisStreamUtil redisStream;

    @GetMapping("add")
    public void add(String key,String data){
    
    
        Map<String, Object> map = new HashMap<>();
        map.put(key,data);
//        添加数据到mystream流中
        RecordId recordId = redisStream.addStream("mystream", map);
//        删除流中消费了的指定key的数据
        redisStream.delField("mystream",recordId.getValue());
    }
}

A vantagem do Stream é que ele pode ser gravado em RDB e AOF para persistência.
Stream é um tipo de dados recém-adicionado. Como outros tipos de dados, cada operação de gravação também será gravada em RDB e AOF.
Precisamos apenas configurar a estratégia de persistência, para que mesmo que o Redis esteja inativo e reiniciado, os dados no Stream possam ser recuperados do RDB ou AOF.

Resumir

Ok, vamos resumir. Neste artigo, partimos da perspectiva de "O Redis pode ser usado como fila" e apresentamos como List, Pub/Sub e Stream são usados ​​como filas, bem como suas respectivas vantagens e desvantagens.

Mais tarde, comparei o Redis com o middleware de fila de mensagens profissional e descobri as deficiências do Redis.

Finalmente, chegamos a um cenário adequado para o Redis fazer filas.

Insira a descrição da imagem aqui

Acho que você gosta

Origin blog.csdn.net/weixin_52315708/article/details/132045977
Recomendado
Clasificación