RabbitMq - Work 模式
一、什么是Work模式
如果有几个消息都需要处理,且每个消息的处理时间很长,仅有一个消费者,那么当它在处理一个消息的时候,其他消息就只有等待。
等待有时候是好的,但在程序中并不那么好,当队列中有多个消息待处理,将其分发给多个消费者,当一个消费者在处理的时候,有其他消费者继续消费队列中的消息,便缓解了等待的尴尬。
那么这篇文章将实现一个生产者,多个消费者的模式,实现任务分发:work模式,如图所示。
二、消息确认机制
问题:怎样保证消息不因消费者gg而丢失
处理一个消息可能会花一定的时间,万一还没处理完消费者就gg了…生产者一发送消息,便会将其标记为已删除,故最终的结果是:这条消息没有得到正确的处理。而且,指派给该消费者且尚未处理的所有消息都会gg。
解决策略:取消自动回复机制
为了解决消息的丢失问题,RabbitMQ提供了消息确认机制:message acknowledgments,一个消费者处理完成后,将会回传一个ack给生产者,以表示处理成功,这样生产者才可以将消息删除。
这样即使一个消费者gg了,没有回传ack,那么发送者便会重发消息到队列,如果这时候有其他的消费者服务该队列,那么便会从队列中取出消息并处理。这就保证了消息的不丢失。
自动回复机制:不管是否处理成功,还是失败,都会回复ack。
1 channel.basicConsume(QUEUE_NAME, true, consumer);
自动恢复机制默认是打开的,在接收端的代码最后:第二个参数为true,表示会自动回复,只要生产发送消息,就会标记删除。所以我们需要将自动回复设置为false。
1 boolean autoAck = false; 2 channel.basicConsume(QUEUE_NAME, autoAck, consumer);
这样来保证消息不会因为消费者的gg而丢失了。
那么取消自动回复以后,我们需要手动回复一次:
1 channel.basicAck(envelope.getDeliveryTag(), false);
注意当前的消息确认机制只适用于同一个channel。
---------------------
application.yml
############################################################# ############## rabbitmq config ############################## ############################################################# spring.rabbitmq.host: 127.0.0.1 spring.rabbitmq.port: 5672 spring.rabbitmq.username: admin spring.rabbitmq.password: admin
1 package com.maozw.mq.config; 2 3 import org.slf4j.Logger; 4 import org.slf4j.LoggerFactory; 5 import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; 6 import org.springframework.amqp.rabbit.connection.ConnectionFactory; 7 import org.springframework.amqp.rabbit.core.RabbitTemplate; 8 import org.springframework.beans.factory.annotation.Value; 9 import org.springframework.beans.factory.config.ConfigurableBeanFactory; 10 import org.springframework.context.annotation.Bean; 11 import org.springframework.context.annotation.Configuration; 12 import org.springframework.context.annotation.Scope; 13 14 /** 15 * @author MAOZW 16 * @Description: Broker:它提供一种传输服务,它的角色就是维护一条从生产者到消费者的路线,保证数据能按照指定的方式进行传输, 17 * Exchange:消息交换机,它指定消息按什么规则,路由到哪个队列。 18 * Queue:消息的载体,每个消息都会被投到一个或多个队列。 19 * Binding:绑定,它的作用就是把exchange和queue按照路由规则绑定起来. 20 * Routing Key:路由关键字,exchange根据这个关键字进行消息投递。 21 * vhost:虚拟主机,一个broker里可以有多个vhost,用作不同用户的权限分离。 22 * Producer:消息生产者,就是投递消息的程序. 23 * Consumer:消息消费者,就是接受消息的程序. 24 * Channel:消息通道,在客户端的每个连接里,可建立多个channel. 25 * @date 2018/11/26 14:33 26 */ 27 @Configuration 28 public class RabbitConfig { 29 30 private static final Logger LOGGER = LoggerFactory.getLogger(RabbitConfig.class); 31 32 @Value("${spring.rabbitmq.host}") 33 private String rabbitmqHost; 34 35 @Value("${spring.rabbitmq.port}") 36 private int rabbitmqPort; 37 38 @Value("${spring.rabbitmq.username}") 39 private String userName; 40 41 @Value("${spring.rabbitmq.password}") 42 private String password; 43 44 45 public static final String EXCHANGE_A = "my-mq-exchange_A"; 46 public static final String EXCHANGE_B = "my-mq-exchange_B"; 47 public static final String EXCHANGE_C = "my-mq-exchange_C"; 48 49 50 public static final String QUEUE_A = "QUEUE_A"; 51 public static final String QUEUE_WORK = "QUEUE_WORK"; 52 public static final String QUEUE_C = "QUEUE_C"; 53 54 public static final String ROUTINGKEY_A = "spring-boot-routingKey_A"; 55 public static final String ROUTINGKEY_B = "spring-boot-routingKey_B"; 56 public static final String ROUTINGKEY_C = "spring-boot-routingKey_C"; 57 58 59 @Bean 60 public ConnectionFactory connectionFactory() { 61 CachingConnectionFactory connectionFactory = new CachingConnectionFactory(); 62 connectionFactory.setUsername(userName); 63 connectionFactory.setPassword(password); 64 connectionFactory.setVirtualHost("/vir_simple"); 65 connectionFactory.setPublisherConfirms(false); 66 return connectionFactory; 67 } 68 69 public static ConnectionFactory getConnectionFactory() { 70 CachingConnectionFactory connectionFactory = new CachingConnectionFactory(); 71 connectionFactory.setUsername("admin"); 72 connectionFactory.setPassword("admin"); 73 connectionFactory.setVirtualHost("/vir_simple"); 74 connectionFactory.setPublisherConfirms(false); 75 return connectionFactory; 76 } 77 }
生产者
1 package com.maozw.mq.work; 2 3 import com.maozw.mq.config.RabbitConfig; 4 import com.rabbitmq.client.Channel; 5 import org.slf4j.Logger; 6 import org.slf4j.LoggerFactory; 7 import org.springframework.amqp.rabbit.connection.Connection; 8 import org.springframework.amqp.rabbit.connection.ConnectionFactory; 9 import org.springframework.beans.factory.annotation.Autowired; 10 import org.springframework.web.bind.annotation.RequestMapping; 11 import org.springframework.web.bind.annotation.RestController; 12 13 import java.io.IOException; 14 15 /** 16 * work 模式 17 * 两种分发: 轮询分发 + 公平分发 18 * 轮询分发:消费端:自动确认消息;boolean autoAck = true; 19 * 公平分发: 消费端:手动确认消息 boolean autoAck = false; channel.basicAck(envelope.getDeliveryTag(),false); 20 * @author MAOZW 21 * @Description: ${todo} 22 * @date 2018/11/26 15:06 23 */ 24 @RestController 25 @RequestMapping("/work") 26 public class WorkProducer { 27 private static final Logger LOGGER = LoggerFactory.getLogger(WorkProducer.class); 28 @Autowired 29 RabbitConfig rabbitConfig; 30 31 @RequestMapping("/send") 32 public String send() throws IOException { 33 ConnectionFactory connectionFactory = rabbitConfig.connectionFactory(); 34 Connection connection = connectionFactory.createConnection(); 35 Channel channel = connection.createChannel(false); 36 //创建队列申明 37 channel.queueDeclare(RabbitConfig.QUEUE_WORK, false, false, false, null); 38 39 /** 40 * 每个消费者 发送确认消息之前,消息队列不会发送下一个消息给消费者,一次只处理一个消息 41 * 自动模式无需设置下面设置 42 */ 43 int prefetchCount = 1; 44 channel.basicQos(prefetchCount); 45 46 String Hello = ">>>> Hello Simple <<<<"; 47 for (int i = 0; i < 50; i++) { 48 String message = Hello + i; 49 channel.basicPublish("", RabbitConfig.QUEUE_WORK, null, message.getBytes()); 50 LOGGER.info("生产消息: " + message); 51 } 52 return "OK"; 53 } 54 }
消费者
1 package com.maozw.mq.work; 2 3 import com.maozw.mq.config.RabbitConfig; 4 import com.rabbitmq.client.AMQP; 5 import com.rabbitmq.client.Channel; 6 import com.rabbitmq.client.DefaultConsumer; 7 import com.rabbitmq.client.Envelope; 8 import org.slf4j.Logger; 9 import org.slf4j.LoggerFactory; 10 import org.springframework.amqp.rabbit.connection.Connection; 11 import org.springframework.amqp.rabbit.connection.ConnectionFactory; 12 13 import java.io.IOException; 14 15 /** 16 * @author MAOZW 17 * @Description: ${todo} 18 * @date 2018/11/26 15:06 19 */ 20 21 public class WorkConsumer2 { 22 private static final Logger LOGGER = LoggerFactory.getLogger(WorkConsumer2.class); 23 24 public static void main(String[] args) throws IOException { 25 ConnectionFactory connectionFactory = RabbitConfig.getConnectionFactory(); 26 Connection connection = connectionFactory.createConnection(); 27 Channel channel = connection.createChannel(false); 28 //创建队列申明 29 channel.queueDeclare(RabbitConfig.QUEUE_WORK, false, false, false, null); 30 31 /** 32 * 改变分发规则 33 */ 34 channel.basicQos(1); 35 36 DefaultConsumer consumer = new DefaultConsumer(channel) { 37 @Override 38 public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { 39 super.handleDelivery(consumerTag, envelope, properties, body); 40 System.out.println("[2] 接口数据 : " + new String(body, "utf-8")); 41 42 try { 43 Thread.sleep(200); 44 } catch (InterruptedException e) { 45 e.printStackTrace(); 46 } finally { 47 System.out.println("[2] done!"); 48 //消息应答:手动回执,手动确认消息 49 channel.basicAck(envelope.getDeliveryTag(),false); 50 } 51 } 52 }; 53 //监听队列 54 /** 55 * autoAck 消息应答 56 * 默认轮询分发打开:true :这种模式一旦rabbitmq将消息发送给消费者,就会从内存中删除该消息,不关心客户端是否消费正常。 57 * 使用公平分发需要关闭autoAck:false 需要手动发送回执 58 */ 59 boolean autoAck = false; 60 channel.basicConsume(RabbitConfig.QUEUE_WORK,autoAck, consumer); 61 } 62 63 }