springboot整合rabbitMq

一.rabbitMq简介及其应用场景

1.简介

为什么要使用消息中间件呢?以熟悉的电商场景为例,如果商品服务和订单服务是两个不同的微服务,在下单的过程中订单服务需要调用商品服务进行扣库存操作。按照传统的方式,下单过程要等到调用完毕之后才能返回下单成功,如果网络产生波动等原因使得商品服务扣库存延迟或者失败,会带来较差的用户体验,如果在高并发的场景下,这样的处理显然是不合适的,那怎么进行优化呢?这就需要消息队列登场了。

消息队列提供一个异步通信机制,消息的发送者不必一直等待到消息被成功处理才返回,而是立即返回。消息中间件负责处理网络通信,如果网络连接不可用,消息被暂存于队列当中,当网络畅通的时候在将消息转发给相应的应用程序或者服务,当然前提是这些服务订阅了该队列。如果在商品服务和订单服务之间使用消息中间件,既可以提高并发量,又降低服务之间的耦合度。

2.消息队列常用应用场景

a.异步处理

假设有这么一个需求:用户注册之后,给用户分别发送一条邮件以及短信,告诉用户你已注册成功。
方案一:串行方式

串行方式.png

方案二:并行方式
并行方式.png

方案三:引入消息队列
消息队列.png

从上面的三张图中,我们不难看出引用消息队列的优点:
1.加快了系统运行的速度
2.用户信息保存进数据库,用户就已经注册成功,短信与邮件成功发送与否其实与用户是否注册成功并无直接关系,所以用户注册成功之后,并将信息写入消息队列之后我们直接返回注册成功,发送短信与邮件模块订阅对应消息,实现了消息的异步操作。

b.流量削峰

流量削峰一般在秒杀活动中应用广泛
场景:秒杀活动,一般会因为流量过大,导致应用挂掉,为了解决这个问题,一般在应用前端加入消息队列。
作用:
1.可以控制活动人数,超过此一定阀值的订单直接丢弃
2.可以缓解短时间的高流量压垮应用(应用程序按自己的最大处理能力获取订单)

c.应用解耦

场景:双11是购物狂节,用户下单后,订单系统需要通知库存系统,传统的做法就是订单系统调用库存系统的接口。


一般做法.png

这种做法有一个缺点:当库存系统出现故障时,订单就会失败。订单系统和库存系统高耦合。


引入消息中间件.png

订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。

库存系统:订阅下单的消息,获取下单消息,进行库操作。
就算库存系统出现故障,消息队列也能保证消息的可靠投递,不会导致消息丢失。

以上内容的来源是:https://blog.csdn.net/whoamiyang/article/details/54954780,在此感谢。

3.为什么选择rabbitMQ

系统吞吐量(TPS)比较 一ZeroMq 最好,RabbitMq 次之, ActiveMq 最差。

持久化消息比较—zeroMq不支持,activeMq和rabbitMq都支持。持久化消息主要是指:MQ down或者MQ所在的服务器down了,消息不会丢失的机制。

可靠性、灵活的路由、集群、事务、高可用的队列、消息排序、问题追踪、可视化管理工具、插件系统、社区—RabbitMq最好,ActiveMq次之,ZeroMq最差。

rabbitMQ完美实现了amqp的接口,与springboot的兼容性更好。

高并发—从实现语言来看,RabbitMQ最高,原因是它的实现语言是天生具备高并发高可用的erlang语言。

综上所述:RabbitMQ的性能相对来说更好更全面,是消息中间件的首选。

4.rabbitMq的安装

rabbitMq安装教程在网上有很多,在这里不在多说。大家可以参考https://blog.csdn.net/zhm3023/article/details/82217222
这篇博客的内容。
注意:RabbitMQ是使用Erlang语言来编写的,所以在安装RabbitMQ之前,需要安装Erlang,根据你选择的Erlang版本,安装对应版本的rabbitMQ。两者版本对应可参考如下链接:
https://www.rabbitmq.com/which-erlang.html

二.springboot中整合rabbitMQ

1.引入amqp启动器

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
  </dependency>

2.配置文件application.yml修改

spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    #确认消息已发送到交换机(Exchange)
    publisher-confirms-type: correlated
    #确认消息已发送到队列(Queue)
    publisher-returns: true

我的sringboot配置文件使用的是yml格式,使用.properties配置文件的小伙伴们可以参考如下配置:

#对于rabbitMQ的支持
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

3.rabbitMQ简介

在正式编码之前,我们做一些rabbitMQ简介,以便更方便的理解代码

