RabbitMQ筆記十五:消息確認之一(Publisher Confirms)

問題

企業中使用消息中間件面臨的常見問題:
1.消息莫名其妙的沒了,也不知道什么情況,有丟消息的問題。
2.發送者沒法確認是否發送成功,消費者處理失敗也無法反饋。

消息可靠性的二種方式
1.事務,利用AMQP協議的一部分,發送消息前設置channel為tx模式(channel.txSelect();),如果txCommit提交成功了,則消息一定到達了broker了,如果在txCommit執行之前broker異常崩潰或者由于其他原因拋出異常,這個時候我們便可以捕獲異常通過txRollback回滾事務了。(大大得削弱消息中間件的性能)
2.消息確認(publish confirms),設置管道為confirmSelect模式(channel.confirmSelect();)

publisher confirms,consumer Acknowledgements

生產者與broker之間的消息確認稱為public confirms,public confirms機制用于解決生產者與Rabbitmq服務器之間消息可靠傳輸,它在消息服務器持久化消息后通知消息生產者發送成功。

發送確認(publisher confirms)

RabbitMQ java Client實現發送確認

deliveryTag(投遞的標識),當Channel設置成confirm模式時,發布的每一條消息都會獲得一個唯一的deliveryTag,任何channel上發布的第一條消息的deliveryTag為1,此后的每一條消息都會加1,deliveryTag在channel范圍內是唯一的。

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;

public class Send {

    static Long id = 0L;

    static TreeSet<Long> tags = new TreeSet<>();

    public static Long send(Channel channel,byte[] bytes) throws Exception{
        AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder().deliveryMode(2).
                contentEncoding("UTF-8").build();
        channel.basicPublish("zhihao.direct.exchange","zhihao.miao.order",properties,bytes);
        return ++id;
    }


    public static void main(String[] args) throws Exception{
        ConnectionFactory connectionFactory = new ConnectionFactory();

        connectionFactory.setUri("amqp://zhihao.miao:123456@192.168.1.131:5672");

        Connection connection = connectionFactory.newConnection();

        Channel channel = connection.createChannel();

        //是當前的channel處于確認模式
        channel.confirmSelect();

        //使當前的channel處于事務模式,與上面的使channel處于確認模式使互斥的
        //channel.txSelect();

        /**
         * deliveryTag 消息id
         * multiple 是否批量
         *      如果是true,就意味著,小于等于deliveryTag的消息都處理成功了
         *      如果是false,只是成功了deliveryTag這一條消息
         */
        channel.addConfirmListener(new ConfirmListener() {
            //消息發送成功并且在broker落地,deliveryTag是唯一標志符,在channek上發布的消息的deliveryTag都會比之前加1
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                System.out.println("=========deliveryTag==========");
                System.out.println("deliveryTag: "+deliveryTag);
                System.out.println("multiple: "+multiple);
                //處理成功發送的消息
                if(multiple){
                    //批量操作
                    for(Long _id:new TreeSet<>(tags.headSet(deliveryTag+1))){
                        tags.remove(_id);
                    }
                }else{
                    //單個確認
                    tags.remove(deliveryTag);
                }

                System.out.println("未處理的消息: "+tags);
            }

            /**
             * deliveryTag 消息id
             * multiple 是否批量
             *      如果是true,就意味著,小于等于deliveryTag的消息都處理失敗了
             *      如果是false,只是失敗了deliveryTag這一條消息
             */
            //消息發送失敗或者落地失敗
            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                System.out.println("===========handleNack===========");
                System.out.println("deliveryTag: "+deliveryTag);
                System.out.println("multiple: "+multiple);
            }
        });

        /**
         * 當Channel設置成confirm模式時,發布的每一條消息都會獲得一個唯一的deliveryTag
         * deliveryTag在basicPublish執行的時候加1
         */


        Long id = send(channel,"你的外賣已經送達".getBytes());
        tags.add(id);
        //channel.waitForConfirms();

        id =send(channel,"你的外賣已經送達".getBytes());
        tags.add(id);
        //channel.waitForConfirms();

        id = send(channel,"呵呵,不接電話".getBytes());
        tags.add(id);
        //channel.waitForConfirms();  

        TimeUnit.SECONDS.sleep(10);

        channel.close();
        connection.close();
    }
}

channel.waitForConfirms():表示等待已經發送給broker的消息act或者nack之后才會繼續執行。
channel.waitForConfirmsOrDie():表示等待已經發送給broker的消息act或者nack之后才會繼續執行,如果有任何一個消息觸發了nack則拋出IOException。

