文档章节

缓存架构之史上讲的最明白的RabbitMQ可靠消息传输实战演练

极客慧
 极客慧
发布于 2018/09/23 15:28
字数 2415
阅读 6034
收藏 234

缓存架构之史上讲的最明白的RabbitMQ可靠消息传输实战演练

一、背景介绍:消息可靠传递的重要性

比如:某个广告主(如:天猫)想在我们的平台(如:今日头条)投放广告,当通过我们的广告系统新建广告的时候,该消息在同步给redis缓存(es)的时候丢失了,而我们又没有发现,造成该广告无法正常显示出来,那这损失就打了,如果1天都没有该广告的投放记录,那就有可能是上百万的损失了,所以消息的可靠传输多我们的广告系统也是很重要的。 其实,生活中这样的场景很场景,再比如:交易系统、订单系统都必须保证消息的可靠传输,否则,损失是巨大的!!!

二、如何保证消息的可靠传递呢?

1、设置交换机、队列和消息都为持久化

**持久化:**保证在服务器重启的时候可以保持不丢失相关信息,重点解决服务器的异常崩溃而导致的消息丢失问题。但是,将所有的消息都设置为持久化,会严重影响RabbitMQ的性能,写入硬盘的速度比写入内存的速度慢的不只一点点。对于可靠性不是那么高的消息可以不采用持久化处理以提高整体的吞吐率,在选择是否要将消息持久化时,需要在可靠性和吞吐量之间做一个权衡。 处于某种应用场景,如:大流量的订单交易系统,为了不影响性能,我们可以不设置持久化,但是我们会定时扫描数据库中的未发送成功的消息,进行重试发送,实际应用场景,我们其实有很多解决方案,不要故步自封,换个角度多想想,只有经历多了,才能应用的更加得心应手。

1)交换机的持久化

@Bean
DirectExchange advanceExchange() {
    return new DirectExchange(exchangeName);
}

注释:查看源码,易知,默认是持久化的

2)队列的持久化

@Bean
public Queue advanceQueue() {
    return new Queue(queueName);
}

注释:查看源码,易知,默认是持久化的

3)消息的持久化

当我们使用RabbitTemplate调用了 convertAndSend(String exchange, String routingKey, final Object object) 方法。默认就是持久化模式

注意:

  • 持久化的消息在到达队列时就被写入到磁盘,并且如果可以,持久化的消息也会在内存中保存一份备份,这样可以提高一定的性能,只有在内存吃紧的时候才会从内存中清楚。
  • 非持久化的消息一般只保存在内存中,在内存吃紧的时候会被换入到磁盘中,以节省内存空间。

2、生产者消息确认机制

当消息发送出去之后,我们如何知道消息有没有正确到达exchange呢?如果在这个过程中,消息丢失了,我们根本不知道发生了什么,也不知道是什么原因导致消息发送失败了 为解决这个问题,主要有如下两种方案:

  • 通过事务机制实现
  • 通过生产者消息确认机制(publisher confirm)实现

但是使用事务机制实现会严重降低RabbitMQ的消息吞吐量,我们采用一种轻量级的方案——生产者消息确认机制

什么是消息确认机制? 简而言之,就是:生产者发送的消息一旦被投递到所有匹配的队列之后,就会发送一个确认消息给生产者,这就使得生产者知晓消息已经正确到达了目的地。 如果消息和队列是持久化存储的,那么确认消息会在消息写入磁盘之后发出。 再补充一个Mandatory参数:当Mandatory参数设为true时,如果目的不可达,会发送消息给生产者,生产者通过一个回调函数来获取该信息。

3、消费者消息确认机制

为了保证消息从队列可靠地到达消费者,RabbitMQ提供了消费者消息确认机制(message acknowledgement)。采用消息确认机制之后,消费者就有足够的时间来处理消息,不用担心处理消息过程中消费者进程挂掉后消息丢失的问题,因为RabbitMQ会一直等待并持有消息,直到消费者确认了该消息。

4、死信队列

DLX,Dead Letter Exchange 的缩写,又死信邮箱、死信交换机。DLX就是一个普通的交换机,和一般的交换机没有任何区别。 当消息在一个队列中变成死信(dead message)时,通过这个交换机将死信发送到死信队列中(指定好相关参数,rabbitmq会自动发送)。

什么是死信呢?什么样的消息会变成死信呢?

  • 消息被拒绝(basic.reject或basic.nack)并且requeue=false.
  • 消息TTL过期
  • 队列达到最大长度(队列满了,无法再添加数据到mq中)

应用场景分析: 在定义业务队列的时候,可以考虑指定一个死信交换机,并绑定一个死信队列,当消息变成死信时,该消息就会被发送到该死信队列上,这样就方便我们查看消息失败的原因了 **如何使用死信交换机呢?

定义业务(普通)队列的时候指定参数:

  • x-dead-letter-exchange: 用来设置死信后发送的交换机
  • x-dead-letter-routing-key:用来设置死信的routingKey
