RabbitMq學習——Springboot整合rabbitmq之手動消息確認(ACK)

一、前言

前幾天我研究了關於springboot整合簡單消息隊列,實現springboot推送消息至隊列中,消費者成功消費。同時也加了消息轉發器,對消息轉發器各種類型的配置等做了總結。
但是,主要還有一點,我一直存在疑問:如何確保消息成功被消費者消費?


說到這裏,我相信很多人會說使用ack啊,關閉隊列自動刪除啊什麼的。主要是道理大家都懂,我要實際的代碼,網上找了半天,和我設想的有很大差異,還是自己做研究總結吧。

二、準備

本次寫案例,就按照最簡單的方式,direct方式進行配置吧,實際流程如下所示:
在這裏插入圖片描述

1、消息轉發器類型:direct直連方式。
2、消息隊列:暫時採取公平分發方式。
3、實現流程:消息生產者生產的消息發送至隊列中,由兩個消費者獲取並消費,消費完成後,清楚消息隊列中的消息。

所以我們接下來先寫配置和demo。

2.1、依賴引入

再一般的springboot 2.1.4項目中,添加一個pom依賴。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

2.2、連接yml的配置

我們這邊暫時只有一個rabbitmq,所以連接操作,基本rabbitmq的信息配置問題直接再yml中編寫就可以了。

spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: xiangjiao
    password: bunana
    virtual-host: /xiangjiao
    publisher-confirms: true   #開啓發送確認
    publisher-returns: true  #開啓發送失敗回退
    
    #開啓ack
    listener:
      direct:
        acknowledge-mode: manual
      simple:
        acknowledge-mode: manual #採取手動應答
        #concurrency: 1 # 指定最小的消費者數量
        #max-concurrency: 1 #指定最大的消費者數量
        retry:
          enabled: true # 是否支持重試

2.3、config注入配置

我們根據圖示
在這裏插入圖片描述
知道我們必須配置以下東西:
1、一個消息轉發器,我們取名directExchangeTx
2、一個消息隊列,取名directQueueTx,並將其綁定至指定的消息轉發器上。

所以我們的配置文件需要這麼寫:

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 直連交換機,發送指定隊列信息,但這個隊列後有兩個消費者同時進行消費
 * @author 7651
 *
 */
@Configuration
public class DirectExchangeTxQueueConfig {
	
	@Bean(name="getDirectExchangeTx")
	public DirectExchange getDirectExchangeTx(){
		return new DirectExchange("directExchangeTx", true, false);
	}
	
	@Bean(name="getQueueTx")
	public Queue getQueueTx(){
		return new Queue("directQueueTx", true, false, false);
	}
	
	@Bean
	public Binding getDirectExchangeQueueTx(
			@Qualifier(value="getDirectExchangeTx") DirectExchange getDirectExchangeTx,
			@Qualifier(value="getQueueTx") Queue getQueueTx){
		return BindingBuilder.bind(getQueueTx).to(getDirectExchangeTx).with("directQueueTxRoutingKey");
	}
}

2.4、消費者的配置

有了隊列和消息轉發器,消息當然需要去消費啊,所以我們接下來配置消息消費者。
在這裏插入圖片描述
從圖中,我們看出,我們需要配置兩個消息消費者,同時監聽一個隊列,所以我們的配置類爲:
消費者一:

import java.io.IOException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import com.rabbitmq.client.Channel;