總結
生產者與broker之間的消息可靠性保證的基本思路就是

  • 當消息發送到broker的時候,會執行監聽的回調函數,其中deliveryTag是消息id(在同一個channel中這個數值是遞增的,而multiple表示是否批量確認消息。
  • 在生產端要維護一個消息發送的表,消息發送的時候記錄消息id,在消息成功落地broker磁盤并且進行回調確認(ack)的時候,根據本地消息表和回調確認的消息id進行對比,這樣可以確保生產端的消息表中的沒有進行回調確認(或者回調確認時網絡問題)的消息進行補救式的重發,當然不可避免的就會在消息端可能會造成消息的重復消息。針對消費端重復消息,在消費端進行冪等處理。(丟消息和重復消息是不可避免的二個極端,比起丟消息,重復消息還有補救措施,而消息丟失就真的丟失了。

Spring AMQP實現實現發送確認

示列
定義消息內容

public class Order {

    private String orderId;

    private String createTime;

    private double price;

    public String getOrderId() {
        return orderId;
    }

    public void setOrderId(String orderId) {
        this.orderId = orderId;
    }

    public String getCreateTime() {
        return createTime;
    }

    public void setCreateTime(String createTime) {
        this.createTime = createTime;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }
}

配置項:

import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MQConfig {

    @Bean
    public ConnectionFactory connectionFactory(){
        CachingConnectionFactory factory = new CachingConnectionFactory();
        factory.setUri("amqp://zhihao.miao:123456@192.168.1.131:5672");
        factory.setPublisherConfirms(true);
        return factory;
    }

    @Bean
    public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory){
        RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);
        return rabbitAdmin;
    }

    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {

            /**
             * @param correlationData 唯一標識,有了這個唯一標識,我們就知道可以確認(失敗)哪一條消息了
             * @param ack
             * @param cause
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                System.out.println("=====消息進行消費了======");
                if(ack){
                    System.out.println("消息id為: "+correlationData+"的消息,已經被ack成功");
                }else{
                    System.out.println("消息id為: "+correlationData+"的消息,消息nack,失敗原因是:"+cause);
                }
            }
        });
        return rabbitTemplate;
    }

}

啟動應用類:

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;

import java.time.LocalDateTime;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@ComponentScan
public class Application {

    public static Order createOrder(){
        Order order = new Order();
        order.setOrderId(UUID.randomUUID().toString());
        order.setCreateTime(LocalDateTime.now().toString());
        order.setPrice(100L);
        return order;
    }

    public static void saveOrder(Order order){
        //入庫操作
        System.out.println("入庫操作");
    }

    public static void main(String[] args) throws Exception{
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Application.class);

        RabbitTemplate rabbitTemplate = context.getBean(RabbitTemplate.class);

        Order order  = createOrder();

        saveOrder(order);

        ObjectMapper objectMapper = new ObjectMapper();
        byte[] body = objectMapper.writeValueAsBytes(order);

        MessageProperties messageProperties = new MessageProperties();
        messageProperties.setContentType("json");

        Message message = new Message(body,messageProperties);

        System.out.println("id: "+order.getOrderId());

        //指定correlationData的值
        rabbitTemplate.send("zhihao.direct.exchange","zhihao.miao.order",message,new CorrelationData(order.getOrderId().toString()));

        TimeUnit.SECONDS.sleep(10);

        context.close();
    }
}

控制臺打印:

入庫操作
id: 11bc9eb3-fbcb-4777-9596-b6f6db81cafc
十月 22, 2017 7:14:14 下午 org.springframework.amqp.rabbit.connection.CachingConnectionFactory createBareConnection
信息: Created new connection: connectionFactory#50ad3bc1:0/SimpleConnection@4efc180e [delegate=amqp://zhihao.miao@192.168.1.131:5672/, localPort= 61095]
=====消息進行消費了======
消息id為: CorrelationData [id=11bc9eb3-fbcb-4777-9596-b6f6db81cafc]的消息,已經被ack成功

原理其實和java client是一樣的,我們在發送消息的時候落地本地的消息表(有表示confirm字段),然后進行回調確認的方法中進行狀態的更新,最后輪詢表中狀態不正確的消息進行輪詢重發。

步驟

  • 在容器中的ConnectionFactory實例中加上setPublisherConfirms屬性
    factory.setPublisherConfirms(true);
  • 在RabbitTemplate實例中增加setConfirmCallback回調方法。
  • 發送消息的時候,需要指定CorrelationData,用于標識該發送的唯一id。

對比與java client的publisher confirm:
1.spring amqp不支持批量確認,底層的rabbitmq java client方式支持批量確認。
2.spring amqp提供的方式更加的簡單明了。

參考資料

關于另外一種Publisher Confirms事務機制可以參考下面這篇博客,很是簡單
深入學習RabbitMQ(二):AMQP事務機制

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 來源 RabbitMQ是用Erlang實現的一個高并發高可靠AMQP消息隊列服務器。支持消息的持久化、事務、擁塞控...
    jiangmo閱讀 10,408評論 2 34
  • 1.什么是消息隊列 消息隊列允許應用間通過消息的發送與接收的方式進行通信,當消息接收方服務忙或不可用時,其提供了一...
    zhuke閱讀 4,501評論 0 12
  • 本文章翻譯自http://www.rabbitmq.com/api-guide.html,并沒有及時更新。 術語對...
    joyenlee閱讀 7,705評論 0 3
  • 1. 歷史 RabbitMQ是一個由erlang開發的AMQP(Advanced Message Queue )的...
    高廣超閱讀 6,125評論 3 51
  • 關于消息隊列,從前年開始斷斷續續看了些資料,想寫很久了,但一直沒騰出空,近來分別碰到幾個朋友聊這塊的技術選型,是時...
    預流閱讀 585,477評論 51 786