系统之间的消息传递
消息在多个系统之间传递有两种方式,1、直接发起远程调用 2、MQ机制传递消息
MQ的优点
1、应用解耦
2、异步调用(只需要发消息,即刻返回)
3、削峰填谷(高并发峰值消息放到队列中,等待消费者分批消费 )
MQ的缺点
1、系统可用性降低 ,需要保证MQ的可用性
2、系统的复杂性提高
3、一致性问题,3个机器,有一个机器消费失败
RabbitMQ的工作图解以及原理
组成部分
Broker:消息队列服务进程,此进程包括两个部分:Exchange和Queue
Exchange:消息队列交换机,按一定的规则将消息路由转发到某个队列,对消息进行过虑。
Queue:消息队列,存储消息的队列,消息到达队列并转发给指定的
Producer:消息生产者,即生产方客户端,生产方客户端将消息发送
Consumer:消息消费者,即消费方客户端,接收MQ转发的消息。Channel:通过通道与broker产生连接
交换机Exchange绑定队列
- 直连交换机 Direct 把消息交给符合指定rotingKey的队列
- 广播交换机 Fanout 将消息交给所有绑定到交换机的队列
- 主题交换机 Topic 把消息交给符合routing pattern(路由模式)的队列,通配符匹配
在实际使用过程中,我们使用主题交换机,因为主题交换机可以改造成为直连交换机和广播交换机。
主题交换机的匹配规则
通配符规则:
#:用来表示任意数量(零个或多个)单词
*:用来表示一个单词 (必须出现的)
队列Q1 绑定键为 *.TT.* 队列Q2绑定键为 TT.#
如果一条消息携带的路由键为 A.TT.B,那么队列Q1将会收到;
如果一条消息携带的路由键为TT.AA.BB,那么队列Q2将会收到;
当一个队列的绑定键为 "#"(井号) 的时候,这个队列将会无视消息的路由键,接收所有的消息。也就是实现了广播交换机的行为
当 * (星号) 和 # (井号) 这两个特殊字符都未在绑定键中出现的时候,此时主题交换机就拥有的直连交换机的行为
RabbitMQ的确认机制
发送方confirmCallback(确认发送到交换机)、returnCallback(交换机投送到队列)
package com.pjb.config;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
/**
* RabbitMQ配置类
*
**/
@Configuration
public class RabbitMqConfig
{
public static final String QUEUE_NAME = "queue_name"; //队列名称
public static final String EXCHANGE_NAME = "exchange_name"; //交换器名称
public static final String ROUTING_KEY = "routing_key"; //路由键
@Bean
public RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory)
{
RabbitTemplate rabbitTemplate = new RabbitTemplate();
rabbitTemplate.setConnectionFactory(connectionFactory);
//设置开启Mandatory,才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数
rabbitTemplate.setMandatory(true);
//确认消息送到交换机(Exchange)回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback()
{
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause)
{
System.out.println("\n确认消息送到交换机(Exchange)结果:");
System.out.println("相关数据:" + correlationData);
System.out.println("是否成功:" + ack);
System.out.println("错误原因:" + cause);
}
});
//确认消息送到队列(Queue)回调
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback()
{
@Override
public void returnedMessage(ReturnedMessage returnedMessage)
{
System.out.println("\n确认消息送到队列(Queue)结果:");
System.out.println("发生消息:" + returnedMessage.getMessage());
System.out.println("回应码:" + returnedMessage.getReplyCode());
System.out.println("回应信息:" + returnedMessage.getReplyText());
System.out.println("交换机:" + returnedMessage.getExchange());
System.out.println("路由键:" + returnedMessage.getRoutingKey());
}
});
return rabbitTemplate;
}
/**
* 队列
*/
@Bean
public Queue queue()
{
/**
* 创建队列,参数说明:
* String name:队列名称。
* boolean durable:设置是否持久化,默认是 false。durable 设置为 true 表示持久化,反之是非持久化。
* 持久化的队列会存盘,在服务器重启的时候不会丢失相关信息。
* boolean exclusive:设置是否排他,默认也是 false。为 true 则设置队列为排他。
* boolean autoDelete:设置是否自动删除,为 true 则设置队列为自动删除,
* 当没有生产者或者消费者使用此队列,该队列会自动删除。
* Map<String, Object> arguments:设置队列的其他一些参数。
*/
return new Queue(QUEUE_NAME, true, false, false, null);
}
/**
* Direct交换器
*/
@Bean
public DirectExchange exchange()
{
/**
* 创建交换器,参数说明:
* String name:交换器名称
* boolean durable:设置是否持久化,默认是 false。durable 设置为 true 表示持久化,反之是非持久化。
* 持久化可以将交换器存盘,在服务器重启的时候不会丢失相关信息。
* boolean autoDelete:设置是否自动删除,为 true 则设置队列为自动删除,
*/
return new DirectExchange(EXCHANGE_NAME, true, false);
}
/**
* 绑定
*/
@Bean
Binding binding(DirectExchange exchange, Queue queue)
{
//将队列和交换机绑定, 并设置用于匹配键:routingKey
return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY);
}
}
消费方ACK确认
消费者确认发生在监听队列的消费者处理业务失败,如:发生了异常,不符合要求的数据等,这些场景我们就需要手动处理,比如重新发送或者丢弃。
RabbitMQ 消息确认机制(ACK)默认是自动确认的,自动确认会在消息发送给消费者后立即确认,但存在丢失消息的可能,如果消费端消费逻辑抛出异常,假如你用回滚了也只是保证了数据的一致性,但是消息还是丢了,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息。
确认模式如下:
- AcknowledgeMode.NONE:自动确认。
- AcknowledgeMode.AUTO:根据情况确认。
- AcknowledgeMode.MANUAL:手动确认。
消费者收到消息后,手动调用 Basic.Ack 或 Basic.Nack 或 Basic.Reject 后,RabbitMQ 收到这些消息后,才认为本次投递完成。
- Basic.Ack 命令:用于确认当前消息。
- Basic.Nack 命令:用于否定当前消息(注意:这是AMQP 0-9-1的RabbitMQ扩展) 。
- Basic.Reject 命令:用于拒绝当前消息。
确认机制方法
void basicAck(long deliveryTag, boolean multiple) throws IOException;
void basicNack(long deliveryTag, boolean multiple, boolean requeue) throws IOException;
void basicReject(long deliveryTag, boolean requeue) throws IOException;
long deliveryTag:唯一标识 ID,当一个消费者向 RabbitMQ 注册后,会建立起一个 Channel ,RabbitMQ 会用 basic.deliver 方法向消费者推送消息,这个方法携带了一个 delivery tag, 它代表了 RabbitMQ 向该 Channel 投递的这条消息的唯一标识 ID,是一个单调递增的正整数,delivery tag 的范围仅限于 Channel。
boolean multiple:是否批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息。
boolean requeue:如果 requeue 参数设置为 true,则 RabbitMQ 会重新将这条消息存入队列,以便发送给下一个订阅的消费者; 如果 requeue 参数设置为 false,则 RabbitMQ 立即会还把消息从队列中移除,而不会把它发送给新的消费者。
package com.pjb.receiver;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
/**
* 接收者
**/
@Component
public class Receiver implements ChannelAwareMessageListener
{
@Override
public void onMessage(Message message, Channel channel) throws Exception
{
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try
{
System.out.println("接收消息: " + new String(message.getBody(), "UTF-8"));
/**
* 确认消息,参数说明:
* long deliveryTag:唯一标识 ID。
* boolean multiple:是否批处理,当该参数为 true 时,
* 则可以一次性确认 deliveryTag 小于等于传入值的所有消息。
*/
channel.basicAck(deliveryTag, true);
/**
* 否定消息,参数说明:
* long deliveryTag:唯一标识 ID。
* boolean multiple:是否批处理,当该参数为 true 时,
* 则可以一次性确认 deliveryTag 小于等于传入值的所有消息。
* boolean requeue:如果 requeue 参数设置为 true,
* 则 RabbitMQ 会重新将这条消息存入队列,以便发送给下一个订阅的消费者;
* 如果 requeue 参数设置为 false,则 RabbitMQ 立即会还把消息从队列中移除,
* 而不会把它发送给新的消费者。
*/
//channel.basicNack(deliveryTag, true, false);
}
catch (Exception e)
{
e.printStackTrace();
/**
* 拒绝消息,参数说明:
* long deliveryTag:唯一标识 ID。
* boolean requeue:如果 requeue 参数设置为 true,
* 则 RabbitMQ 会重新将这条消息存入队列,以便发送给下一个订阅的消费者;
* 如果 requeue 参数设置为 false,则 RabbitMQ 立即会还把消息从队列中移除,
* 而不会把它发送给新的消费者。
*/
channel.basicReject(deliveryTag, true);
}
}
}
死信队列和延时队列
成为死信的条件
- 消费者使用basic.reject或basic.nack声明消费失败,并且消息的requeue参数参数设置为false
- 消息时一个过期消息,超时无人消费
- 要投递的队列消息堆积满了,最早的消息可能成为死信
如果该队列配置了dead-letter-exchange属性,指定了一个交换机,name队列中的死信就会投递到这个交换机中,而这个交换机称为死信交换机(Dead Letter Exchange,简称DLX)
队列发现消费者无法处理消息时,比如失败次数太多,队列就会根据配置的死信交换机路由到死信队列
如何绑定死信交换机
- 给队列指定dead-letter-exchange属性,指定一个交换机
- 给队列设置dead-letter-routing-key属性,设置死信交换机与死信队列的RoutingKey
TTL
TTL(Time-To-Live),如果一个队列中的消息TTL结束仍未消费,则会变成死信,ttl超时分为两种情况
- 消息所在的队列设置了存活时间
- 消息本身设置了存活时间
// 队列上设置了超时
@Bean
public Queue ttlQueue(){
return QueueBuilder
.durable("ttl.queue")
.ttl(10000)
.deadLetterExchange("dl.direct")
.deadLetterRoutingKey("dl")
.build();
}
@Test
public void testTTLMessage() {
// 准备消息 设置超时
Message message = MessageBuilder
.withBody("hello, ttl messsage".getBytes(StandardCharsets.UTF_8))
// 不设置默认是持久化
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)
.setExpiration("5000")
.build();
// 2.发送消息
rabbitTemplate.convertAndSend("ttl.direct", "ttl", message);
// 3.记录日志
log.info("消息已经成功发送!");
}
延时队列
延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费
例如:
- 用户下单,如果用户在15分钟内未支付,则自动取消
- 延时发短信
实现方式:
1、定时器实现
2、延迟队列 发送MQ延迟队列,等待30min后在消费消息,判断支付状态
很可惜 RabbitMQ并未提供延迟队列功能,但是可以使用TTL+死信队列组合实现延迟队列效果,设置队列过期时间为30min,成为死信,然后队列发送消息给死信交换机,死信交换机绑定的队列再消费就可以实现这样的效果,消费者一定监听死信队列
消息幂等性保证
其实就是保证同一个消息不被消费者重复消费两次。当消费者消费完消息之后,通常会发送一个ack应答确认信息给生产者,但是这中间有可能因为网络中断等原因,导致生产者未能收到确认消息,由此这条消息将会被 重复发送给其他消费者进行消费,实际上这条消息已经被消费过了,这就是重复消费的问题。
例如 ,减库存操作,第一次由于网络原因,返回结果错误,实际库存已经减掉,在次重新发送减库存,就多减掉啦
消费者端实现幂等性,意味着我们的消息永远不会消费多次,即使我们收到了多条一样的消息。通常有两种方式来避免消费重复消费
- 消息全局ID或者写个唯一标识(如时间戳、UUID等) :每次消费消息之前根据消息id去判断该消息是否已消费过,如果已经消费过,则不处理这条消息,否则正常消费消息,并且进行入库操作。(消息全局ID作为数据库表的主键,防止重复)
- 利用Redis的setnx 命令分布式锁:给消息分配一个全局ID,只要消费过该消息,将 < id,message>以K-V键值对形式写入redis,消费者开始消费前,先去redis中查询有没消费记录即可。
// 发送方
@Component
public class MessageIdempotentProducer {
private static final String EXCHANGE_NAME = "message_idempotent_exchange";
private static final String ROUTE_KEY = "message.insert";
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 发送消息
*/
public void sendMessage() {
//创建消费对象,并指定全局唯一ID(这里使用UUID,也可以根据业务规则生成,只要保证全局唯一即可)
MessageProperties messageProperties = new MessageProperties();
messageProperties.setMessageId(UUID.randomUUID().toString());
messageProperties.setContentType("text/plain");
messageProperties.setContentEncoding("utf-8");
Message message = new Message("hello,message idempotent!".getBytes(), messageProperties);
rabbitTemplate.convertAndSend(EXCHANGE_NAME, ROUTE_KEY, message);
}
}
// 接收方
@Component
public class MessageIdempotentConsumer {
private static final Logger logger = LoggerFactory.getLogger(MessageIdempotentConsumer.class);
@Autowired
private MessageIdempotentRepository messageIdempotentRepository;
@RabbitHandler
//org.springframework.amqp.AmqpException: No method found for class [B 这个异常,并且还无限循环抛出这个异常。
//注意@RabbitListener位置,笔者踩坑,无限报上面的错,还有另外一种解决方案: 配置转换器
@RabbitListener(queues = "message_idempotent_queue")
@Transactional
public void handler(Message message, Channel channel) throws IOException {
/**
* 发送消息之前,根据消息全局ID去数据库中查询该条消息是否已经消费过,如果已经消费过,则不再进行消费。
*/
// 获取消息Id
String messageId = message.getMessageProperties().getMessageId();
if (StringUtils.isBlank(messageId)) {
logger.info("获取消费ID为空!");
return;
}
MessageIdempotent messageIdempotent = messageIdempotentRepository.findOne(messageId);
//如果找不到,则进行消费此消息
if (null == messageIdempotent) {
//获取消费内容
String msg = new String(message.getBody(), StandardCharsets.UTF_8);
logger.info("-----获取生产者消息-----------------" + "messageId:" + messageId + ",消息内容:" + msg);
//手动ACK
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
//存入到表中,标识该消息已消费
MessageIdempotent idempotent = new MessageIdempotent();
idempotent.setMessageId(messageId);
idempotent.setMessageContent(msg);
messageIdempotentRepository.save(idempotent);
} else {
//如果根据消息ID(作为主键)查询出有已经消费过的消息,那么则不进行消费;
logger.error("该消息已消费,无须重复消费!");
}
}
}
springBoot 整合 RabbitMQ
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
配置文件
server:
port: 8021
spring:
application:
name: rabbitmq-provider
#配置rabbitMq 服务器
rabbitmq:
host: 127.0.0.1
port: 5672
username: root
password: root
#虚拟host 可以不设置,使用server默认host
virtual-host: JCcccHost
配置类
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TopicRabbitConfig {
//绑定键
public final static String man = "topic.man";
public final static String woman = "topic.woman";
@Bean
public Queue firstQueue() {
return new Queue(TopicRabbitConfig.man);
}
@Bean
public Queue secondQueue() {
return new Queue(TopicRabbitConfig.woman);
}
@Bean
TopicExchange exchange() {
return new TopicExchange("topicExchange");
}
//将firstQueue和topicExchange绑定,而且绑定的键值为topic.man
//这样只要是消息携带的路由键是topic.man,才会分发到该队列
@Bean
Binding bindingExchangeMessage() {
return BindingBuilder.bind(firstQueue()).to(exchange()).with(man);
}
//将secondQueue和topicExchange绑定,而且绑定的键值为用上通配路由键规则topic.#
// 这样只要是消息携带的路由键是以topic.开头,都会分发到该队列
@Bean
Binding bindingExchangeMessage2() {
return BindingBuilder.bind(secondQueue()).to(exchange()).with("topic.#");
}
}
发送消息
@GetMapping("/sendTopicMessage1")
public String sendTopicMessage1() {
String messageId = String.valueOf(UUID.randomUUID());
String messageData = "message: M A N ";
String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
Map<String, Object> manMap = new HashMap<>();
manMap.put("messageId", messageId);
manMap.put("messageData", messageData);
manMap.put("createTime", createTime);
rabbitTemplate.convertAndSend("topicExchange", "topic.man", manMap);
return "ok";
}
@GetMapping("/sendTopicMessage2")
public String sendTopicMessage2() {
String messageId = String.valueOf(UUID.randomUUID());
String messageData = "message: woman is all ";
String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
Map<String, Object> womanMap = new HashMap<>();
womanMap.put("messageId", messageId);
womanMap.put("messageData", messageData);
womanMap.put("createTime", createTime);
rabbitTemplate.convertAndSend("topicExchange", "topic.woman", womanMap);
return "ok";
}
}
接收消息
@Component
@RabbitListener(queues = "topic.man")
public class TopicManReceiver {
@RabbitHandler
public void process(Map testMessage) {
System.out.println("TopicManReceiver消费者收到消息 : " + testMessage.toString());
}
}
@Component
@RabbitListener(queues = "topic.woman")
public class TopicTotalReceiver {
@RabbitHandler
public void process(Map testMessage) {
System.out.println("TopicTotalReceiver消费者收到消息 : " + testMessage.toString());
}
}