持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情
前情回顾:juejin.cn/post/715319… 上次我们讲解了rabbitmq的基本理论和要学习的集中模型,这次我们来学学这写模型及其应用场景
基本消息队列模型--HelloWorld案例
最简单的案例入手:
- Publisher:消息发布者
- Queue:消息队列,负责接收并且缓存消息
- Consumer:订阅队列,处理队列中的信息
和上面说的一样,消息提供者发布消息,到队列中,服务消费者从队列中拿去,慢慢消化
我们创建一个多模块项目
父模块的pom的parent是springboot,我们不用再引入springboot依赖
引入一个lombok,单元测试,AMQP的依赖
去
在publisher模块里,我们写了一个test类
public class PublisherTest {
@Test
public void testSendMessage() throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("你的主机名");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("你的账户");
factory.setPassword("你的密码");
// 1.2.建立连接
Connection connection = factory.newConnection();
// 2.创建通道Channel
Channel channel = connection.createChannel();
// 3.创建队列
String queueName = "simple.queue";
channel.queueDeclare(queueName, false, false, false, null);
// 4.发送消息
String message = "hello, rabbitmq!";
channel.basicPublish("", queueName, null, message.getBytes());
System.out.println("发送消息成功:【" + message + "】");
// 5.关闭通道和连接
channel.close();
connection.close();
}
}
第一步,建立连接,填上你的账户密码和你对应的虚拟主机
第二步,创建通道
第三步,创建队列名称
第四步,发送消息,往你的队列中发
第五步,关闭通道和连接
我们在对应的可视化界面也都可以看到对应代码的作用结果
同时,我们也可以看到发送的消息
当我们执行完第五步,都断开链接了,消息却还在,说明它已经完成了消息发送的功能却没有
下面是接收消息的消费者
public class ConsumerTest {
public static void main(String[] args) throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("主机名);
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("账户");
factory.setPassword("密码");
// 1.2.建立连接
Connection connection = factory.newConnection();
// 2.创建通道Channel
Channel channel = connection.createChannel();
// 3.创建队列
String queueName = "simple.queue";
channel.queueDeclare(queueName, false, false, false, null);
// 4.订阅消息
channel.basicConsume(queueName, true, new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
// 5.处理消息
String message = new String(body);
System.out.println("接收到消息:【" + message + "】");
}
});
System.out.println("等待接收消息。。。。");
}
}
前面几步和消息发送的代码类似,我们不做赘述
到订阅消息这一步的时候,用到了回调函数的思想,我们将这个回调函数的逻辑绑定到队列上,但是此时消息还没有过来,因此我们会先执行等待接收消息。。。然后等rabbitmq把消息传递过来了,再去执行回调函数,也就是接收到消息【】这时候正是说明了这种异步的机制
在执行完我们发现
队列里的ready的数目已经变成了0,说明消息已经被消费了
SpringAMOP
AMQP:Advanced Message Queue Protocal ,是在应用程序或之间传递业务信息的开放标准。该协议与语言和平台无关,更符合微服务中独立性的要求
Spring AMQP:Spring AMQP 是基于AMQP协议定义的一套API规范,提供模板来发送和接收消息,包含两部分,其中spring-amqp是基础抽象,spring-rabbit是底层默认实现
这个东西是帮助我们便捷的消息接收发送和队列的声明,之后我们几个模板都用它来实现
基本消息队列模型--HelloWorld案例--AMQP实现
1.引入依赖
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2.在publisher服务编写application.yml,添加mq连接信息
spring:
rabbitmq:
host: 主机名
port: 5672
virtual-host: /
username: nika
password: 密码
3.在test类测试
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmpqTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSimpleQueue(){
String queueName = "simple.queue";
String message = "hello,Spring amqp!";
rabbitTemplate.convertAndSend(queueName,message);
}
}
你看,用ampq方式,只需要写一个队列名和要发送的信息就好,是不是很方便?
在启动完成测试类以后,由于我们之前的服务消费者服务没关,自动就收到了,说明发送消息成功!
我们再来试着用amqp的方式去写一个监听消息的方法
1.配置文件书写,和publisher一样
2.写一个listener
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueue(String msg){
System.out.println("消费者接收到simple.queue的消息:【" + msg + "】");
}
}
我们先把它注册成一个component,让spring帮助识别
之后rabbit的监听器,标注要监听的队列名,再用一个string接收传递来的队列消息,打印试试
然后我们启动main函数,等待接收消息
我们再启动运行这个函数,发送消息
然后我们再cosumer的日志里发现:
接收消息成功!
WorkQueue模型--AMQP
workqueue从helloworld案例中进化了,从一个消费者变成多个,防止队列消息堆积,共同处理队列消息
下面我们来模拟一下workqueue,实现一个队列绑定多个消费者
1.再Publisher服务测试定义方法,每秒产生50条消息,发送到simple.queue
2.在consumer服务中定义俩消息监听者,都监听simple.queue队列
3.消费者1号每秒处理50条消息,消费者二号每秒处理10条消息
我们在SpringRabbitListener类中这么写:
@Component
public class SpringRabbitListener {
// @RabbitListener(queues = "simple.queue")
// public void listenSimpleQueue(String msg){
// System.out.println("消费者接收到simple.queue的消息:【" + msg + "】");
// }
@RabbitListener(queues = "simple.queue")
public void listenWorkQueue1(String msg) throws InterruptedException {
System.out.println("消费者1号接收到的消息:【" + msg + "】" + LocalTime.now());
Thread.sleep(20);
}
@RabbitListener(queues = "simple.queue")
public void listenWorkQueue2(String msg) throws InterruptedException {
System.err.println("消费者2号接收到的消息:【" + msg + "】" + LocalTime.now());
Thread.sleep(100);
}
}
用线程休眠来模拟一分钟的处理效率
消息发送者这么定义:
@Test
public void testWorkQueue() throws InterruptedException {
String queueName = "simple.queue";
String message = "hello,Spring amqp!___";
for (int i = 0; i <= 50; i++) {
rabbitTemplate.convertAndSend(queueName,message+i);
Thread.sleep(20);
}
}
一分钟发50条消息。
我们先来做个预测吧,因为消息发送者一秒发送50条消息,1号消费者每秒处理50条,2号每秒处理10条,理应处理速度小于1s,按照能者多劳的理论,1号肯定做的事情比2号多,然而事实真的是这样吗?
我们来试试运行代码。
我们看下日志,不难发现,1号消费者处理的量和2号一样,并没有我们预想的能者多劳,这是为什么呢?
原来,有个问题叫消息预取,消费者会提前去队列里取好任务,管他能力够不够,先一人拿一半再说,所以会出现这种情况
我们可以进行配置取消消息预取,在properties文件中,设置prefetch值
这样消费者就会拿一条消息,处理一条消息
我们重启看看配置是否生效
生效!能者多劳!!
发布订阅模型介绍 -- AMQP
发布,订阅模式与之前的案例的区别是允许将同一消息发送给多个消费者。实现方式是加入exchange(交换机)。
发布订阅模型--Fanout Exchange
Fanout Exchange 会将收到的消息路由到每个跟其绑定的queue
我们来实现一下,上案例!
1.在consumer服务中,利用代码声明队列和交换机,并且将两者绑定
2.在consumer服务中,编写俩消费者方法,分别监听fanout.queue1和fanout.queue2
3.在publisher中编写测试方法,向itcast.fanout发送消息
@Configuration
public class FanoutConfig {
//itcast.fanout
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("itcast.fanout");
}
//fanout.queue1
@Bean
public Queue fanoutQueue1(){
return new Queue("fanout.queue1");
}
//绑定队列1到交换机
@Bean
public Binding fanoutBinding1(Queue fanoutQueue1,FanoutExchange fanoutExchange){
return BindingBuilder
.bind(fanoutQueue1)
.to(fanoutExchange);
}
//fanout.queue2
@Bean
public Queue fanoutQueue2(){
return new Queue("fanout.queue2");
}
//绑定队列2到交换机
@Bean
public Binding fanoutBinding2(Queue fanoutQueue2,FanoutExchange fanoutExchange){
return BindingBuilder
.bind(fanoutQueue2)
.to(fanoutExchange);
}
}
我们启动代码,看看网站上
exchange里面有itcast.fanout交换机
queue有三,正确!
也确实绑定上了
我们现在开始写消息的接收
@RabbitListener(queues = "fanout.queue1")
public void listenFanoutQueue1(String msg){
System.out.println("消费者接收到fanoutQueue1的消息:【" + msg + "】" );
}
@RabbitListener(queues = "fanout.queue2")
public void listenFanoutQueue2(String msg){
System.out.println("消费者接收到fanoutQueue2的消息:【" + msg + "】" );
}
和发送
@Test
public void testSendFanoutExchange() {
//交换机名称
String exchangeName = "itcast.fanout";
//消息
String message = "hello,everyone";
//发送消息
rabbitTemplate.convertAndSend(exchangeName,"",message);
}
这样就实现了一次发送,多个队列都可以收到消息
经过上面的案例,我们来总结一下交换机的作用吧
1.接收publisher发送的消息
2.按照将消息按照规则路由到与之绑定的队列
3.不能缓存消息,路由失败,消息丢失
4.fanoutexchange 会将消息路由到每一个绑定的队列当中
发布订阅模型--Direct Exchange
Direct Exchange 会将接收到的消息根据规则路由到指定的Queue,因此被成为路由模式,而我们上述说的fanoutExchange则是把消息发送到每一个Queue中
- 每一个Queue都会和Exchange设置一个BindingKey
- 发送者发送消息的时候指定一个RoutingKey
- Exchange将消息路由到BindingKey和RoutingKey一致的队列
看图片理解可能更加清晰
比方说,现在我们已经将队列和交换机绑定好了,就像上图一样。
如果发布者发布的信息里的RoutingKey携带了red,那么消息会进入这两个队列,因为BindingKey里面都有red
如果发布者发布的信息里的RoutingKey携带了yellow,那么消息只会进入下面那个队列
好了接下来我们来对上述逻辑进行代码实现
思路:
1.利用@RabbitListener去声明交换机,队列,routingkey(这里就不用bean去声明交换机队列和它们的绑定关系了,因为实在太麻烦,要写好多好多bean)
2.在consumer服务中,编写俩消费者方法,分别监听direct.queue1,和direct.queue2(自定义的俩队列)
在listner类里面定义这些
@RabbitListener( bindings = @QueueBinding(
value = @Queue(name = "directQueue1"),
exchange = @Exchange(name = "itcast.direct",type = ExchangeTypes.DIRECT),
key = {"red","blue"}
))
public void listenDirectQueue1(String msg){
System.out.println("消费者接收到DirectQueue1的消息:【" + msg + "】" );
}
@RabbitListener( bindings = @QueueBinding(
value = @Queue(name = "directQueue2"),
exchange = @Exchange(name = "itcast.direct",type = ExchangeTypes.DIRECT),
key = {"red","yellow"}
))
public void listenDirectQueue2(String msg){
System.out.println("消费者接收到DirectQueue2的消息:【" + msg + "】" );
}
然后重启代码
交换机,队列,都被注册上去了,再看看绑定关系
OK,完美!
我们再定义发布者,先发送red吧
@Test
public void testSendDirectExchange() {
//交换机名称
String exchangeName = "itcast.fanout";
//消息
String message = "hello";
//发送消息
rabbitTemplate.convertAndSend(exchangeName,"red",message);
}
我们之前的预测是俩都会收到,看看结果!
没错!那接下来我们发送yellow,我们的预测是队列2会收到
测试成功!
我们来做个小总结吧,无论是用bean还是用注解,都是要申明交换机,queue和两者之间的联系,如果,你要精准发送,就得用一些key来路由
发布订阅--TopicExchange
想象一个场景,如果说这个key很多很多,那是不是这个key在方法上面越写越多?这很麻烦。
TopicExchange和DirectExchange十分类似,区别在于routingkey必须是多个单词的列表,并且要以 .分割
我们还是看一张图:
Queue与Exchange指定BindingKey的时候,可以使用通配符:
#:代表0个或多个单词
*:代表一个单词
举个例子:
如果queue1的bindingkey是china.#,那么发布者的routingkey如果写china.abasbdbabs.asdasdasd.asdasdaaasdchina后面跟好多内容,是不是都可以匹配到queue1啊?
再举个例子:
如果queue2的bindingkey是#.news,那么发布者的routingkey如果写
asdasdasdasdasdasd.asdasdasdasdasd.news也都可以匹配到queue2啊?
来,思路讲完了,上代码!
思路:
1.还是利用@RabbitListener注解定义交换机队列,bindingkey
2.在consumer服务里面,编写俩消费者方法,分别监听topic.queue1和topic.queue2
3.在publisher里面编写测试方法,发送消息
@RabbitListener( bindings = @QueueBinding(
value = @Queue(name = "topicQueue1"),
exchange = @Exchange(name = "itcast.topic",type = ExchangeTypes.TOPIC),
key = {"china.#"}
))
public void listenTopicQueue1(String msg){
System.out.println("消费者接收到TopicQueue1的消息:【" + msg + "】" );
}
@RabbitListener( bindings = @QueueBinding(
value = @Queue(name = "topicQueue2"),
exchange = @Exchange(name = "itcast.topic",type = ExchangeTypes.TOPIC),
key = {"#.news"}
))
public void listenTopicQueue2(String msg){
System.out.println("消费者接收到TopicQueue2的消息:【" + msg + "】" );
}
重启,在可视化网站上看看有没有被注册。
@Test
public void testSendTopicExchange() {
//交换机名称
String exchangeName = "itcast.topic";
//消息
String message = "hello";
//发送消息
rabbitTemplate.convertAndSend(exchangeName,"china.news",message);
}
来,经过我们上述的分析,这段代码是会被哪个队列收到呢?
没错!是两个都收到了,因为i都符合匹配规则!
@Test
public void testSendTopicExchange() {
//交换机名称
String exchangeName = "itcast.topic";
//消息
String message = "hello";
//发送消息
rabbitTemplate.convertAndSend(exchangeName,"asdasdasdasdasdasdas.news",message);
}
这段代码由于只匹配队列2的规则,所以:
完美!
消息转换器
之前我们的案例传送的数据都是string,但是rabbitTemplate能否传对象呢?
我们来试一试
先在发布者这边写下发布内容和要发布的队列
@Test
public void testSendObject() {
String exchangeName = "object.queue";
HashMap<String, String> map = new HashMap<>();
map.put("nika","菜鸡");
map.put("南信大","不行");
//发送消息
rabbitTemplate.convertAndSend(exchangeName,map);
}
由于我们不想让队列里的消息被消费,我们想看看在rabbitmq可视化里面的消息嘛,所以这里我们只配置了队列的bean,这样,消息发送以后就只会停留在队列中,不会被消费
@Bean
public Queue objectQueue(){
return new Queue("object.queue");
}
在可视化里面,我们看到了之前放进去的map:
什么?这是啥?为什么那么长?
原来这个东西spring内部帮我们做了序列化,并且用的是jdk的序列化(object。outputstream)!这种序列化安全性差,且效率不过,你看看消息体那么大,占用内存空间,传输速度也会变慢!
因此我推荐大家,不要用这个序列化方式!建议修改!
那么怎么样去修改呢?
我们推荐用JSON序列化,步骤如下:
-
在父模块引入依赖
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> -
配置bean
@Bean public MessageConverter messageConverter(){ return new Jackson2JsonMessageConverter(); //这也是springmvc里面经常用到的工具转化类 }这里我们是在启动类里进行的配置,为了方便,嗯
好了重启服务!
发送!
ok,短小精悍!可读性强的一!
我们完成了消息的发送的序列化,而消息接收肯定得反序列化
]同样在consumer模块配置相同代码即可