@Bean
public Queue helloQueue() {
    //将普通队列绑定到私信交换机上
    Map<String, Object> args = new HashMap<>(2);
    args.put(DEAD_LETTER_QUEUE_KEY, deadExchangeName);
    args.put(DEAD_LETTER_ROUTING_KEY, deadRoutingKey);
    Queue queue = new Queue(queueName, true, false, false, args);
    return queue;
}

三、实战演练

项目代码下载地址https://gitee.com/jikeh/JiKeHCN-RELEASE.git 项目名:spring-boot-rabbitmq-reliability

1、开启生产者消息确认机制

# 开启发送确认
spring.rabbitmq.publisher-confirms=true
# 开启发送失败退回
spring.rabbitmq.publisher-returns=true

2、开启消费者消息确认机制

# 开启ACK
spring.rabbitmq.listener.simple.acknowledge-mode=manual

3、基本配置

@Configuration
public class RabbitConfig {

    public final static String queueName = "hello_queue";

    /**
     * 死信队列:
     */
    public final static String deadQueueName = "dead_queue";
    public final static String deadRoutingKey = "dead_routing_key";
    public final static String deadExchangeName = "dead_exchange";

    /**
     * 死信队列 交换机标识符
     */
    public static final String DEAD_LETTER_QUEUE_KEY = "x-dead-letter-exchange";
    /**
     * 死信队列交换机绑定键标识符
     */
    public static final String DEAD_LETTER_ROUTING_KEY = "x-dead-letter-routing-key";

    @Bean
    public Queue helloQueue() {
        //将普通队列绑定到私信交换机上
        Map<String, Object> args = new HashMap<>(2);
        args.put(DEAD_LETTER_QUEUE_KEY, deadExchangeName);
        args.put(DEAD_LETTER_ROUTING_KEY, deadRoutingKey);
        Queue queue = new Queue(queueName, true, false, false, args);
        return queue;
    }

    /**
     * 死信队列:
     */

    @Bean
    public Queue deadQueue() {
        Queue queue = new Queue(deadQueueName, true);
        return queue;
    }

    @Bean
    public DirectExchange deadExchange() {
        return new DirectExchange(deadExchangeName);
    }

    @Bean
    public Binding bindingDeadExchange(Queue deadQueue, DirectExchange deadExchange) {
        return BindingBuilder.bind(deadQueue).to(deadExchange).with(deadRoutingKey);
    }

}

注释:hell_queue就配置了死信交换机、死信队列

4、生产者核心代码

@Component
public class HelloSender implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback{

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void send(String exchange, String routingKey) {
        String context = "你好现在是 " + new Date();
        System.out.println("send content = " + context);
        this.rabbitTemplate.setMandatory(true);
        this.rabbitTemplate.setConfirmCallback(this);
        this.rabbitTemplate.setReturnCallback(this);
        this.rabbitTemplate.convertAndSend(exchange, routingKey, context);
    }

    /**
     * 确认后回调:
     * @param correlationData
     * @param ack
     * @param cause
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (!ack) {
            System.out.println("send ack fail, cause = " + cause);
        } else {
            System.out.println("send ack success");
        }
    }

    /**
     * 失败后return回调:
     *
     * @param message
     * @param replyCode
     * @param replyText
     * @param exchange
     * @param routingKey
     */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        System.out.println("send fail return-message = " + new String(message.getBody()) + ", replyCode: " + replyCode + ", replyText: " + replyText + ", exchange: " + exchange + ", routingKey: " + routingKey);
    }
}

5、消费者核心代码

@Component
@RabbitListener(queues = RabbitConfig.queueName)
public class HelloReceiver {

    @RabbitHandler
    public void process(String hello, Channel channel, Message message) throws IOException {
        try {
            Thread.sleep(2000);
            System.out.println("睡眠2s");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            //告诉服务器收到这条消息 已经被我消费了 可以在队列删掉;否则消息服务器以为这条消息没处理掉 后续还会在发
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
            System.out.println("receiver success = " + hello);
        } catch (Exception e) {
            e.printStackTrace();
            //丢弃这条消息
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
            System.out.println("receiver fail");
        }

    }

}

6、测试生产者消息确认功能:分为4种场景来测试

//1、exchange, queue 都正确, confirm被回调, ack=true
@RequestMapping("/send1")
@ResponseBody
public String send1() {
    helloSender.send(null, RabbitConfig.queueName);
    return "success";
}

//2、exchange 错误, queue 正确, confirm被回调, ack=false
@RequestMapping("/send2")
@ResponseBody
public String send2() {
    helloSender.send("fail-exchange", RabbitConfig.queueName);
    return "success";
}

//3、exchange 正确, queue 错误, confirm被回调, ack=true; return被回调 replyText:NO_ROUTE
@RequestMapping("/send3")
@ResponseBody
public String send3() {
    helloSender.send(null, "fail-queue");
    return "success";
}

//4、exchange 错误, queue 错误, confirm被回调, ack=false
@RequestMapping("/send4")
@ResponseBody
public String send4() {
    helloSender.send("fail-exchange", "fail-queue");
    return "success";
}

