赞
踩
代码地址:https://gitee.com/suisui9857/mq-demo
微服务间通讯有同步和异步两种方式:
同步通讯: 时效性较强,需要实时响应,可以立即得到结果。(打电话)
缺点:
异步通讯: 就像发邮件,不需要马上回复。
用户支付后需要调用订单服务完成订单状态修改,调用物流服务,从仓库分配响应的库存并准备发货。
在事件模式中,支付服务是事件发布者(publisher),在支付完成后只需要发布一个支付成功的事件(event),事件中带上订单id。订单服务和物流服务是事件订阅者(Consumer),订阅支付成功的事件,监听到事件后完成自己业务即可。
为了解除事件发布者与订阅者之间的耦合,两者并不是直接通信,而是有一个中间人(Broker)。发布者发布事件到Broker,不关心谁来订阅事件。订阅者从Broker订阅事件,不关心谁发来的消息。Broker 是一个像数据总线一样的东西,所有的服务要接收数据和发送数据都发到这个总线上,这个总线就像协议一样,让服务间的通讯变得标准和可控。
优点:
缺点:
MQ,中文是消息队列(MessageQueue),字面来看就是存放消息的队列。也就是事件驱动架构中的Broker。
几种常见MQ的对比:
RabbitMQ | ActiveMQ | RocketMQ | Kafka | |
---|---|---|---|---|
公司/社区 | Rabbit | Apache | 阿里 | Apache |
开发语言 | Erlang | Java | Java | Scala&Java |
协议支持 | AMQP,XMPP,SMTP,STOMP | OpenWire,STOMP,REST,XMPP,AMQP | 自定义协议 | 自定义协议 |
可用性 | 高 | 一般 | 高 | 高 |
单机吞吐量 | 一般 | 差 | 高 | 非常高 |
消息延迟 | 微秒级 | 毫秒级 | 毫秒级 | 毫秒以内 |
消息可靠性 | 高 | 一般 | 高 | 一般 |
追求可用性:Kafka、 RocketMQ 、RabbitMQ
追求可靠性:RabbitMQ、RocketMQ
追求吞吐能力:RocketMQ、Kafka
追求消息低延迟:RabbitMQ、Kafka
MQ的基本结构:
在线手册:https://www.rabbitmq.com/getstarted.html
无交换机:
基本消息队列(BasicQueue):
工作消息队列(WorkQueue):
有交换机:
发布订阅(Publish,Subscribe)根据交换机类型分为三种:
广播(Fanout Exchange):
路由(Direct Exchange):
主题(Topic Exchange):
publisher:消息发布者,将消息发送到队列queue
queue:消息队列,负责接收并缓存消息
consumer:订阅队列,处理队列中的消息
导入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
基本消息队列发送消息:
1.建立connection
2.创建channel
3.利用channel声明队列
4.利用channel向队列发送消息
public class PublisherTest { @Test public void testSendMessage() throws IOException, TimeoutException { // 1.建立连接 ConnectionFactory factory = new ConnectionFactory(); // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码 factory.setHost("192.168.221.199"); factory.setPort(5672); factory.setVirtualHost("/"); factory.setUsername("admin"); factory.setPassword("12345678"); // 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(); } }
基本消息队列接收消息:
1.建立connection
2.创建channel
3.利用channel声明队列,都声明队列是避免队列不存在
4.定义consumer的消费行为handleDelivery()
5.利用channel将消费者与队列绑定
public class ConsumerTest { public static void main(String[] args) throws IOException, TimeoutException { // 1.建立连接 ConnectionFactory factory = new ConnectionFactory(); // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码 factory.setHost("192.168.221.199"); factory.setPort(5672); factory.setVirtualHost("/"); factory.setUsername("admin"); factory.setPassword("12345678"); // 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("等待接收消息。。。。"); } }
Advanced Message Queuing Protocol(AMQP):是用于在应用程序或之间传递业务消息的开放标准,该协议与语言和平台无关,更符合微服务中独立性的要求。
Spring AMQP是基于AMQP协议定义的一套API规范,提供了模板来发送和接收消息,包含两部分,其中spring-amqp是基础镜像,spring-rabbit是底层的默认实现。
SpringAMQP提供了三个功能:
1.父工程引入依赖
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
注意:需提前创建simple.queu队列
消息发送:
2.在publisher服务的application.yml中配置MQ地址:
spring:
rabbitmq:
host: 192.168.221.199 # rabbitMQ的ip地址
port: 5672 # 端口
username: admin #用户名
password: 12345678 #密码
virtual-host: / # 虚拟主机
3.在publisher服务中编写测试类SpringAmqpTest,并利用RabbitTemplate实现消息发送:
@RunWith(SpringRunner.class) @SpringBootTest public class SpringAmqpTest { @Autowired private RabbitTemplate rabbitTemplate; @Test public void testSimpleQueue() { // 队列名称 String queueName = "simple.queue"; // 消息 String message = "hello, spring amqp!"; // 发送消息 rabbitTemplate.convertAndSend(queueName, message); } }
消息接收:
1.在consumer服务的application.yml中添加配置:
spring:
rabbitmq:
host: 192.168.221.199 # rabbitMQ的ip地址
port: 5672 # 端口
username: admin #用户名
password: 12345678 #密码
virtual-host: / # 虚拟主机
2.创建SpringRabbitListener接收消息:
@Component
public class SpringRabbitListener {
//配置监听队列,指定队列名称
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueueMessage(String msg) throws InterruptedException {
System.out.println("spring 消费者接收到消息:【" + msg + "】");
}
}
Work queues也被称为(Task queues),多个消费者绑定到一个队列,同一条消息只会被一个消费者处理,多个消费者共同消费队列中的消息。
注意:需提前创建simple.queu队列
消息发送实现:
@RunWith(SpringRunner.class) @SpringBootTest public class SpringAmqpTest { @Autowired private RabbitTemplate rabbitTemplate; /** * workQueue * 向队列中不停发送消息,模拟消息堆积。 */ @Test public void testWorkQueue() throws InterruptedException { // 队列名称 String queueName = "simple.queue"; // 消息 String message = "hello, message_"; for (int i = 0; i < 20; i++) { // 发送消息 rabbitTemplate.convertAndSend(queueName, message + i); Thread.sleep(20); } } }
消息接收实现:
@Component
public class SpringRabbitListener {
@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(200);
}
}
测试结果:消息平分给每个消费者,并没有考虑到消费者的处理能力。
修改consumer服务的application.yml文件:
spring:
rabbitmq:
listener:
simple:
prefetch: 1 #控制消费者预取的消息数量,每次只能获取一条消息,处理完成才能获取下一个消息
允许将同一个消息发送给多个消费者,实现方式是加入了exchange(交换机):
常见的交换机有:Fanout广播,Direct路由,Topic话题。
exchange负责消息路由,而不是存储,路由失效则消息丢失。
Fanout Exchange:将接收到的消息路由到每一个跟其绑定的queue。
1.在consumer服务中,利用代码声明队列,交换机,并将两者绑定
@Configuration public class FanoutConfig { //声明交换机itcast.fanout @Bean public FanoutExchange fanoutExchange(){ return new FanoutExchange("itcast.fanout"); } //声明队列fanout.queue1 @Bean public Queue fanoutQueyue1(){ return new Queue("fanout.queue1"); } //声明队列fanout.queue2 @Bean public Queue fanoutQueyue2(){ return new Queue("fanout.queue2"); } //@Configuration的类会被spring代理,其中类方法调用其他bean会去ioc中找 //绑定队列1到交换机 public Binding fanoutBinding1(Queue fanoutQueyue1,FanoutExchange fanoutExchange){ return BindingBuilder.bind(fanoutQueyue1).to(fanoutExchange); } //绑定队列2到交换机 public Binding fanoutBinding2(Queue fanoutQueyue2,FanoutExchange fanoutExchange){ return BindingBuilder.bind(fanoutQueyue2).to(fanoutExchange); } }
2.在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "fanout.queue1")
public void listenFanoutQueue1(String msg) throws InterruptedException {
System.out.println("消费者接收到fanout.queue1的消息:【" + msg + "】" + LocalTime.now());
}
@RabbitListener(queues = "fanout.queue2")
public void listenFanoutQueue2(String msg) throws InterruptedException {
System.out.println("消费者接收到fanout.queue2的消息:【" + msg + "】" + LocalTime.now());
}
}
3.在publish中编写测试方法,向itcast.fanout发送消息
@RunWith(SpringRunner.class) @SpringBootTest public class SpringAmqpTest { @Autowired private RabbitTemplate rabbitTemplate; /** * 广播发送消息 */ @Test public void testSendFanoutExchange() throws InterruptedException { //交换机名称 String exchangeName ="itcast.fanout"; //消息 String message = "hello, everyOne!"; //发送消息 rabbitTemplate.convertAndSend(exchangeName,"", message); } }
交换机的作用:
1.接收publisher发送的消息
2.将消息按照规则路由到与之绑定的队列
3.不能缓存消息,路由失败,消息丢失
Direct Exchange:将接收到的消息根据规则路由到指定的Queue。
1.利用@RabbitListener声明Exchange,Queue,RoutingKey
2.在consumer服务中,编写两个消费者方法,分别监听direct.queue1和direct.queue2
@Component public class SpringRabbitListener { //定向交换机@RabbitListener声明Exchange,Queue,RoutingKey @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "direct.queue1"), exchange = @Exchange(name = "itcast.direct",type = ExchangeTypes.DIRECT), key = {"red","blue"} )) public void listenDirectQueue1(String msg) throws InterruptedException { System.out.println("消费者接收到direct.queue1的消息:【" + msg + "】" + LocalTime.now()); } @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "direct.queue2"), exchange = @Exchange(name = "itcast.direct",type = ExchangeTypes.DIRECT), key = {"red","yellow"} )) public void listenDirectQueue2(String msg) throws InterruptedException { System.out.println("消费者接收到direct.queue2的消息:【" + msg + "】" + LocalTime.now()); } }
3.在publish中编写测试方法,向itcast.direct发送消息
@RunWith(SpringRunner.class) @SpringBootTest public class SpringAmqpTest { /** * 定向发送消息 */ @Test public void testSendDirectExchange() throws InterruptedException { //交换机名称 String exchangeName ="itcast.direct"; //消息 String message = "hello, blue!"; //发送消息,绑定blue的人可以收到 rabbitTemplate.convertAndSend(exchangeName,"blue", message); } }
Direct交换机与Fanout交换机的差异?
基于@RabbitListener注解声明队列和交换机有哪些常见注解?
TopicExchange与DirectExchange类似,区别在于routingKey必须是多个单词的列表,并以”.“分割。
Queue与Exchange指定BindingKey时可以使用通配符:
#:代指0个或多个单词
举例:tem.#:能够匹配item.spu.insert
或者 item.spu
*:代指一个单词
举例:item.*
:只能匹配item.spu
1.并利用@RabbitListener声明Exchange、Queue、RoutingKey
2.在consumer服务中,编写两个消费者方法,分别监听topic.queue1和topic.queue2
@Component public class SpringRabbitListener { @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "topic.queue1"), exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC), key = "china.#" )) public void listenTopicQueue1(String msg){ System.out.println("消费者接收到topic.queue1的消息:【" + msg + "】"); } @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "topic.queue2"), exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC), key = "#.news" )) public void listenTopicQueue2(String msg){ System.out.println("消费者接收到topic.queue2的消息:【" + msg + "】"); } }
3.在publisher中编写测试方法,向itcast. topic发送消息
@RunWith(SpringRunner.class) @SpringBootTest public class SpringAmqpTest { /** * topicExchange */ @Test public void testSendTopicExchange() { // 交换机名称 String exchangeName = "itcast.topic"; // 消息 String message = "喜报!孙悟空大战哥斯拉,胜!"; // 发送消息 rabbitTemplate.convertAndSend(exchangeName, "china.news", message); } }
描述下Direct交换机与Topic交换机的差异?
**.**
分割#
:代表0个或多个词*
:代表1个词Spring会将发送的消息序列化为字节发送给MQ,接收消息的时候,还会把字节反序列化为Java对象。
认情况下Spring采用的序列化方式是JDK序列化。众所周知,JDK序列化存在下列问题:
修改消息发送的代码,发送一个Map对象:
@Test
public void testSendMap() throws InterruptedException {
// 准备消息
Map<String,Object> msg = new HashMap<>();
msg.put("name", "柳岩");
msg.put("age", 21);
// 发送消息
rabbitTemplate.convertAndSend("object.queue","", msg);
}
消息接收,用Map接收:
@RabbitListener(queues = "object.queue")
public void listenObjectQueue(Map<String,Object> msg){
System.out.println("消费者接收到object.queue的消息:【" + msg + "】");
}
发送消息后查看控制台:
配置JSON转换器
在publisher和consumer两个服务中都引入依赖:
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.10</version>
</dependency>
在publisher和consumer两个服务的启动类中添加Bean:
@Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。