Olá a todos, meu nome é Lou Tsai.
O artigo do RabbitMQ foi escrito antes, mas o exemplo dado na época foi a versão Demo. Este artigo integra principalmente o RabbitMQ no projeto técnico com base no conhecimento teórico escrito anteriormente.
Não BB, no diretório de artigos:
A seguir, vamos primeiro revisar o conhecimento teórico. Se você já tem esse conhecimento claro, pode pular diretamente para a parte de combate real.
1. Fila de mensagens
1.1 Modo de fila de mensagens
Existem atualmente dois modos principais de filas de mensagens, nomeadamente "modo ponto a ponto" e "modo de publicação/assinatura".
modo ponto a ponto
Uma mensagem específica só pode ser consumida por um consumidor, e vários produtores podem enviar mensagens para a mesma fila de mensagens, mas quando uma mensagem é processada por um mensageiro, a mensagem será bloqueada ou removida da fila e outros consumidores não poderão processar a mensagem .
Deve-se notar que se um consumidor não conseguir processar uma mensagem, o sistema de mensagens geralmente colocará a mensagem de volta na fila para que outros consumidores possam continuar o processamento.
Se você quiser aprender testes automatizados, recomendo aqui um conjunto de vídeos para você. Este vídeo pode ser considerado o tutorial de teste automatizado número um em toda a rede na estação B. Ao mesmo tempo, o número de pessoas online aumentou atingiu 1.000 e há notas para coletar e compartilhar com você.Dashen Technical Exchange: 798478386
modelo de publicação/assinatura
Uma única mensagem pode ser buscada e processada simultaneamente por vários assinantes. Em geral, existem dois tipos de assinaturas:
-
Assinaturas temporárias : essas assinaturas só existem enquanto o consumidor está instalado e funcionando. Depois que o consumidor sai, a assinatura correspondente e as mensagens não processadas são perdidas.
-
Assinatura durável (durável) : Esta assinatura sempre existirá, a menos que seja excluída ativamente. Após a saída do consumidor, o sistema de mensagens continuará a manter a assinatura e as mensagens subsequentes poderão continuar a ser processadas.
1.2 Características do RabbitMQ
-
Roteamento de mensagens (suporte) : RabbitMQ pode suportar diferentes tipos de roteamento de mensagens por meio de diferentes switches;
-
Mensagens ordenadas (não suportadas) : Ao consumir mensagens, se o consumo falhar, as mensagens serão colocadas de volta na fila e depois consumidas novamente, o que fará com que as mensagens fiquem fora de ordem;
-
Tempo de atraso da mensagem (muito bom) : através da fila de atraso, você pode especificar o tempo de atraso da mensagem, o tempo de expiração TTL, etc.;
-
Tratamento tolerante a falhas (muito bom) : Lida com falhas de processamento de mensagens com novas tentativas de entrega e Dead Letter Exchange (DLX);
-
Escalonamento (geral) : O escalonamento não é muito inteligente, porque mesmo que seja escalonado, ainda há apenas uma fila mestre, e apenas esta fila mestre pode resistir à carga, então entendo que o escalonamento do RabbitMQ é muito fraco (entendimento pessoal) .
-
Persistência (não muito boa) : Mensagens sem consumo podem suportar persistência.Isso é para garantir que as mensagens possam ser restauradas quando a máquina estiver inoperante, mas as mensagens consumidas serão excluídas imediatamente, porque o RabbitMQ não foi projetado para armazenar dados históricos.
-
Retrocesso de mensagens (suporte) : como as mensagens não suportam armazenamento permanente, o retrocesso naturalmente não é suportado.
-
Alto rendimento (médio) : Como todas as solicitações são executadas na fila mestre no final, seu design faz com que o desempenho de uma única máquina não atinja o padrão de nível 100.000.
2. Um estudo preliminar sobre o princípio do RabbitMQ
Lançado em 2007, RabbitMQ é um sistema de enfileiramento de mensagens de código aberto desenvolvido na linguagem Erlang e implementado com base no protocolo AMQP.
2.1 Conceitos básicos
Quando se trata de RabbitMQ, é preciso mencionar o protocolo AMQP. O protocolo AMQP é um protocolo binário com recursos modernos. É um protocolo avançado de enfileiramento de mensagens padrão da camada de aplicativo que fornece serviços de mensagens unificadas.É um padrão aberto para protocolos da camada de aplicativo e é projetado para middleware orientado a mensagens.
Primeiro entenda vários conceitos importantes do protocolo AMQP:
-
Servidor: Receba a conexão do cliente e implemente o serviço da entidade AMQP.
-
Conexão: conexão, conexão de rede entre aplicação e servidor, conexão TCP.
-
Canal: As operações de canal, leitura e gravação de mensagens são executadas no canal. Um cliente pode estabelecer múltiplos canais, cada canal representando uma tarefa de sessão.
-
Mensagem: Mensagem, os dados transmitidos entre a aplicação e o servidor, a mensagem pode ser muito simples ou complexa. Consiste em Propriedades e Corpo. Propriedades é a embalagem externa, que pode modificar a mensagem, como prioridade da mensagem, atraso e outros recursos avançados; Corpo é o conteúdo do corpo da mensagem.
-
Host Virtual: host virtual para isolamento lógico. Pode haver várias trocas e filas em um host virtual e não pode haver trocas ou filas com o mesmo nome no mesmo host virtual.
-
Exchange: A exchange recebe mensagens e as encaminha para uma ou mais filas de acordo com regras de roteamento. Se a rota não estiver disponível, devolva-a ao produtor ou descarte-a diretamente. Existem quatro tipos de switches comumente usados pelo RabbitMQ: direto, tópico, fanout e cabeçalhos, que serão descritos em detalhes posteriormente.
-
Vinculação: ligação, conexão virtual entre a troca e a fila de mensagens, a ligação pode conter um ou mais RoutingKey.
-
RoutingKey: Chave de roteamento. Quando o produtor envia uma mensagem ao switch, ele enviará um RoutingKey para especificar as regras de roteamento, para que o switch saiba para qual fila enviar a mensagem. A chave de roteamento geralmente é uma string separada por ".", como "com.rabbitmq".
-
Fila: Fila de mensagens, usada para armazenar mensagens para consumo dos consumidores.
2.2 Princípio de funcionamento
O modelo do protocolo AMQP consiste em três partes: produtor, consumidor e servidor. O processo de execução é o seguinte:
-
O produtor se conecta ao servidor, estabelece uma conexão e abre um canal.
-
Os produtores declaram trocas e filas, definem propriedades relacionadas e vinculam trocas e filas por meio de chaves de roteamento.
-
Os consumidores também precisam realizar operações como estabelecer uma conexão e abrir um canal para receber mensagens.
-
O produtor envia uma mensagem ao host virtual no servidor.
-
O switch no host virtual seleciona regras de roteamento de acordo com a chave de roteamento e as envia para diferentes filas de mensagens.
-
Os consumidores que assinam a fila de mensagens podem receber a mensagem e consumi-la.
2.3 Interruptores comumente usados
Existem quatro tipos de opções comumente usadas pelo RabbitMQ: direto, tópico, fanout e cabeçalhos:
-
Troca Direta: Como você pode ver no texto, troca direta significa que a troca precisa estar vinculada a uma fila, exigindo que a mensagem corresponda exatamente a uma chave de roteamento específica. Simplificando, é um envio ponto a ponto, um para um.
-
Fanout Exchange: Este tipo de troca requer filas vinculadas à troca. Uma mensagem enviada para uma exchange é encaminhada para todas as filas vinculadas a essa exchange. Muito parecido com a transmissão de sub-rede, cada host na sub-rede recebe uma cópia da mensagem. Simplificando, é publicar e assinar.
-
Troca de Tópicos: Se traduzido diretamente, é chamado de troca de tópicos. Se for traduzido a partir do uso acima, pode ser chamado de troca de curinga, o que seria mais apropriado. Essa opção usa curingas para corresponder e rotear para a fila correspondente. Existem dois tipos de curingas: "*" e "#". Ressalta-se que o símbolo “.” deve ser adicionado antes do curinga.
-
* símbolo: Existe e apenas uma palavra corresponde. Por exemplo, a.* pode corresponder a "ab" e "ac", mas não a "abc".
-
# símbolo: Combine uma ou mais palavras. Por exemplo, "rabbit.#" pode corresponder não apenas a "rabbit.ab", "rabbit.a", mas também a "rabbit.abc".
-
-
Troca de cabeçalhos: esse tipo de opção relativamente não é muito usado. É um pouco diferente dos três tipos acima: seu roteamento não usa routingKey para correspondência de roteamento, mas roteamento combinando o valor da chave transportado no cabeçalho da solicitação. Para criar uma fila, você precisa definir as informações do cabeçalho de ligação. Existem dois modos: correspondência completa e correspondência parcial. Conforme mostrado na figura acima, o switch irá combinar o valor da chave vinculado à fila de acordo com o valor da chave transportado nas informações do cabeçalho enviadas pelo produtor e encaminhar para a fila correspondente.
3. Construção do ambiente RabbitMQ
Como uso um Mac, posso consultar diretamente o site oficial:
https://www.rabbitmq.com/install-homebrew.html
Deve-se notar que deve ser executado primeiro:
brew update
Em seguida, execute:
brew install rabbitmq
Antes de brew update não ser executado, quando brew install RabbitMQ era executado diretamente, vários erros estranhos eram relatados, a maioria dos quais era "403 Forbidde".
Mas quando você executa "brew install coelhomq", outros programas serão instalados automaticamente. Se você instalar o Rabbitmq usando o código fonte, porque iniciar o serviço depende do ambiente erlang, você precisa instalar o erlang manualmente, mas o oficial já fez isso para você com um clique. Não é ótimo instalar automaticamente todos os programas dos quais o Rabbitmq depende!
A saída da execução final bem-sucedida é a seguinte:
Inicie o serviço:
# 启动方式1:后台启动
brew services start rabbitmq
# 启动方式2:当前窗口启动
cd /usr/local/Cellar/rabbitmq/3.8.19
rabbitmq-server
Digite no navegador:
http://localhost:15672/
A interface de gerenciamento de segundo plano do RabbitMQ aparecerá (o nome de usuário e a senha são convidados):
Instalado através do brew, uma linha de comando para fazer isso, realmente perfumado!
4. Integração RabbitMQ
4.1 Trabalho preliminar
Adicionar Conta:
## 添加账号
./rabbitmqctl add_user admin admin
## 添加访问权限
./rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
## 设置超级权限
./rabbitmqctl set_user_tags admin administrator
Pom introduz dependências:
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.5.1</version>
</dependency>
4.2 Implementação do código
código principal
Primeiro, conclua um singleton ConnectionFactory, cada máquina tem seu próprio ConnectionFactory para evitar a inicialização todas as vezes (em iterações posteriores, irei removê-lo e transformá-lo em um pool de conexões).
/**
* @author Louzai
* @date 2023/5/10
*/
public class RabbitmqUtil {
/**
* 每个key都有自己的工厂
*/
private static Map<String, ConnectionFactory> executors = new ConcurrentHashMap<>();
/**
* 初始化一个工厂
*
* @param host
* @param port
* @param username
* @param passport
* @param virtualhost
* @return
*/
public static ConnectionFactory init(String host,
Integer port,
String username,
String passport,
String virtualhost) {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost(host);
factory.setPort(port);
factory.setUsername(username);
factory.setPassword(passport);
factory.setVirtualHost(virtualhost);
return factory;
}
/**
* 工厂单例,每个key都有属于自己的工厂
*
* @param key
* @param host
* @param port
* @param username
* @param passport
* @param virtualhost
* @return
*/
public static ConnectionFactory getOrInitConnectionFactory(String key,
String host,
Integer port,
String username,
String passport,
String virtualhost) {
ConnectionFactory connectionFactory = executors.get(key);
if (null == connectionFactory) {
synchronized (RabbitmqUtil.class) {
connectionFactory = executors.get(key);
if (null == connectionFactory) {
connectionFactory = init(host, port, username, passport, virtualhost);
executors.put(key, connectionFactory);
}
}
}
return connectionFactory;
}
}
Obtenha RabbitmqClient:
/**
* @author Louzai
* @date 2023/5/10
*/
@Component
public class RabbitmqClient {
@Autowired
private RabbitmqProperties rabbitmqProperties;
/**
* 创建一个工厂
* @param key
* @return
*/
public ConnectionFactory getConnectionFactory(String key) {
String host = rabbitmqProperties.getHost();
Integer port = rabbitmqProperties.getPort();
String userName = rabbitmqProperties.getUsername();
String password = rabbitmqProperties.getPassport();
String virtualhost = rabbitmqProperties.getVirtualhost();
return RabbitmqUtil.getOrInitConnectionFactory(key, host, port, userName,password, virtualhost);
}
}
foco! Bata no quadro-negro! ! ! Aqui está a lógica central do RabbmitMQ.
O tipo de switch que usamos é o Direct Exchange. Esse switch precisa estar vinculado a uma fila e exige que a mensagem corresponda exatamente a uma chave de roteamento específica. Simplificando, é um envio ponto a ponto, um para um.
Quanto ao motivo de não utilizarmos o modo de transmissão e troca de tópico, porque o cenário de utilização da escola técnica é o envio de uma única mensagem, e o modo de envio e consumo ponto a ponto pode atender plenamente às nossas necessidades.
Os três métodos a seguir são muito simples:
-
Enviar mensagem: obter fábrica -> criar link -> criar canal -> declarar switch -> enviar mensagem -> fechar link;
-
Consumir mensagens: obter fábrica -> criar link -> criar canal -> determinar fila de mensagens -> vincular fila ao switch -> aceitar e consumir mensagens;
-
Modo perpétuo de mensagem de consumo: consome mensagens RabbitMQ em modo sem bloqueio.
@Component
public class RabbitmqServiceImpl implements RabbitmqService {
@Autowired
private RabbitmqClient rabbitmqClient;
@Autowired
private NotifyService notifyService;
@Override
public void publishMsg(String exchange,
BuiltinExchangeType exchangeType,
String toutingKey,
String message) throws IOException, TimeoutException {
ConnectionFactory factory = rabbitmqClient.getConnectionFactory(toutingKey);
// TODO: 这种并发量起不来,需要改造成连接池
//创建连接
Connection connection = factory.newConnection();
//创建消息通道
Channel channel = connection.createChannel();
// 声明exchange中的消息为可持久化,不自动删除
channel.exchangeDeclare(exchange, exchangeType, true, false, null);
// 发布消息
channel.basicPublish(exchange, toutingKey, null, message.getBytes());
System.out.println("Publish msg:" + message);
channel.close();
connection.close();
}
@Override
public void consumerMsg(String exchange,
String queue,
String routingKey) throws IOException, TimeoutException {
ConnectionFactory factory = rabbitmqClient.getConnectionFactory(routingKey);
// TODO: 这种并发量起不来,需要改造成连接池
//创建连接
Connection connection = factory.newConnection();
//创建消息信道
final Channel channel = connection.createChannel();
//消息队列
channel.queueDeclare(queue, true, false, false, null);
//绑定队列到交换机
channel.queueBind(queue, exchange, routingKey);
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println("Consumer msg:" + message);
// 获取Rabbitmq消息,并保存到DB
// 说明:这里仅作为示例,如果有多种类型的消息,可以根据消息判定,简单的用 if...else 处理,复杂的用工厂 + 策略模式
notifyService.saveArticleNotify(JsonUtil.toObj(message, UserFootDO.class), NotifyTypeEnum.PRAISE);
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 取消自动ack
channel.basicConsume(queue, false, consumer);
}
@Override
public void processConsumerMsg() {
System.out.println("Begin to processConsumerMsg.");
Integer stepTotal = 1;
Integer step = 0;
// TODO: 这种方式非常 Low,后续会改造成阻塞 I/O 模式
while (true) {
step ++;
try {
System.out.println("processConsumerMsg cycle.");
consumerMsg(CommonConstants.EXCHANGE_NAME_DIRECT, CommonConstants.QUERE_NAME_PRAISE,
CommonConstants.QUERE_KEY_PRAISE);
if (step.equals(stepTotal)) {
Thread.sleep(10000);
step = 0;
}
} catch (Exception e) {
}
}
}
}
Aqui está apenas um exemplo: se você quiser usá-lo em um ambiente de produção, que problemas você acha que existem? Você pensa sobre isso primeiro, e eu contarei no final do artigo.
entrada de chamada
Na verdade, usamos o método de chamada assíncrona integrado do Java antes. Para facilitar a verificação, migrei a função de curtidas de artigos para o RabbitMQ. Enquanto for curtida, seguiremos o modo RabbitMQ.
// 点赞消息走 RabbitMQ,其它走 Java 内置消息机制
if (notifyType.equals(NotifyTypeEnum.PRAISE) && rabbitmqProperties.getSwitchFlag()) {
rabbitmqService.publishMsg(
CommonConstants.EXCHANGE_NAME_DIRECT,
BuiltinExchangeType.DIRECT,
CommonConstants.QUERE_KEY_PRAISE,
JsonUtil.toStr(foot));
} else {
Optional.ofNullable(notifyType).ifPresent(notify -> SpringUtil.publishEvent(new NotifyMsgEvent<>(this, notify, foot)));
}
Onde fica a entrada do consumo? Na verdade, quando o programa é iniciado, iniciamos o RabbitMQ para consumo e então todo o processo continua rodando no programa.
@Override
public void run(ApplicationArguments args) {
// 设置类型转换, 主要用于mybatis读取varchar/json类型数据据,并写入到json格式的实体Entity中
JacksonTypeHandler.setObjectMapper(new ObjectMapper());
// 应用启动之后执行
GlobalViewConfig config = SpringUtil.getBean(GlobalViewConfig.class);
if (webPort != null) {
config.setHost("http://127.0.0.1:" + webPort);
}
// 启动 RabbitMQ 进行消费
if (rabbitmqProperties.getSwitchFlag()) {
taskExecutor.execute(() -> rabbitmqService.processConsumerMsg());
}
log.info("启动成功,点击进入首页: {}", config.getHost());
}
4.3 Demonstração
Clicamos no botão "Curtir" várias vezes para acionar o envio da mensagem RammitMQ.
Você também pode ver as mensagens enviadas e consumidas através do log.
Eu confio! Muitos links não fechados. . .
Ainda há muitos canais que não estão fechados. . .
Estima-se que depois de um tempo de funcionamento, toda a memória se esgotará e a máquina travará, como pode quebrar? A resposta é o pool de conexões!
4.4 Ramos de código
Para facilitar a todos o aprendizado do processo de evolução da função, cada módulo abrirá uma ramificação separada, incluindo a versão atualizada posteriormente:
-
Armazém de código: https://github.com/itwanger/paicoding
-
Ramo de código: feature/add_rabbitmq_20230506
Se você precisar executar o RabbitMQ, a configuração a seguir precisará ser alterada para verdadeiro, porque o código padrão é falso.
5 Epílogo
Este artigo permite que todos conheçam os princípios básicos do RabbitMQ e como integrar o RabbitMQ, mas não pode ser usado no ambiente de produção real, mas esta é realmente a primeira versão que escrevi, e é puramente por diversão, porque existem Existem ainda muitos problemas.
Vou listar resumidamente:
-
Você precisa adicionar um pool de conexões ao Connection, caso contrário a memória continuará a ser consumida e a máquina definitivamente não será capaz de lidar com isso;
-
O método de consumo do RabbitMQ precisa ser transformado, pois o método while + sleep é muito simples e rude;
-
Se a tarefa de consumo for interrompida, você precisará de um mecanismo de consumo para reiniciar o RabbitMQ;
-
Se a máquina desligar, as mensagens dentro do RabbitMQ não poderão ser perdidas após a reinicialização.
Se você também estiver muito interessado nas questões acima, pode basear-se diretamente no branch feature/add_rabbitmq_20230506 e, em seguida, me dar um PR, tecnologia, gosto de aprender enquanto jogo.