一、前言
前幾天我研究了關於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方式確保消息隊列中的消息都能在消費者中成功消費。那麼,消息轉發器和消息隊列之間呢?消息生產者和消息轉發器之間呢?
這些問題我們留在下一篇博客中再分別討論吧。
當然,差點忘了一個小問題。
我們思考一個問題,如果消息隊列對應的消費者只有一個,並且那個消費者炸了,會出現什麼問題呢??