a.Message

消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。

b.publisher

消息的生产者,也是一个向交换器发布消息的客户端应用程序。
有4种类型:direct(默认),fanout, topic, 和headers,不同类型的Exchange转发消息的策略有所区别

c.Exchange

交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。

d.Queue

消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。

e.Binding

绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。
exchange与queue是多对多的对应关系。

f.Channel

信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。Consumer
消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。

g.Broker

表示消息队列服务器实体

各部分对应关系图.png

4.rabbitMQ四种交换机

a.direct exchange(直连型交换机)

处理路由键routing-key。需要将一个队列绑定到交换机上,要求该消息与一个特定的路由键完全匹配。这是一个完整的匹配。
首先我们创建一个DirectRabbitConfig.java类,用于队列和交换机持久化以及连接使用设置。详细说明在代码见下列代码的注释。

package cn.xdw.helloworld.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class DirectRabbitConfig {

    // 任务日志
    private Logger logger = LoggerFactory.getLogger(this.getClass());

    //队列 起名:TestDirectQueue
    @Bean
    public Queue TestDirectQueue() {
        // durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
        // exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
        // autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
        //   return new Queue("TestDirectQueue",true,true,false);

        //一般设置一下队列的持久化就好,其余两个就是默认false
        return new Queue("TestDirectQueue",true);
    }

    //Direct交换机 起名:TestDirectExchange
    @Bean
    DirectExchange TestDirectExchange() {
        //  return new DirectExchange("TestDirectExchange",true,true);
        return new DirectExchange("TestDirectExchange",true,false);
    }

    //绑定  将队列和交换机绑定, 并设置用于匹配键:TestDirectRouting
    // (队列与交换机是通过路由来进行绑定的,所以在传值的时候,只需要穿交换机的名称以及绑定路由的名称,交换机会自动通过路由来将消息分配到指定队列)
    @Bean
    Binding bindingDirect() {
        // 路由键routing-key为TestDirectRouting
        return BindingBuilder.bind(TestDirectQueue()).to(TestDirectExchange()).with("TestDirectRouting");
    }
}

接着,我们编写一个发送消息的方法

    @RequestMapping("/directSend/{id}")
    @ResponseBody
    public Object directSend(@PathVariable int id) {
        Teacher teacher = teacherService.detail(id);
        if(teacher != null) {
            // 将数据添加进队列
            rabbitTemplate.convertAndSend("TestDirectExchange", "TestDirectRouting", teacher);
        }
        return teacher;
    }

接着,我们编写消息接收端部分的代码,也就是我们的消费者

package cn.xdw.helloworld.rabbitmq;


import cn.xdw.helloworld.entity.Teacher;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
@RabbitListener(queues="TestDirectQueue")
public class DirectReceiver {

    @RabbitHandler
    public void process(Teacher teacher) {
        System.out.println("第一个DirectReceiver消费者收到消息  : " + teacher.toString());
    }
}

运行程序,我们,访问我们的接口地址,结果如下:


直连型交换机运行结果.png

补充说明:直连交换机是一对一,如果配置多台监听绑定到同一个直连交互的同一个队列,结果是采用轮询的方式对消息进行消费,而且消息不存在重复消费。

b.fanout exchange(扇形交换机,广播模式)

创建FanoutRabbitConfig.java:

package cn.xdw.helloworld.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FanoutRabbitConfig {

    @Bean
    public Queue queueA() {
        return new Queue("fanout.A");
    }

    @Bean
    public Queue queueB() {
        return new Queue("fanout.B");
    }

    @Bean
    public Queue queueC() {
        return new Queue("fanout.C");
    }

    @Bean
    FanoutExchange fanoutExchange() {
        return new FanoutExchange("fanoutExchange");
    }

    @Bean
    Binding bindingExchangeA() {
        return BindingBuilder.bind(queueA()).to(fanoutExchange());
    }

    @Bean
    Binding bindingExchangeB() {
        return BindingBuilder.bind(queueB()).to(fanoutExchange());
    }

    @Bean
    Binding bindingExchangeC() {
        return BindingBuilder.bind(queueC()).to(fanoutExchange());
    }
}

上述代码在交换机与路由进行绑定时,并未指定绑定路由键routing-key,该种模式的交换机,只要生产者发送消息至交换机,与该交换机绑定的所有队列都能收到消息。

    @RequestMapping("/fanoutSend/{id}")
    @ResponseBody
    public Object fanoutSend(@PathVariable int id) {
        Teacher teacher = teacherService.detail(id);
        if(teacher != null) {
            // 将数据添加进队列(根据性别,使用不同的routeKey)
            rabbitTemplate.convertAndSend("fanoutExchange", null, teacher);
        }
        return teacher;
    }

