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事务机制

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • 来源 RabbitMQ是用Erlang实现的一个高并发高可靠AMQP消息队列服务器。支持消息的持久化、事务、拥塞控...
    jiangmo阅读 10,343评论 2 34
  • 1.什么是消息队列 消息队列允许应用间通过消息的发送与接收的方式进行通信,当消息接收方服务忙或不可用时,其提供了一...
    zhuke阅读 4,451评论 0 12
  • 本文章翻译自http://www.rabbitmq.com/api-guide.html,并没有及时更新。 术语对...
    joyenlee阅读 7,632评论 0 3
  • 1. 历史 RabbitMQ是一个由erlang开发的AMQP(Advanced Message Queue )的...
    高广超阅读 6,092评论 3 51
  • 关于消息队列,从前年开始断断续续看了些资料,想写很久了,但一直没腾出空,近来分别碰到几个朋友聊这块的技术选型,是时...
    预流阅读 584,386评论 51 785