准备工作
第一步:在SpringBoot的pom.xml中引入AMQP高级消息队列协议的依赖。
org.springframework.boot
spring-boot-starter-amqp
第二步:配置系统配置文件(根据需要)
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
virtual-host: /
publisher-confirms: true # 开启发送确认
publisher-returns: true # 开启发送失败退回
listener:
direct:
acknowledge-mode: manual # 开启ACK
retry:
enabled: true # 消费者端的重试
simple:
retry:
enabled: true # 消费者端的重试
auto-startup: true # 启动时自动启动容器
default-requeue-rejected: true # 投递失败时是否重新排队
acknowledge-mode: manual # 开启ACK
template:
reply-timeout: 10000 # 超时时间
retry:
enabled: true # RabbitTemplate(生产端)实现重试
initial-interval: 1000 # 第一次与第二次发布消息的时间间隔
max-attempts: 3 # 尝试发布消息的最大数量
max-interval: 10000 # 尝试发布消息的最大时间间隔
multiplier: 1.0 # 上一次尝试时间间隔的乘数
第三步:配置Rabbitmq类(RabbitmqConfig)
ConnectionFactory为Connection的制造工厂。连接工厂,通过RabbitTemplate进行转发消息,接受信息。
开启ACK手动确认机制
要求消费者在消费完消息后发送一个回执给RabbitMQ,RabbitMQ收到消息回执(acknowledgment)后才将该消息从Queue中移除;如果RabbitMQ没有收到回执并检测到消费者的RabbitMQ连接断开,则RabbitMQ会将该消息发送给其他消费者进行处理。
@Configuration
public class RabbitmqConfig {
private static final Logger LOGGER = LoggerFactory.getLogger("programLog");
@Value("${spring.rabbitmq.host}")
private String addresses;
@Value("${spring.rabbitmq.port}")
private int port;
@Value("${spring.rabbitmq.username}")
private String username;
@Value("${spring.rabbitmq.password}")
private String password;
@Value("${spring.rabbitmq.virtual-host}")
private String virtualHost;
@Value("${spring.rabbitmq.publisher-confirms}")
private boolean publisherConfirms;
@Value("${spring.rabbitmq.publisher-returns}")
private boolean publisherReturns;
@Autowired
private QueueConfig queueConfig;
/**
* @Description: 连接工厂
*/
@Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory(addresses, port);
connectionFactory.setUsername(username);
connectionFactory.setPassword(password);
connectionFactory.setVirtualHost(virtualHost);
// 如果要进行消息发送确认回调,则这里必须要设置为true
connectionFactory.setPublisherConfirms(publisherConfirms);
// 如果要进行消息发送失败退回,则这里必须要设置为true
connectionFactory.setPublisherReturns(publisherReturns);
return connectionFactory;
}
/**
* 因为要设置回调类,所以应是prototype类型
*/
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public RabbitTemplate rabbitTemplate() {
RabbitTemplate template = new RabbitTemplate(this.connectionFactory());
return template;
}
/**
* 定义消息转换实例转化成 JSON 传输
* 传输实体就可以不用实现序列化
*/
@Bean
public MessageConverter integrationEventMessageConverter() {
return new Jackson2JsonMessageConverter();
}
@Bean
public SimpleMessageListenerContainer messageContainer() {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory());
container.setQueues(queueConfig.getQueueOne(), queueConfig.getQueueTwo(), queueConfig.getQueueThree(), queueConfig.getQueueFour());
//将channel暴露给listener才能手动确认,AcknowledgeMode.MANUAL时必须为ture
container.setExposeListenerChannel(true);
//消费者的最大数量,并发消费的时候需要设置,且>=concurrentConsumers
container.setMaxConcurrentConsumers(10);
//消费者的最小数量
container.setConcurrentConsumers(10);
//在单个请求中处理的消息个数,他应该大于等于事务数量
container.setPrefetchCount(1);
//开启ACK手动确认机制
container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
container.setMessageListener((ChannelAwareMessageListener) (message, channel) -> {
try {
// 通过basic.qos方法设置1,这样RabbitMQ就会使得每个Consumer在同一个时间点最多处理一个Message
channel.basicQos(1);
LOGGER.info("ACK消费端接收到消息:" + message.getMessageProperties() + ":" + new String(message.getBody()));
LOGGER.info("当前使用路由key:" + message.getMessageProperties().getReceivedRoutingKey());
// deliveryTag:消息传送的次数,发布的每一条消息都会获得一个唯一的deliveryTag
// multiple:批量确认标志,如果值为true,则执行批量确认,此deliveryTag之前收到的消息全部进行确认; 如果值为false,则只对当前收到的消息进行确认
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
e.printStackTrace();
if (message.getMessageProperties().getRedelivered()) {
LOGGER.info("消息已重复处理失败,拒绝再次接收...");
// deliveryTag:消息传送的次数,发布的每一条消息都会获得一个唯一的deliveryTag,deliveryTag在channel范围内是唯一的
// multiple:批量确认标志。如果值为true,包含本条消息在内的、所有比该消息deliveryTag值小的消息都被拒绝了(除了已经被 ack 的以外);如果值为false,只拒绝三条消息
// requeue:如果值为true,则重新放入RabbitMQ的发送队列,如果值为false,则通知RabbitMQ销毁这条消息
channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
} else {
LOGGER.info("消息即将再次返回队列处理...");
// deliveryTag:消息传送的次数,发布的每一条消息都会获得一个唯一的deliveryTag,deliveryTag在channel范围内是唯一的
// requeue:如果值为true,则重新放入RabbitMQ的发送队列,如果值为false,则通知RabbitMQ销毁这条消息
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
});
return container;
}
}
第四步:配置生产者发送消息(RabbitSender)
方法实现RabbitTemplate.ConfirmCallback和RabbitTemplate.ReturnCallback的ack回调函数接口
/**
* 生产者
*/
@Service
public class RabbitSender implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
private static Logger logger = LoggerFactory.getLogger("programLog");
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init() {
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
}
/**
* 实现RabbitTemplate.ConfirmCallback和RabbitTemplate.ReturnCallback的回调函数接口
*/
/**
* 实现消息发送到RabbitMQ交换器后接收ack回调,如果消息发送确认失败就进行重试
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
logger.info("消息发送成功,消息ID:{}", correlationData.getId());
} else {
logger.info("消息发送失败,消息ID:{}", correlationData.getId());
}
}
/**
* 实现消息发送到RabbitMQ交换器,但无相应队列与交换器绑定时的回调,发送失败回调
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
logger.error("消息发送失败,replyCode:{}, replyText:{},exchange:{},routingKey:{},消息体:{}", replyCode, replyText, exchange, routingKey, new String(message.getBody()));
}
/**
* convertAndSend 异步发送,消息是否发送成功用ConfirmCallback和ReturnCallback回调函数类确认
* 发送MQ消息
*/
public void sendMessage(String exchangeName, String routingKey, Object message) {
rabbitTemplate.convertAndSend(exchangeName, routingKey, message, new CorrelationData(UUID.randomUUID().toString()));
}
}
第五步:配置消费队列Queue类(QueueConfig)
Queue是RabbitMQ的内部对象,用于存储消息。RabbitMQ中的消息都只能存储在Queue中,生产者生产消息并最终投递到Queue中,消费者可以从Queue中获取消息并消费。多个消费者可以订阅同一个Queue,这时Queue中的消息会被平均分摊给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理。
@Configuration
public class QueueConfig {
/**
* 队列的名字
*/
private static final String QUEUE_ONE = "QUEUE_ONE";
private static final String QUEUE_TWO = "QUEUE_TWO";
private static final String QUEUE_THREE = "QUEUE_THREE";
private static final String QUEUE_FOUR = "QUEUE_FOUR";
/**
* DIRECT_QUEUE 队列名字
* durable="true" 是否持久化 rabbitmq 重启的时候不需要创建新的队列,保证绝大部分情况下RabbitMQ消息不会丢失
* auto-delete 表示消息队列没有在使用时将被自动删除 默认是false
* exclusive 表示该消息队列是否只在当前 connection 生效,默认是false
*/
@Bean(name = QUEUE_ONE)
public Queue getQueueOne() {
return new Queue(QUEUE_ONE, true, false, false);
}
@Bean(name = QUEUE_TWO)
public Queue getQueueTwo() {
return new Queue(QUEUE_TWO, true, false, false);
}
@Bean(name = QUEUE_THREE)
public Queue getQueueThree() {
return new Queue(QUEUE_THREE, true, false, false);
}
@Bean(name = QUEUE_FOUR)
public Queue getQueueFour() {
return new Queue(QUEUE_FOUR, true, false, false);
}
}
第六步:配置消费者MessageListener
确认消息已经消费成功。拒绝当前消息,并把消息返回原队列重新排队消费。
/**
* 消费者
*/
@Component
public class MessageListener {
private static Logger LOGGER = LoggerFactory.getLogger("programLog");
@RabbitListener(queues = "QUEUE_ONE")
@RabbitHandler
public void queueOneMessage(String sendMessage, Channel channel, Message message) throws IOException {
messageBasicAck(sendMessage, channel, message);
}
@RabbitListener(queues = "QUEUE_TWO")
@RabbitHandler
public void queueTwoMessage(String sendMessage, Channel channel, Message message) throws IOException {
messageBasicAck(sendMessage, channel, message);
}
@RabbitListener(queues = "QUEUE_THREE")
@RabbitHandler
public void queueThreeMessage(String sendMessage, Channel channel, Message message) throws IOException {
messageBasicAck(sendMessage, channel, message);
}
@RabbitListener(queues = "QUEUE_FOUR")
@RabbitHandler
public void queueFourMessage(String sendMessage, Channel channel, Message message) throws IOException {
messageBasicAck(sendMessage, channel, message);
}
public void messageBasicAck(String sendMessage, Channel channel, Message message) throws IOException {
try {
Assert.notNull(sendMessage, "sendMessage 消息体不能为NULL");
LOGGER.info("处理MQ消息");
// 通过basic.qos方法设置prefetch_count=1,这样RabbitMQ就会使得每个Consumer在同一个时间点最多处理一个Message
channel.basicQos(1);
LOGGER.info("Consumer {} Message :" + message);
// 确认消息已经消费成功
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (IOException e) {
LOGGER.error("MQ消息处理异常,消息ID:{},消息体:{}", message.getMessageProperties().getCorrelationId(), sendMessage, e);
// 拒绝当前消息,并把消息返回原队列
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
}
使用示例:
RabbitMQ常用的Exchange Type(交换机,消息转发器)有fanout、direct、topic、headers这四种。
fanout类型的Exchange路由它会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中。
绑定规则:
生产者(P)发送到Exchange(X)的所有消息都会路由到图中的两个Queue,并最终被两个消费者(C1与C2)消费。
Fanout交换机绑定队列
创建交换机:所有发送到该Exchange的消息路由到所有与它绑定的Queue中
消息队列绑定到交换机:fanout类型的Exchange会无视binding key,将消息路由到所有绑定到该Exchange的Queue。
@Configuration
public class FanoutExchangeBindingQueue {
private static final String FANOUT_EXCHANGE = "FANOUT_EXCHANGE"; // fanout交换机
/**
* 所有发送到该Exchange的消息路由到所有与它绑定的Queue中
*/
@Bean(name = FANOUT_EXCHANGE)
public FanoutExchange fanoutExchange() {
return new FanoutExchange(FANOUT_EXCHANGE, true, false);
}
/**
* 将fanout队列和交换机进行绑定
* fanout类型的Exchange会无视binding key,将消息路由到所有绑定到该Exchange的Queue
*/
@Bean
public Binding fanoutExchangeBindingQueueOne(@Qualifier("QUEUE_ONE") Queue queueOne, @Qualifier("FANOUT_EXCHANGE") FanoutExchange fanoutExchange) {
return BindingBuilder.bind(queueOne).to(fanoutExchange);
}
@Bean
public Binding fanoutExchangeBindingQueueTwo(@Qualifier("QUEUE_TWO") Queue queueTwo, @Qualifier("FANOUT_EXCHANGE") FanoutExchange fanoutExchange) {
return BindingBuilder.bind(queueTwo).to(fanoutExchange);
}
}
消息生成控制:注意交换机名称
@RequestMapping(value = "/fanoutSend", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
public void fanoutSend() {
String message = "fanout 发送消息";
rabbitSender.sendMessage("FANOUT_EXCHANGE", "", message);
}
direct类型的Exchange路由规则把消息路由到那些binding key与routing key完全匹配的Queue中。
绑定规则:
routingKey=” routingKey.one”发送消息到Exchange,则消息会路由到Queue1和Queue2;如果我们以routingKey=”a”或routingKey=”b”来发送消息,则消息只会路由到Queue2;如果以其他routingKey发送消息,则消息不会路由到这两个Queue中。
声明交换机和rountintkey: routing key,可绑定交换机转发消息到指定Queue
创建交换机:所有发送到该Exchange的消息路由到所有与它绑定的Queue中
消息队列绑定到交换机:把消息路由到那些binding key与routing key完全匹配的Queue中。
@Configuration
public class DirectExchangeBindingQueue {
private static final String DIRECT_EXCHANGE = "DIRECT_EXCHANGE"; // direct交换机
private static final String DIRECT_KEY = "routingKey.one"; // routing key,可绑定交换机转发消息到指定Queue
/**
* 所有发送到该Exchange的消息路由到所有与它绑定的Queue中
*/
@Bean(name = DIRECT_EXCHANGE)
public DirectExchange directExchange() {
return new DirectExchange(DIRECT_EXCHANGE, true, false);
}
/**
* 将direct队列和交换机进行绑定
* 把消息路由到那些binding key与routing key完全匹配的Queue中
*/
@Bean
public Binding directExchangeBindingQueueOne(@Qualifier("QUEUE_ONE") Queue queueOne, @Qualifier("DIRECT_EXCHANGE") DirectExchange directExchange) {
return BindingBuilder.bind(queueOne).to(directExchange).with(DIRECT_KEY);
}
}
消息生成控制:注意交换机名称和rountingkey
@RequestMapping(value = "/directSend", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
public void directSend() {
String message = "direct 发送消息";
rabbitSender.sendMessage("DIRECT_EXCHANGE", "routingKey.one", message);
}
它与direct类型的Exchage相似,也是将消息路由到binding key与routing key相匹配的Queue中,但这里的匹配规则有些不同:
绑定规则:
routingKey=”lazy.brown.topic”的消息会路由到Q2,routingKey=”lazy.pink.rabbit”的消息会路由到Q2;routingKey=”lazy.rabbit”的消息将会被丢弃,因为它们没有匹配任何routingKey。
声明交换机和rountintkey
将队列和topic交换机进行绑定
@Configuration
public class TopicExchangeBindingQueue {
private static final String TOPIC_EXCHANGE = "TOPIC_EXCHANGE"; // topic交换机
// 模糊匹配,“*”匹配一个单词,“#”匹配多个/0个单词
private static final String TOPIC_KEY_ONE = "routingKey.#";
private static final String TOPIC_KEY_TWO = "#.topic";
private static final String TOPIC_KEY_THREE = "#";
/**
* 将消息路由到binding key与routing key相匹配的Queue中,匹配规则与direct有所不同
* binding key存在两种特殊字符“*”与“#”,做模糊匹配,“*”匹配一个单词,“#”匹配多个/0个单词
*/
@Bean(name = TOPIC_EXCHANGE)
public TopicExchange topicExchange() {
return new TopicExchange(TOPIC_EXCHANGE, true, false);
}
/**
* 将topic队列和交换机进行绑定
*/
@Bean
public Binding topicExchangeBindingQueueOne(@Qualifier("QUEUE_ONE") Queue queueOne, @Qualifier("TOPIC_EXCHANGE") TopicExchange topicExchange) {
return BindingBuilder.bind(queueOne).to(topicExchange).with(TOPIC_KEY_ONE);
}
@Bean
public Binding topicExchangeBindingQueueTwo(@Qualifier("QUEUE_TWO") Queue queueTwo , @Qualifier("TOPIC_EXCHANGE") TopicExchange topicExchange) {
return BindingBuilder.bind(queueTwo).to(topicExchange).with(TOPIC_KEY_TWO);
}
@Bean
public Binding topicExchangeBindingQueueThree(@Qualifier("QUEUE_THREE") Queue queueThree, @Qualifier("TOPIC_EXCHANGE") TopicExchange topicExchange) {
return BindingBuilder.bind(queueThree).to(topicExchange).with(TOPIC_KEY_THREE);
}
}
注意rountingkey的赋值:
第一条信息只能到第一消息队列排队消费
第二条信息只能到第二消息队列排队消费
三条信息都可以到第三消息队列排队消费
@RequestMapping(value = "/topicSend", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
public void topicSend() {
String message = "topic 发送消息";
rabbitSender.sendMessage("TOPIC_EXCHANGE", "rountingKey.key", message);
rabbitSender.sendMessage("TOPIC_EXCHANGE", "key.topic", message);
rabbitSender.sendMessage("TOPIC_EXCHANGE", "Key", message);
}
headers类型的Exchange不依赖于routing key与binding key的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配。
创建交换机:对比消息的键值对是否完全匹配Queue与Exchange绑定时指定的键值对。
绑定消息队列
@Configuration
public class HeaderExchangeBindingQueue {
private static final String HEADERS_EXCHANGE = "HEADERS_EXCHANGE"; // header交换机
/**
* 不依赖于routing key与binding key的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配
* 对比消息的键值对是否完全匹配Queue与Exchange绑定时指定的键值对
*/
@Bean(name = HEADERS_EXCHANGE)
public HeadersExchange headersExchange() {
return new HeadersExchange(HEADERS_EXCHANGE, true, false);
}
/**
* 将headers队列和交换机进行绑定
* whereAll:当消息生产者传到Exchange中的headers中的键值对所有都符合(所有Map或所有Key)要求时,才启用该队列
*/
@Bean
public Binding headersExchangeBindingQueueOne(@Qualifier("QUEUE_ONE") Queue queueOne, @Qualifier("HEADERS_EXCHANGE") HeadersExchange headersExchange) {
Map map = new HashMap<>();
map.put("headers1", "value1");
map.put("headers2", "value2");
return BindingBuilder.bind(queueOne).to(headersExchange).whereAll(map).match();
}
/**
* whereAny:只要消息生产者传到Exchange中的headers中的键值对有至少一个(至少一个Map或至少一个Key)符合要求时,就启用该队列
*/
@Bean
public Binding headersExchangeBindingQueueTwo(@Qualifier("QUEUE_TWO") Queue queueTwo, @Qualifier("HEADERS_EXCHANGE") HeadersExchange headersExchange) {
Map map = new HashMap<>();
map.put("headers1", "value1");
map.put("headers2", "value2");
return BindingBuilder.bind(queueTwo).to(headersExchange).whereAny(map).match();
}
}
生产消息控制
@RequestMapping(value = "/headersSend", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
public void headersSend() {
String msg = "headers 发送消息";
MessageProperties properties = new MessageProperties();
properties.setHeader("headers1", "value1");
properties.setHeader("headers2", "value2");
Message message = new Message(msg.getBytes(), properties);
rabbitSender.sendMessage("HEADERS_EXCHANGE", "", message);
}
参考资料:SO JSON在线解析《我为什么要选择RabbitMQ ,RabbitMQ简介,各种MQ选型对比》
版权所属:SO JSON在线解析
原文地址:https://www.sojson.com/blog/48.html
转载时必须以链接形式注明原始出处及本声明。