上述代码为发送代码,可以看出我们发送消息时,只指定了交换机,并未指定routing-key。

package cn.xdw.helloworld.rabbitmq;

import cn.xdw.helloworld.entity.Teacher;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
@RabbitListener(queues = "fanout.A")
public class FanoutReceiverA {

    @RabbitHandler
    public void process(Teacher teacher) {
        System.out.println("FanoutReceiverA消费者收到消息  : " +teacher.toString());
    }

}

上述代码为消费者端代码,这里其实有三个监听队列器,这里只插入一段代码,其他两个监听器代码修改一下监听队列的名称就行了。
运行结果如下:


扇形监听器运行结果.png

从中我们可以看出,3个队列都接收到了消息。

c.topic exchange(主题交换机)

创建TopicRabbitConfig.java配置文件:

package cn.xdw.helloworld.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TopicRabbitConfig {

    public final static String MAN = "topic.man";

    public final static String WOMAN = "topic.woman";

    @Bean
    public Queue firstQueue() {
        return new Queue(TopicRabbitConfig.MAN);
    }

    @Bean
    public Queue secondQueue() {
        return new Queue(TopicRabbitConfig.WOMAN);
    }

    @Bean
    TopicExchange exchange() {
        return new TopicExchange("topicExchange");
    }

    //将firstQueue和topicExchange绑定,而且绑定的键值为topic.man
    //这样只要是消息携带的路由键是topic.man,才会分发到该队列
    @Bean
    Binding bindingExchangeMessage() {
        return BindingBuilder.bind(firstQueue()).to(exchange()).with(MAN);
    }

    //将secondQueue和topicExchange绑定,而且绑定的键值为用上通配路由键规则topic.#
    // 这样只要是消息携带的路由键是以topic.开头,都会分发到该队列
    @Bean
    Binding bindingExchangeMessage2() {
        return BindingBuilder.bind(secondQueue()).to(exchange()).with("topic.#");
    }
}

上述代码创建了一个名为topicExchange交换机,创建了两个路由,分别为"topic.man"与"topic.woman", 创建两个binding,分别绑定两个队列,两个binding的routing-key分别为topic.man与topic.#。
通配符说明:
符号"#":匹配一个或者多个词lazy.# 可以匹配lazy.irs或者lazy.irs.cor
符号"*":只能匹配一个词lazy.* 可以匹配lazy.irs或者lazy.cor

接下来,我们创建生产者方法,即发送消息到队列的方法:

    @RequestMapping("/topicSend/{id}")
    @ResponseBody
    public Object topicSend(@PathVariable int id) {
        Teacher teacher = teacherService.detail(id);
        if(teacher != null) {
            // 将数据添加进队列(根据性别,使用不同的routeKey)
            if(teacher.getSex() == 0) {
                rabbitTemplate.convertAndSend("topicExchange", TopicRabbitConfig.MAN, teacher);
            } else {
                rabbitTemplate.convertAndSend("topicExchange", TopicRabbitConfig.WOMAN, teacher);
            }

        }
        return teacher;
    }

最后我们创建消费者代码(创建两个监听器,分别对之前定义的两个消息队列进行监听):

package cn.xdw.helloworld.rabbitmq;

import cn.xdw.helloworld.entity.Teacher;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
@RabbitListener(queues = "topic.man")
public class TopicManReceiver {
    @RabbitHandler
    public void process(Teacher teacher) {
        System.out.println("TopicManReceiver消费者收到消息  : " + teacher.toString());
    }

}
package cn.xdw.helloworld.rabbitmq;

import cn.xdw.helloworld.entity.Teacher;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
// 填写线程的名字
@RabbitListener(queues = "topic.woman")
public class TopicTotalReceiver {
    @RabbitHandler
    public void process(Teacher teacher) {
        System.out.println("TopicTotalReceiver消费者收到消息  : " + teacher.toString());
    }
}

运行代码:
当sex=1时,我们发送了一个名为routing-key为topic.woman的消息,topic.man与topic.#只有topic.#适配,所以只有与之绑定的队列topic.woman收到了消息


sex为1时结果.png

当sex=0时,我们发送了一个名为routing-key为topic.man的消息,topic.man与topic.#两者都适配,所以两个队列都收到了消息,运行结果如下:


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