7、测试消费者消息确认功能

  • 1)当添加这行代码的时候: channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); 测试结果:消息被正常消费,消息从队列中删除
  • 2)当注释掉这行代码的时候: channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); 测试结果:消息会被重复消费,一直保留在队列当中

8、测试死信队列

当执行这行代码的时候: channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false); 消息会被加入到死信队列中:

四、拓展

除了我们上面讲的基本可靠性保证外,其实还有很多性能优化方案、可靠性保证方案:集群监控、流控、镜像队列、HAProxy+Keeplived高可靠负载均衡 我们后续会继续分享上述内容,欢迎持续关注…… 下节课,我们将会将该功能应用到缓存架构上了

更多内容,请关注头条号:极客慧,或者访问http://www.jikeh.cn

© 著作权归作者所有

共有 人打赏支持
极客慧
粉丝 29
博文 8
码字总数 14207
作品 0
杭州
高级程序员
私信 提问
加载中

评论(5)

吕兵阳
吕兵阳
可以 收藏了
极客慧
极客慧

引用来自“lemonwater”的评论

没有Python版本实现的代码吗
暂时还没有,思路都一样😄
极客慧
极客慧

引用来自“martinyuan”的评论

错别字很多啊。第一段就发现两处。😓
说明是原创的,不是copy的😄
lemonwater
lemonwater
没有Python版本实现的代码吗
martinyuan
martinyuan
错别字很多啊。第一段就发现两处。😓
史上最透彻的 RabbitMQ 可靠消息传输实战

可能是缓存架构之史上讲的最明白的 RabbitMQ 可靠消息传输实战演练。 一、背景介绍:消息可靠传递的重要性 比如:某个广告主(如:天猫)想在我们的平台(如:今日头条)投放广告,当通过我们...

程序员之家_
2018/09/27
0
0
(一)RabbitMQ消息队列-RabbitMQ的优劣势及产生背景

本篇并没有直接讲到技术,例如没有先写个Helloword。我想在选择了解或者学习一门技术之前先要明白为什么要现在这个技术而不是其他的,以免到最后发现自己学错了。同时如果已经确定就是他,最...

Super_RD
2017/04/18
0
0
RabbitMQ 实战教程 文集

RabbitMQ 实战教程 文集 此系列博客经梁总(梁桂钊的博客)授权录入本站点,下面推荐下梁总的技术公众号【服务端的思维】,公众号不定时更新技术文章,干货满满!! RabbitMQ 实战教程(一) He...

chenssy
2018/10/06
0
0
Docker下RabbitMQ三部曲之一:极速体验(单机和集群)

从本章开始,我们一起在Docker环境实战RabbitMQ环境部署和对应的Java开发,当前是《Docker下RabbitMQ三部曲》系列的第一篇,整个三部曲由以下三篇文章组成: 1. 第一篇,即本章,我们用最快的...

boling_cavalry
2018/05/12
0
0
消息中间件—RabbitMQ(初探篇)

文章摘要:本篇文章为RabbitMQ的入门文章,不像其他一些程序代码和应用实战性的文章会带着大家从一个“Hello World”的简单例子出发,在该篇幅中主要给大家讲下RabbitMQ消息队列的起源、为何...

癫狂侠
2018/05/23
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Kafka入门

1、Kafka使用背景 在我们大量使用分布式数据库、分布式计算集群的时候,是否会遇到这样的一些问题: 我们想分析下用户行为(pageviews),以便我们设计出更好的广告位 我想对用户的搜索关键词...

watermelon11
昨天
0
0
Cannot list resources Permission denied: user=Administrator, access=READ_EXECUTE, inode="/tmp"

Cannot list resources Permission denied: user=ÁõÎĺÁ, access=READ_EXECUTE, inode="/tmp":root:supergroup:drwx-wx-wx windows系统 Spring boot 整合hadoop时报错。 java.lang.Ille......

lwenhao
昨天
1
0
【结构型】- 享元模式

享元模式 作用:利用共享技术有效地支持大量细粒度对象的复用 享元模式状态 内部状态:在享元对象内部不随外界环境改变而改变的共享部分,存储于享元对象内部 外部状态:随着环境的改变而改变...

ZeroneLove
昨天
2
0
Vue 中使用UEditor富文本编辑器-亲测可用-vue-ueditor-wrap

一、Vue中在使用Vue CLI开发中默认没法使用UEditor 其中UEditor中也存在不少错误,再引用过程中。 但是UEditor相对还是比较好用的一个富文本编辑器。 vue-ueditor-wrap说明 Vue + UEditor + ...

tianma3798
昨天
5
0
php-fpm配置

php-fpm配置 修改bbs.wangzb.cc.conf配置文件,将端口9000改为9001,重新访问网站是失败的 修改配置文件 # vim /etc/nginx/conf.d/bbs.wangzb.cc.conf# nginx -s reloadfastcgi_pass 1...

wzb88
昨天
4
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部