RabbitMQ消息中间件
一、消息中间件介绍
-
我们用java来举例子, 打个比方 我们客户端发送一个下单请求给订单系统(order)订单系统发送了 一个请求给我们的库存系统告诉他需要更改库存了, 我已经下单了, 这里, 每一个请求我们都可以看作一条消息,但是 我们客户端需要等待订单系统告诉我这条消息的处理结果(我到底有没有下单成功) 但是 订单系统不需要知道库存系统这条消息的处理情况 因为无论你库存有没有改动成功, 我订单还是下了, 因为是先下完了订单(下成功了) 才去更改库存, 库存如果更改出BUG了 那是库存系统的问题, 这个BUG不会影响订单系统。如果这里你能理解的话, 那么我们就能发现 我们用户发送的这条消息(下订单),是需要同步的(我需要知道结果), 订单发送给库存的消息,是可以异步的(我不想知道你库存到底改了没,我只是通知你我这边成功下了一个订单)。
那么如果我们还按原来的方式去实现这个需求的话, 那么结果会是这样:
在实际使用的时候,我们也可以使用线程池处理我们的订单入库存的步骤,但是线程池会有线程池阻塞等缺点。
我们可以采用如下设计方式实现异步存储入库操作。
二、RabbitMQ介绍以及AMQP协议
-
AMQP 协议中的基本概念:
• Broker: 接收和分发消息的应用,我们在介绍消息中间件的时候所说的消息系统就是Message Broker。
• Virtual host: 出于多租户和安全因素设计的,把AMQP的基本组件划分到一个虚拟的分组中,类似于网络中的namespace概念。当多个不同的用户使用同一个RabbitMQ server提供的服务时,可以划分出多个vhost,每个用户在自己的vhost创建exchange/queue等。
• Connection: publisher/consumer和broker之间的TCP连接。断开连接的操作只会在client端进行,Broker不会断开连接,除非出现网络故障或broker服务出现问题。
• Channel: 如果每一次访问RabbitMQ都建立一个Connection,在消息量大的时候建立TCP Connection的开销将是巨大的,效率也较低。Channel是在connection内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的channel进行通讯,AMQP method包含了channel id帮助客户端和message broker识别channel,所以channel之间是完全隔离的。Channel作为轻量级的Connection极大减少了操作系统建立TCP connection的开销。
• Exchange: message到达broker的第一站,根据分发规则,匹配查询表中的routing key,分发消息到queue中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)。
• Queue: 消息最终被送到这里等待consumer取走。一个message可以被同时拷贝到多个queue中。
• Binding: exchange和queue之间的虚拟连接,binding中可以包含routing key。Binding信息被保存到exchange中的查询表中,用于message的分发依据。
Exchange的类型
direct :
这种类型的交换机的路由规则是根据一个routingKey的标识,交换机通过一个routingKey与队列绑定 ,在生产者生产消息的时候 指定一个routingKey 当绑定的队列的routingKey 与生产者发送的一样 那么交换机会吧这个消息发送给对应的队列。
fanout:
这种类型的交换机路由规则很简单,只要与他绑定了的队列, 他就会吧消息发送给对应队列(与routingKey没关系)
topic:
这种类型的交换机路由规则也是和routingKey有关 只不过 topic他可以根据:星,#( 星号代表过滤一单词,#代表过滤后面所有单词, 用.隔开)来识别routingKey 我打个比方 假设 我绑定的routingKey 有队列A和B A的routingKey是:星.user B的routingKey是: #.user
那么我生产一条消息routingKey 为: error.user 那么此时 2个队列都能接受到, 如果改为 topic.error.user 那么这时候 只有B能接受到了
headers:
这个类型的交换机很少用到,他的路由规则 与routingKey无关 而是通过判断header参数来识别的, 基本上没有应用场景,因为上面的三种类型已经能应付了。
-
主流MQ对比
三、Springboot整合RabbitMQ
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
配置连接、交换机、队列
@Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new
CachingConnectionFactory("localhost",5672); //我这里直接在构造方法传入了
// connectionFactory.setHost();
// connectionFactory.setPort(); connectionFactory.setUsername("guest"); connectionFactory.setPassword("guest"); connectionFactory.setVirtualHost("testhost");
//是否开启消息确认机制
//connectionFactory.setPublisherConfirms(true);
return connectionFactory;
}
创建交换机并绑定队列
@Bean
public DirectExchange defaultExchange() {
return new DirectExchange("directExchange");
}
@Bean
public Queue queue() { //名字 是否持久化
return new Queue("testQueue", true);
}
@Bean
public Binding binding() {
//绑定一个队列 to: 绑定到哪个交换机上面 with:绑定的路由建(routingKey)
return BindingBuilder.bind(queue()).to(defaultExchange()).with("direct.key");
}
发送消息模板
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
//注意 这个ConnectionFactory 是使用javaconfig方式配置连接的时候才需要传入的 如果是yml配置的连接的话是不需要的
RabbitTemplate template = new RabbitTemplate(connectionFactory);
return template;
}
@Component
public class TestSend {
@Autowired
RabbitTemplate rabbitTemplate;
public void testSend() {
//至于为什么调用这个API 后面会解释
//参数介绍: 交换机名字,路由建, 消息内容
rabbitTemplate.convertAndSend("directExchange", "direct.key", "hello");
}
}
接收消息
@Component
public class TestListener {
@RabbitListener(queues = "testQueue")
public void get(String message) throws Exception{
System.out.println(message);
}
}
四、可靠性保证
-
生产者
不确定消息有没有发送到Broker,可以采用失败回调,发送方确认模式
@Bean public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) { RabbitTemplate template = new RabbitTemplate(connectionFactory); //开启mandatory模式(开启失败回调) template.setMandatory(true); //指定失败回调接口的实现类 template.setReturnCallback(new MyReturnCallback()); return template; } public class MyReturnCallback implements RabbitTemplate.ReturnCallback { @Override public void returnedMessage(Message message, int replyCode, String replyText,String exchange, String routingKey) { } } System.out.println(message); System.out.println(replyCode); System.out.println(replyText); System.out.println(exchange); System.out.println(routingKey); } }
当指定的交换机不能吧消息路由到队列时(没有指定路由建或者指定的RouteKey没有绑定对应的队列或者压根就没有绑定队列都会失败) 消息就会发送失败
//开启发送确认模式 connectionFactory.setPublisherConfirms(true); //在rabbitmqTemplate配置一下 template.setConfirmCallback(new MyConfirmCallback()); //失败回调 public class MyConfirmCallback implements RabbitTemplate.ConfirmCallback{ @Override //业务id,是否发送成功,原因 public void confirm(CorrelationData correlationData, boolean ack, String cause) { System.out.println(correlationData); System.out.println(ack); System.out.println(cause); } }
为了进一步保证我们交换机的可靠性,我们可以定义一个备用交换机进行绑定主交换机,当主交换机不可用时,会自动进入备用交换机,一般情况备用交换机都是采用fanout类型。
@Bean public DirectExchange defaultExchange() { Map<String, Object> map = new HashMap<>(); //备用交换机名称 map.put("alternate-exchange", "name"); return new DirectExchange("directExchangeTest4", false, false, map); }
-
消费者
手动确认
为什么要确认消费? 默认情况下消费者在拿到rabbitmq的消息时 已经自动确认这条消息已经消费了, 讲白话就 是rabbitmq的队列里就会删除这条消息了, 但是 我们实际开发中 难免会遇到这种情况, 比如说 拿到这条消息发现我处理不了比如说参数不对, 又比如说 我当前这个系统出问题了, 暂时不能处理这个消息, 但是这个消息已 经被你消费掉了 rabbitmq的队列里也删除掉了, 你自己这边又处理不了, 那么这个消息就被遗弃了。 这种情 况在实际开发中是不合理的, rabbitmq提供了解决这个问题的方案, 也就是我们上面所说的confirm模式 只是我 们刚刚讲的是发送方的 这次我们来讲消费方的。
配置手动确认:
@Bean public SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory(ConnectionFactory connectionFactory){ SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory = new SimpleRabbitListenerContainerFactory(); //这个connectionFactory就是我们自己配置的连接工厂直接注入进来 simpleRabbitListenerContainerFactory.setConnectionFactory(connectionFactory); //这边设置消息确认方式由自动确认变为手动确认 simp
消费者手动确认:
@Component public class TestListener { //containerFactory:指定我们刚刚配置的容器 @RabbitListener(queues = "testQueue",containerFactory = "simpleRabbitListenerContainerFactory") public void getMessage(Message message, Channel channel) throws Exception{ System.out.println(new String(message.getBody(),"UTF-8")); System.out.println(message.getBody()); //这里我们调用了一个下单方法 如果下单成功了 那么这条消息就可以确认被消费了,即实际业务处理类 boolean f =placeAnOrder(); if (f){ //传入这条消息的标识, 这个标识由rabbitmq来维护 我们只需要从message中拿出来就可以 //第二个boolean参数指定是不是批量处理的 什么是批量处理我们待会儿会讲到 channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); }else { //当然 如果这个订单处理失败了 我们也需要告诉rabbitmq 告诉他这条消息处理失败了 可以退回 也可以遗弃 要注意的是 无论这条消息成功与否 一定要通知 就算失败了 如果不通知的话 rabbitmq端会显示这条 消息一直处于未确认状态,那么这条消息就会一直堆积在rabbitmq端 除非与rabbitmq断开连接 那么他就会把这条 消息重新发给别人 所以 一定要记得通知! //前两个参数 和上面的意义一样, 最后一个参数 就是这条消息是返回到原队列 还是这条消息作废 就是不退回了。 channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,true); //其实 这个API也可以去告诉rabbitmq这条消息失败了 与basicNack不同之处 就是 他不能批量 处理消息结果 只能处理单条消息 其实basicNack作为basicReject的扩展开发出来的 //channel.basicReject(message.getMessageProperties().getDeliveryTag(),true); } }
消息预取
rabbitmq 默认 他会最快 以轮询的机制吧队列所有的消息发送给所有客户端
那么 这种机制会有什么问题呢, 对于Rabbitmq来讲 这样子能最快速的使自己不会囤积消息而对性能造成影响, 但是 对于我们整个系统来讲, 这种机制会带来很多问题, 比如说 我一个队列有2个人同时在消费,而且他们处理 能力不同, 我打个最简单的比方 有100个订单消息需要处理(消费) 现在有消费者A 和消费者B , 消费者A消费一 条消息的速度是 10ms 消费者B 消费一条消息的速度是15ms ( 当然 这里只是打比方) 那么 rabbitmq 会默认给 消费者A B 一人50条消息让他们消费 但是 消费者A 他500ms 就可以消费完所有的消息 并且处于空闲状态 而 消费 者B需要750ms 才能消费完 如果从性能上来考虑的话 这100条消息消费完的时间一共是750ms(因为2个人同时在 消费) 但是如果 在消费者A消费完的时候 能把这个空闲的性能用来和B一起消费剩下的信息的话, 那么这处理速 度就会快非常多。
配置消息预取:
//设置消息预取的数量1-2500,500平均值 simpleRabbitListenerContainerFactory.setPrefetchCount(1);
死信交换机
在创建队列的时候 可以给这个队列附带一个交换机, 那么这个队列作废的消息就会被重 新发到附带的交换机,然后让这个交换机重新路由这条消息
@Bean public Queue queue() { Map<String,Object> map = new HashMap<>(); //设置消息的过期时间 单位毫秒 map.put("x-message-ttl",10000); //设置附带的死信交换机 map.put("x-dead-letter-exchange","exchange.dlx"); //指定重定向的路由建 消息作废之后可以决定需不需要更改他的路由建 如果需要 就在这里指定 map.put("x-dead-letter-routing-key","dead.order"); return new Queue("testQueue", true,false,false,map); }
大致效果如图