消費者手動確認
一般情況下我們是不會使用消費者的自動確認模式的,通常我們會手動確認消息是否消費。
我們使用channel.basicAck或者channel.basicNack 來進行消息的確認
代碼示例
public void consumerDirect(){
Connection connection = Common.getConnection();
try {
Channel channel = connection.createChannel();
channel.basicConsume(DIRECT_QUEUE_1,new QueueingConsumer(channel){
@SneakyThrows
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, StandardCharsets.UTF_8);
Thread.sleep(20);
//模擬消費失敗
if((Math.random()*100)>90){
retryMsg(channel,properties,envelope,msg);
}else{
System.out.println("消費者確認 " + Thread.currentThread().getId() + " 收到消息" + msg + " 來源交換器:" + envelope.getExchange());
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
使用死信交換器
死信交換器是 RabbitMQ 對 AMQP 規範的一個擴展,往往用在對問題消息的診斷上(主要針對消費者),還有延時隊列的功能。
消息變成死信一般是以下三種情況:
- 消息被拒絕,並且設置 requeue 參數爲 false
- 消息過期(默認情況下 Rabbit
中的消息不過期,但是可以設置隊列的過期時間和消息的過期時間以達到消息過期的效果) - 隊列達到最大長度(一般當設置了最大隊列長度或大小並達到最大值時)
死信交換器仍然只是一個普通的交換器,創建時並沒有特別要求和操作。在創建隊列的時候,聲明該交換器將用作保存被拒絕的消息即可,相關的
參數是 x-dead-letter-exchange。
代碼示例
//聲明死信隊列
channel.queueDeclare(FANOUT_DLX_QUEUE,true,false,false,null);
// 聲明死信交換器,當隊列,就會使用這個交換器
channel.exchangeDeclare( FANOUT_DLX_EXCHANGE,"fanout",true,false,null);
//隊列綁定,匹配所有的路右鍵
channel.queueBind(FANOUT_DLX_QUEUE,FANOUT_DLX_EXCHANGE,"#");
Map<String,Object> argsMap = new HashMap<String,Object>();
argsMap.put("x-dead-letter-exchange",FANOUT_DLX_EXCHANGE);
//這裏把死信交換器綁定了過來,當消息在一個隊列裏面變成死信(過期,超出內存,被拒絕不能重新入隊)的時候就會進入死信隊列
channel.queueDeclare(DIRECT_QUEUE_1,true,false,false,argsMap);
消費者中的注意事項
QOS預取
在消費者進行消費的時候,應當儘量使用QOS預取的模式,默認是沒有設置的,也就是無限大,這樣會導致消費者一下子接受的數據太多,導致內存溢出
使用示例
public void consumerConfirm(){
Connection connection = Common.getConnection();
try {
Channel channel = connection.createChannel();
//設置QOS預取數量爲200,默認是不限制,限制QoS可以減少消費者的壓力,避免消息過多直接內存溢出
//測試場景:在設置堆內存爲5M的情況下,如果生成這連續發送1000條消息,消費者如果不設置QoS會導致內存溢出
channel.basicQos(100);
channel.basicConsume(DIRECT_CONSUMER_CONFIRM_QUEUE_1,new QueueingConsumer(channel){
@SneakyThrows
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, StandardCharsets.UTF_8);
Thread.sleep(20);
//模擬消費失敗
if((Math.random()*100)>90){
retryMsg(channel,properties,envelope,msg);
}else{
System.out.println("消費者確認 " + Thread.currentThread().getId() + " 收到消息" + msg + " 來源交換器:" + envelope.getExchange());
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
保證消息不能重複消費
我們應當儘量在消費者端保證消息的冪等性來避免消息的重複消費
private void dealMsg(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body, Channel channel,String consumer) throws IOException, InterruptedException {
String msg = new String(body, StandardCharsets.UTF_8);
if(isExist(msg)){
System.out.println(consumer+" 重複消費消息:"+msg);
channel.basicAck(envelope.getDeliveryTag(),true);
}else {
System.out.println(consumer + " " + Thread.currentThread().getId() + " 收到消息" + msg + " 來源交換器:" + envelope.getExchange());
//延遲處理
Thread.sleep(500);
channel.basicAck(envelope.getDeliveryTag(), true);
EXIST_SET.add(msg);
}
}
/**
*
* 冪等校驗,其方式多樣,可以根據數據庫的唯一主鍵,也可以利用緩存等等方式
* @param msg
* @return
*/
private boolean isExist(String msg){
return EXIST_SET.contains(msg);
}
失敗重試機制
當消息確認失敗的時候,應該進行一定次數的重試,而不是直接丟棄或者重新入隊
/**
* @Title:
* @MethodName:
* @param
* @Return
* @Exception
* @Description:
* 消費者確認
* 1、使用basicAck或basicNack手動確認
* 2、使用QoS預取
* 3、進行消息的重試。三次失敗的進入死信隊列
* @author: jenkin
* @date: 2020-04-11 11:05
*/
public void consumerConfirm(){
Connection connection = Common.getConnection();
try {
Channel channel = connection.createChannel();
//設置QOS預取數量爲200,默認是不限制,限制QoS可以減少消費者的壓力,避免消息過多直接內存溢出
//測試場景:在設置堆內存爲5M的情況下,如果生成這連續發送1000條消息,消費者如果不設置QoS會導致內存溢出
channel.basicQos(100);
channel.basicConsume(DIRECT_CONSUMER_CONFIRM_QUEUE_1,new QueueingConsumer(channel){
@SneakyThrows
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, StandardCharsets.UTF_8);
Thread.sleep(20);
//模擬消費失敗
if((Math.random()*100)>90){
retryMsg(channel,properties,envelope,msg);
}else{
System.out.println("消費者確認 " + Thread.currentThread().getId() + " 收到消息" + msg + " 來源交換器:" + envelope.getExchange());
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 失敗重試
* @param channel
* @param properties
* @param envelope
* @param msg
* @throws IOException
*/
private void retryMsg(Channel channel, AMQP.BasicProperties properties, Envelope envelope, String msg) throws IOException {
Map<String, Object> headers = properties.getHeaders();
Object retryTimes = headers.get("retryTimes");
int times = retryTimes==null?0:Integer.parseInt(String.valueOf(retryTimes));
System.out.println("重試次數 "+times);
Map<String, Object> propertiesMap = new HashMap<>();
propertiesMap.put("retryTimes", times +1);
AMQP.BasicProperties persistentTextPlain = MessageProperties.PERSISTENT_TEXT_PLAIN.builder().headers(propertiesMap).build();
System.out.println("消費者Nack " + Thread.currentThread().getId() + " 收到消息" + msg + " 來源交換器:" + envelope.getExchange());
if(!(times>=3)) {
channel.basicAck(envelope.getDeliveryTag(), false);
channel.basicPublish(DIRECT_EXCHANGE, DIRECT_CONSUMER_CONFIRM_QUEUE_1, true, persistentTextPlain, ("重試: " + msg).getBytes());
}else{
channel.basicNack(envelope.getDeliveryTag(), false,false);
}
}