@Component
@RabbitListener(queues="directQueueTx")
public class Consumer1 {
	@RabbitHandler
	public void process(String msg,Channel channel, Message message) throws IOException {
		//拿到消息延遲消費
		try {
			Thread.sleep(1000*1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		
		try {
			/**
			 * 確認一條消息:<br>
			 * channel.basicAck(deliveryTag, false); <br>
			 * deliveryTag:該消息的index <br>
			 * multiple:是否批量.true:將一次性ack所有小於deliveryTag的消息 <br>
			 */
			channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
			System.out.println("get msg1 success msg = "+msg);
			
		} catch (Exception e) {
			//消費者處理出了問題,需要告訴隊列信息消費失敗
			/**
			 * 拒絕確認消息:<br>
			 * channel.basicNack(long deliveryTag, boolean multiple, boolean requeue) ; <br>
			 * deliveryTag:該消息的index<br>
			 * multiple:是否批量.true:將一次性拒絕所有小於deliveryTag的消息。<br>
			 * requeue:被拒絕的是否重新入隊列 <br>
			 */
			channel.basicNack(message.getMessageProperties().getDeliveryTag(),
					false, true);
			System.err.println("get msg1 failed msg = "+msg);
			
			/**
			 * 拒絕一條消息:<br>
			 * channel.basicReject(long deliveryTag, boolean requeue);<br>
			 * deliveryTag:該消息的index<br>
			 * requeue:被拒絕的是否重新入隊列 
			 */
			//channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
		}
	}
}

消息消費者二:

import java.io.IOException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import com.rabbitmq.client.Channel;

@Component
@RabbitListener(queues="directQueueTx")
public class Consumer2 {
	@RabbitHandler
	public void process(String msg,Channel channel, Message message) throws IOException {
		//拿到消息延遲消費
		try {
			Thread.sleep(1000*3);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		
		try {
			channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
			System.out.println("get msg2 success msg = "+msg);
			
		} catch (Exception e) {
			//消費者處理出了問題,需要告訴隊列信息消費失敗
			channel.basicNack(message.getMessageProperties().getDeliveryTag(),
					false, true);
			System.err.println("get msg2 failed msg = "+msg);
		}
	}
}

兩個消費者之間唯一的區別在於兩者獲取消息後,延遲時間不一致。

2.5、消息生產者

有了消息消費者,我們需要有一個方式提供消息並將消息推送到消息隊列中。

public interface IMessageServcie {
	public void sendMessage(String exchange,String routingKey,Object msg);
}
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.core.RabbitTemplate.ConfirmCallback;
import org.springframework.amqp.rabbit.core.RabbitTemplate.ReturnCallback;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import cn.linkpower.service.IMessageServcie;

@Component
public class MessageServiceImpl implements IMessageServcie,ConfirmCallback,ReturnCallback {
	
	private static Logger log = LoggerFactory.getLogger(MessageServiceImpl.class);
	
	@Autowired
	private RabbitTemplate rabbitTemplate;
	
	@Override
	public void sendMessage(String exchange,String routingKey,Object msg) {
		//消息發送失敗返回到隊列中, yml需要配置 publisher-returns: true
		rabbitTemplate.setMandatory(true);
		//消息消費者確認收到消息後,手動ack回執
		rabbitTemplate.setConfirmCallback(this);
		rabbitTemplate.setReturnCallback(this);
		//發送消息
		rabbitTemplate.convertAndSend(exchange,routingKey,msg);
	}

	@Override
	public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
		log.info("---- returnedMessage ----replyCode="+replyCode+" replyText="+replyText+" ");
	}
	
	@Override
	public void confirm(CorrelationData correlationData, boolean ack, String cause) {
		log.info("---- confirm ----ack="+ack+"  cause="+String.valueOf(cause));
		log.info("correlationData -->"+correlationData.toString());
		if(ack){
			log.info("---- confirm ----ack==true  cause="+cause);
		}else{
			log.info("---- confirm ----ack==false  cause="+cause);
		}
	}

}

除了定義好了消息發送的工具服務接口外,我們還需要一個類,實現請求時產生消息,所以我們寫一個controller。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import cn.linkpower.service.IMessageServcie;

@Controller
public class SendMessageTx {
	
	@Autowired
	private IMessageServcie messageServiceImpl;
	
	@RequestMapping("/sendMoreMsgTx")
	@ResponseBody
	public String sendMoreMsgTx(){
		//發送10條消息
		for (int i = 0; i < 10; i++) {
			String msg = "msg"+i;
			System.out.println("發送消息  msg:"+msg);
			messageServiceImpl.sendMessage("directExchangeTx", "directQueueTxRoutingKey", msg);
			//每兩秒發送一次
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		return "send ok";
	}
}

運行springboot項目,訪問指定的url,是可以觀察到消息產生和消費的。

有些人會問,寫到這裏就夠了嗎,你這和之前博客相比,和沒寫一樣啊,都是教我們如何配置,如何生產消息,如何消費消息。

所以接下來的纔是重點了,我們一起研究一個事,當我們配置的消費者二出現消費消息時,出問題了,你如何能夠保證像之前那樣,消費者一處理剩下的消息?

三、ack配置和測試

3.1、模擬消費者二出問題

我們發送的消息格式都是 msg1、msg2、…

所以,我們不妨這麼想,當我消費者二拿到的消息msg後面的數字大於3,表示我不要了。

import java.io.IOException;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import com.rabbitmq.client.Channel;

@Component
@RabbitListener(queues="directQueueTx")
public class Consumer2 {
	@RabbitHandler
	public void process(String msg,Channel channel, Message message) throws IOException {
		//拿到消息延遲消費
		try {
			Thread.sleep(1000*3);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		
		
		try {
			if(!isNull(msg)){
				String numstr = msg.substring(3);
				Integer num = Integer.parseInt(numstr);
				if(num >= 3){
					channel.basicNack(message.getMessageProperties().getDeliveryTag(),
							false, true);
					System.out.println("get msg2 basicNack msg = "+msg);
				}else{
					channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
					System.out.println("get msg2 basicAck msg = "+msg);
				}
			}
		} catch (Exception e) {
			//消費者處理出了問題,需要告訴隊列信息消費失敗
			channel.basicNack(message.getMessageProperties().getDeliveryTag(),
					false, true);
			System.err.println("get msg2 failed msg = "+msg);
		}
	}
	
	public static boolean isNull(Object obj){
		return obj == null || obj == ""||obj == "null";
	}
}

再次請求接口,我們統計日誌信息打印發現:
在這裏插入圖片描述
發現:

當我們對消息者二進行限制大於等於3時,不接受消息隊列傳遞來的消息時,消息隊列會隨機重發那條消息,直至消息發送至完好的消費者一時,纔會把消息消費掉。

四、分析幾個回執方法

4.1、確認消息

channel.basicAck(long deliveryTag, boolean multiple);

屬性 含義
deliveryTag 消息的隨機標籤信息
multiple 是否批量;true表示一次性的將小於deliveryTag的值進行ack

我們一般使用下列方式:

channel.basicAck(
message.getMessageProperties().getDeliveryTag(), 
false);

4.2、拒絕消息

channel.basicNack(long deliveryTag, boolean multiple, boolean requeue) ;

屬性 含義
deliveryTag 消息的隨機標籤信息
multiple 是否批量;true表示一次性的將小於deliveryTag的值進行ack
requeue 被拒絕的消息是否重新入隊列

我們接下來還是修改消費者二,將這個方法最後個參數更改爲false,看現象是什麼?

import java.io.IOException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import com.rabbitmq.client.Channel;

@Component
@RabbitListener(queues="directQueueTx")
public class Consumer2 {
	@RabbitHandler
	public void process(String msg,Channel channel, Message message) throws IOException {
		//拿到消息延遲消費
		try {
			Thread.sleep(1000*3);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		
		
		try {
			if(!isNull(msg)){
				String numstr = msg.substring(3);
				Integer num = Integer.parseInt(numstr);
				if(num >= 3){
					channel.basicNack(message.getMessageProperties().getDeliveryTag(),
							false, false);
					System.out.println("get msg2 basicNack msg = "+msg);
				}else{
					channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
					System.out.println("get msg2 basicAck msg = "+msg);
				}
			}
		} catch (Exception e) {
			//消費者處理出了問題,需要告訴隊列信息消費失敗
			channel.basicNack(message.getMessageProperties().getDeliveryTag(),
					false, true);
			System.err.println("get msg2 failed msg = "+msg);
		}
	}
	
	public static boolean isNull(Object obj){
		return obj == null || obj == ""||obj == "null";
	}
}

在這裏插入圖片描述
重啓項目,重新請求測試接口。
在這裏插入圖片描述
發現,當出現設置參數爲false時,也就是如下所示的設置時:

channel.basicNack(
	message.getMessageProperties().getDeliveryTag(),
	false, 
	false);

如果此時消費者二出了問題,這條消息不會重新迴歸隊列中重新發送,會丟失這條數據。
並且再消息隊列中不會保存:
在這裏插入圖片描述

4.3、拒絕消息

channel.basicReject(long deliveryTag, boolean requeue);

屬性 含義
deliveryTag 消息的隨機標籤信息
requeue 被拒絕的消息是否重新入隊列

這個和上面的channel.basicNack又有什麼不同呢?我們還是修改消費者二實驗下。
在這裏插入圖片描述
請求測試接口,查看日誌信息。
在這裏插入圖片描述
發現,此時的日誌信息配置

channel.basicReject(
message.getMessageProperties().getDeliveryTag(),
 true);

channel.basicNack(
message.getMessageProperties().getDeliveryTag(),
false, true);

實現的效果是一樣的,都是將信息拒絕接收,由於設置的requeue爲true,所以都會將拒絕的消息重新入隊列中,重新進行消息分配並消費。

五、總結

這一篇博客,我們總結了相關的配置,三個確認(或回執)信息的方法,並區別了他們的各項屬性,也知道了當消息再一個消費者中處理失敗了,如何不丟失消息重新進行消息的分配消費問題。

但是這個只是隊列和消費者之間的消息確認機制,使用手動ACK方式確保消息隊列中的消息都能在消費者中成功消費。那麼,消息轉發器和消息隊列之間呢?消息生產者和消息轉發器之間呢?

這些問題我們留在下一篇博客中再分別討論吧。

當然,差點忘了一個小問題。
我們思考一個問題,如果消息隊列對應的消費者只有一個,並且那個消費者炸了,會出現什麼問題呢??

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章