五、RabbitMq高级特性
在消息的使用过程当中存在一些问题。比如发送消息我们如何确保消息的投递的可靠性呢?如何保证消费消息可靠性呢?如果不能保证在某些情况下可能会出现损失。比如当我们发送消息的时候和接收消息的时候能否根据消息的特性来实现某一些业务场景的模拟呢?订单30分钟过期等等,系统通信的确认等等。
1、生产者可靠性消息投递
作为消息发送方希望杜绝任何消息丢失或者投递失败场景(服务器挂了,网络抖动)。RabbitMQ 为我们提供了两种方式用来控制消息的投递可靠性模式,mq提供了如下两种模式:
- confirm模式
生产者发送消息到交换机的时机 - return模式
交换机转发消息给queue的时机
1.生产者发送消息到交换机
2.交换机根据routingkey 转发消息给队列
3.消费者监控队列,获取队列中信息
4.消费成功删除队列中的消息
- 消息从 product 到 exchange 则会返回一个 confirmCallback 。
- 消息从 exchange 到 queue 投递失败则会返回一个 returnCallback 。
1.1、confirmCallback 实现
spring:
rabbitmq:
host: localhost
port: 5672
username: admin
password: admin
virtual-host: /pay
publisher-confirms: true #开启confirm模式
创建回调函数
@Component
public class MyConfirmCallback implements RabbitTemplate.ConfirmCallback {
/**
* Confirmation callback.
*
* @param correlationData correlation data for the callback.
* @param ack true for ack, false for nack
* @param cause An optional cause, for nack, when available, otherwise null.
*/
/**
*
* @param correlationData 消息信息
* @param ack 确认标识:true,MQ服务器exchange表示已经确认收到消息 false 表示没有收到消息
* @param cause 如果没有收到消息,则指定为MQ服务器exchange消息没有收到的原因,如果已经收到则指定为null
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if(ack){
System.out.println("发送消息到交换机成功,"+cause);
}else{
System.out.println("发送消息到交换机失败,原因是:"+cause);
}
}
}
controller:
@RestController
@RequestMapping("/order")
public class RabbitMQController {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private RabbitTemplate.ConfirmCallback confirmCallback;
//@Autowired
//private Exchange exchange;
@GetMapping("/add")
public String addOrder(){
System.out.println("下单成功...");
rabbitTemplate.setConfirmCallback(confirmCallback);
//发送消息
rabbitTemplate.convertAndSend("topic_exchangex", "order.insert","消息主体 order.insert");
return "success";
}
}
1.2、returncallback代码实现
1)配置yml开启returncallback
spring:
rabbitmq:
host: localhost
port: 5672
username: admin
password: admin
virtual-host: /pay
publisher-confirms: true
publisher-returns: true
returncallback代码
@Component
public class MyReturnCallBack implements RabbitTemplate.ReturnCallback {
/**
*
* @param message 消息信息
* @param replyCode 退回的状态码
* @param replyText 退回的信息
* @param exchange 交换机
* @param routingKey 路由key
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("退回的消息是:"+new String(message.getBody()));
System.out.println("退回的replyCode是:"+replyCode);
System.out.println("退回的replyText是:"+replyText);
System.out.println("退回的exchange是:"+exchange);
System.out.println("退回的routingKey是:"+routingKey);
}
}
controller:
@RestController
@RequestMapping("/order")
public class RabbitMQController {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private RabbitTemplate.ConfirmCallback confirmCallback;
@Autowired
private RabbitTemplate.ReturnCallback returnCallback;
//@Autowired
//private Exchange exchange;
@GetMapping("/add")
public String addOrder(){
System.out.println("下单成功...");
rabbitTemplate.setConfirmCallback(confirmCallback);
rabbitTemplate.setReturnCallback(returnCallback);
//发送消息
rabbitTemplate.convertAndSend("topic_exchange", "1order.insert1","消息主体 order.insert");
return "success";
}
}
confirm模式用于在消息发送到交换机时机使用,return模式用于在消息被交换机路由到队列中发送错误时使用。
但是一般情况下我们使用confirm即可,因为路由key 由开发人员指定,一般不会出现错误。
2、消费者确认机制(ACK)
在消费方也有可能出现问题,比如没有接受消息,比如接受到消息之后,在代码执行过程中出现了异常,这种情况下我们需要额外的处理,那么就需要手动进行确认签收消息。rabbtimq给我们提供了一个机制:ACK机制。
ACK机制:有三种方式
- 自动确认 acknowledge=“auto”
- 手动确认 acknowledge=“manual”
解释:
其中自动确认是指:
当消息一旦被Consumer接收到,则自动确认收到,并将相应 message 从 RabbitMQ 的消息缓存中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。
其中手动确认方式是指:
则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()等方法,让其按照业务功能进行处理,比如:重新发送,比如拒绝签收进入死信队列等等。
ack代码实现 :
1)创建普通消息监听器
@Component
@RabbitListener(queues = "springboot_topic_queue1")
public class MessageListener {
//普通消息监听器
// @RabbitHandler
// public void myRabbitMQListener(String msg){
// System.out.println("消费者收到:" + msg);
// }
//正常则签收,不正常则进行丢弃处理。
/**
*
* @param message 消息本身(消息的内容 和包括消息包含一些别的数据:比如:交换机 消息的序号)
* @param channel 通道
* @param msg 消息本身 只是 消息内容
*/
@RabbitHandler //@RabbitHandler 根据不同的数据调用不同的方法 String pojo
public void myRabbitMQListener(Message message, Channel channel, String msg){
System.out.println("消费者收到:" + msg);
try {
//处理本地业务
System.out.println("处理本地业务开始======start======");
Thread.sleep(2000);
int i=1/0;
System.out.println("处理本地业务结束======end======");
//签收消息
//参数1 指定消息的序号
//参数2 指定是否批量的进行签收 true 表示批量处理签收
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
e.printStackTrace();
//如果出现异常,则拒绝消息 可以重回队列 也可以丢弃 可以根据业务场景来
try {
//参数1 指定消息的序号
//参数2 指定是否批量的进行签收 true 表示批量处理拒绝签收
//参数3 指定 是否重回队列 true 表示消息重回队列 false 表示 丢弃消息
channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
//参数1 指定消息的序号
//参数2 指定 是否重回队列 true 表示消息重回队列 false 表示 丢弃消息
//channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e1) {
e1.printStackTrace();
}
}
}
}
========================
//如果出现异常,则拒绝消息 可以重回队列 也可以丢弃 可以根据业务场景来
try {
if (message.getMessageProperties().getRedelivered()) {
//消息已经重新投递,不需要再次投递
System.out.println("已经投递一次了");
} else {
//第三个参数:设置是否重回队列
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
//channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e1) {
e1.printStackTrace();
}
- yml设置为手动确认模式
spring:
rabbitmq:
host: localhost
port: 5672
username: admin
password: admin
virtual-host: /pay
publisher-confirms: true
publisher-returns: true
listener:
simple:
acknowledge-mode: manual
ACK确认的方式
ack确认方式有几种:
- 签收消息
- 拒绝消息 批量处理/单个处理
以上可以根据不同的业务进行不同的选择。需要注意的是,如果拒绝签收,下一次启动又会自动的进行消费。
第一种:签收
channel.basicAck()
第二种:拒绝签收 批量处理
channel.basicNack()
第三种:拒绝签收 不批量处理
channel.basicReject()
3、消费端限流
如果并发量大的情况下,生产方不停的发送消息,可能处理不了那么多消息,此时消息在队列中堆积很多,当消费端启动,瞬间就会涌入很多消息,消费端有可能瞬间垮掉,这时我们可以在消费端进行限流操作,每秒钟放行多少个消息。这样就可以进行并发量的控制,减轻系统的负载,提供系统的可用性,这种效果往往可以在秒杀和抢购中进行使用。在rabbitmq中也有限流的一些配置。
代码实现
spring:
rabbitmq:
host: localhost
port: 5672
username: admin
password: admin
virtual-host: /pay
publisher-confirms: true
publisher-returns: true
listener:
simple:
acknowledge-mode: manual
prefetch: 1 # 设置每一个消费端 最多处理的未确认的消息的数量 默认是250个。
Listener:
@Component
@RabbitListener(queues = "springboot_topic_queue1",concurrency = "2")
public class MessageListener {
//普通消息监听器
// @RabbitHandler
// public void myRabbitMQListener(String msg){
// System.out.println("消费者收到:" + msg);
// }
@RabbitHandler
public void myRabbitMQListener(Message message, Channel channel, String msg) throws IOException {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者收到:" + msg);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
}
}
tomcat7 并发200
4、TTL
TTL 全称 Time To Live(存活时间/过期时间)。当消息到达存活时间后,还没有被消费,会被自动清除。
RabbitMQ设置过期时间有两种:
- 针对某一个队列设置过期时间 ;从消息入队列开始计算,超过了队列的超时时间配置,那么消息就会自动清除。
- 针对某一个特定的消息设置过期时间;队列中的消息设置过期时间之后,如果这个消息没有被消费则被清除。
需要注意一点的是:
针对某一个特定的消息设置过期时间时,即使消息过期也不会马上被丢弃, 因为消息是否过期是在即将投递到消费者之前被判定的。如果某一个消息A 设置过期时间5秒,消息B在队头,消息B没有设置过期时间,B此时过了已经5秒钟了还没被消费。注意,此时A消息并不会被删除,因为它并没有在队头。
一般在工作当中,单独使用TTL的情况较少。我们后面会讲到延时队列。在这里有用处。
创建过期队列
@Configuration
public class TtlConfig {
//创建过期队列
@Bean
public Queue createqueuettl1(){
//设置队列过期时间为10000 10S钟
return QueueBuilder.durable("queue_demo02").withArgument("x-message-ttl",10000).build();
}
//创建交换机
@Bean
public DirectExchange createExchangettl(){
return new DirectExchange("exchange_direct_demo02");
}
//创建绑定
@Bean
public Binding createBindingttl(){
return BindingBuilder.bind(createqueuettl1()).to(createExchangettl()).with("item.ttl");
}
}
5、死信队列
死信队列:当消息成为Dead message后,可以被重新发送到另一个交换机,这个交换机就是Dead Letter Exchange(死信交换机 简写:DLX)。
如下图的过程:
成为死信的三种条件:
- 队列消息长度到达限制;
- 消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;
- 原队列存在消息过期设置,消息到达超时时间未被消费;
5.1、死信的处理过程
DLX也是一个正常的Exchange,和一般的Exchange没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性。
当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange上去,进而被路由到另一个队列。
可以监听这个队列中的消息做相应的处理。
5.2、死信队列的设置
死信队列也是一个正常的exchange.只需要设置一些参数即可。
给队列设置参数: x-dead-letter-exchange 和 x-dead-letter-routing-key。
队列长度和超时:
@Configuration
public class DlxConfig {
//创建队列 1 这个接收转发过来的死信 queue1
@Bean
public Queue createqueuetdlq(){
return QueueBuilder.durable("queue_demo03").build();
}
//创建队列 2 queue2 用来接收生产者发送过来的消息 然后要过期 变成死信 转发给了queue1
@Bean
public Queue createqueuetdelq2(){
return QueueBuilder
.durable("queue_demo03_deq")
.withArgument("x-max-length",1)//设置队列的长度
.withArgument("x-message-ttl",10000)//设置队列的消息过期时间 10S
.withArgument("x-dead-letter-exchange","exchange_direct_demo03_dlx")//设置死信交换机名称
.withArgument("x-dead-letter-routing-key","item.dlx")//设置死信路由key item.dlx 就是routingkey
.build();
}
//创建死信交换机
@Bean
public DirectExchange createExchangedel(){
return new DirectExchange("exchange_direct_demo03_dlx");
}
// queue1 绑定给 死信交换机 routingkey 和 队列转发消息时指定的死信routingkey 要一致
@Bean
public Binding createBindingdel(){
return BindingBuilder.bind(createqueuetdlq()).to(createExchangedel()).with("item.dlx");
}
}
拒绝签收进入死信
@Component
@RabbitListener(queues = "queue_demo03_deq")
public class DLxListner {
@RabbitHandler
public void lis(Message message, Channel channel, String msg){
System.out.println("消息是:"+msg);
try {
System.out.println("我拒绝签收");
channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
} catch (IOException e) {
e.printStackTrace();
}
}
}
6、延迟队列
订单30分钟支付
延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。在rabbitmq中,并没有延迟队列概念,但是我们可以使用ttl 和死信队列的方式进行达到延迟的效果。这种需求往往在某些应用场景中出现。当然还可以使用插件。
1.生产者产生一个消息发送到queue1
2.queue1中的消息过期则转发到queue2
3.消费者在queue2中获取消息进行消费
配置类:
@Configuration
public class DelayConfig {
//正常的队列 接收死信队列转移过来的消息
@Bean
public Queue createQueue2(){
return QueueBuilder.durable("queue_order_queue2").build();
}
//死信队列 --->将来消息发送到这里 这里不设置过期时间,我们应该在发送消息时设置某一个消息(某一个用户下单的)的过期时间
@Bean
public Queue createQueue1(){
return QueueBuilder
.durable("queue_order_queue1")
.withArgument("x-dead-letter-exchange","exchange_order_delay")//设置死信交换机
.withArgument("x-dead-letter-routing-key","item.order")//设置死信路由key
.build();
}
//创建交换机
@Bean
public DirectExchange createOrderExchangeDelay(){
return new DirectExchange("exchange_order_delay");
}
//创建绑定 将正常队列绑定到死信交换机上
@Bean
public Binding createBindingDelay(){
return BindingBuilder.bind(createQueue2()).to(createOrderExchangeDelay()).with("item.order");
}
}
controller:
/**
* 发送下单
*
* @return
*/
@RequestMapping("/send6")
public String send6() {
//发送消息到死信队列 可以使用默认的交换机 指定ourtingkey为死信队列名即可
System.out.println("用户下单成功,10秒钟之后如果没有支付,则过期,回滚订单");
System.out.println("时间:"+new Date());
rabbitTemplate.convertAndSend("queue_order_queue1", (Object) "哈哈我要检查你是否有支付", new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration("10000");//设置该消息的过期时间
return message;
}
});
return "用户下单成功,10秒钟之后如果没有支付,则过期,回滚订单";
}
设置监听类
@Component
@RabbitListener(queues = "queue_order_queue2")
public class OrderListener {
@RabbitHandler
public void orderhandler(Message message, Channel channel, String msg) {
System.out.println("获取到消息:" + msg + ":时间为:" + new Date());
try {
System.out.println("模拟检查开始=====start");
Thread.sleep(1000);
System.out.println("模拟检查结束=====end");
System.out.println("用户没付款,检查没通过,进入回滚库存处理");
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
e.printStackTrace();
}
}
}
六、rabbitmq应用的问题
1、幂等性
幂等性指一次和多次请求某一个资源,对于资源本身应该具有同样的结果。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。在MQ中指,消费多条相同的消息,得到与消费该消息一次相同的结果。
以转账为例:
1.发送消息
2.消息内容包含了id 和 版本和 金额
3.消费者接收到消息,则根据ID 和版本执行sql语句,
update account set money=money-?,version=version+1 where id=? and version=?
4.如果消费第二次,那么同一个消息内容是修改不成功的。
七、RabbitMQ集群
1、rabbitmq集群通信原理
RabbitMQ这款消息队列中间件产品本身是基于Erlang编写,Erlang语言天生具备分布式特性(通过同步Erlang集群各节点的magic cookie来实现)。因此,RabbitMQ天然支持Clustering。集群是保证可靠性的一种方式,同时可以通过水平扩展以达到增加消息吞吐量能力的目的,这里只需要保证erlang_cookie的参数一致集群即可通信。
rabbimtq集群包括两种:普通集群和镜像集群。
普通集群有缺点也有优点,镜像集群有缺点也有优点。
大致上,
如果是普通集群:那么每一个节点的数据,存储了另外一个节点的元数据,当需要使用消息时候,从另外一台节点 拉取数据,这样性能很高,但是性能瓶颈发生在单台服务器上。而且宕机有可能出现消息丢失。
如果镜像集群,那么在使用时候,每个节点都相互通信互为备份,数据共享。那么这样一来使用消息时候,就直接获取,不再零时获取,但是缺点就是消耗很大的性能和带宽