初始MQ
同步调用
同步调用的优点:
时效性较强,可以立即得到结果
同步调用的问题:
耦合度高:每次加入新的需求,都要修改原来的代码
性能和吞吐能力下降:调用者需要等待服务提供者响应,如果调用链过长则响应时间等于每次调用的时间之和。
有额外的资源消耗:调用链中的每个服务在等待响应过程中,不能释放请求占用的资源,高并发场景下会极度浪费系统资源
有级联失败问题:如果服务提供者出现问题,所有调用方都会跟着出问题,如同多米诺骨牌一样,迅速导致整个微服务群故障
异步调用
异步通信的优点:
耦合度低
吞吐量提升
故障隔离
流量削峰
异步通信的缺点:
依赖于Broker的可靠性、安全性、吞吐能力
架构复杂了,业务没有明显的流程线,不好追踪管理
有哪些MQ?
RabbitMQ
下载安装
通过命令安装RabbitMQ:
# 通过docker拉取镜像
docker pull rabbitmq:3-management
# 运行容器
docker run \
-e RABBITMQ_DEFAULT_USER=mymq \
-e RABBITMQ_DEFAULT_PASS=123456 \
--name mq \
--hostname mq1 \
-p 15672:15672 \
-p 5672:5672 \
-d \
rabbitmq:3-management
成功运行RabbitMQ后,通过浏览器输入192.168.153.100:15672访问RabbitMQ,登录后如下:
在user界面可以新增用户与虚拟主机
建议每个用户单独配置一个虚拟主机,彼此隔离
RabbitMQ中的几个概念:
channel:操作MQ的工具
exchange:路由消息到队列中
queue:缓存消息
virtual host:虚拟主机,是对queue、exchange等资源的逻辑分组
helloworld案例
导入资料中的案例,部分代码如下:
public class ConsumerTest {
public static void main(String[] args) throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("192.168.153.100");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("mymq");
factory.setPassword("123456");
// 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页面,可以看到connection,channel和queue都已被成功创建
控制台输出如下:
总结
基本消息队列的消息发送流程:
1.建立connection
2.创建channel
3.利用channel声明队列
4.利用channel向队列发送消息
基本消息队列的消息接收流程:
1.建立connection
2.创建channel
3.利用channel声明队列
4.定义consumer的消费行为handleDelivery()
5.利用channel将消费者与队列绑定
SpringAMQP
入门案例
1.引入依赖
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2.配置RabbMQ
在publisher中的application.yml中写入以下配置
spring:
rabbitmq:
host: 192.168.153.100 #rabbitMQ的ip地址
port: 5672 #端口
username: mymq
password: 123456
virtual-host: "/"
3.建立测试方法
@RunWith(SpringRunner.class)
@SpringBootTest
@EnableRabbit
public class SpringAmqpTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSendMessage2SimpleQueue() {
String queueName = "simple.queue";
String message = "hello spring amqp";
rabbitTemplate.convertAndSend(queueName,message);
}
}
提示,这个方法不能自己创建队列,所以需要提前创建一个队列,才能接收到消息,可以运行之前的publisher方法
4.编写consumer配置
在consumer中的application.yml中写入以下配置
spring:
rabbitmq:
host: 192.168.153.100 #rabbitMQ的ip地址
port: 5672 #端口
username: mymq
password: 123456
virtual-host: "/"
5.编写消费逻辑
在consumer中新建一个listener类
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueue(String msg){
System.out.println("消费者接收到simple.queue的消息:【" + msg + "】");
}
}
启动后成功在控制台中接收到消息
总结
SpringAMQP如何发送消息?
引入amqp的starter依赖
配置RabbitMQ地址
利用RabbitTemplate的convertAndSend方法
SpringAMQP如何接收消息?
引入amqp的starter依赖
配置RabbitMQ地址
定义类,添加@Component注解
类中声明方法,添加@RabbitListener注解,方法参数就时消息
注意:消息一旦消费就会从队列删除,RabbitMQ没有消息回溯功能
WorkQueue工作队列
模拟WorkQueue,实现一个队列绑定多个消费者
1.在publisher服务中添加一个测试方法,循环发送50条消息到simple.queue队列
@Test
public void testSendMessage2WorkQueue() throws InterruptedException {
String queueName = "simple.queue";
String message = "hello spring amqp__";
for (int i = 1; i <= 50; i++) {
rabbitTemplate.convertAndSend(queueName,message + i);
Thread.sleep(20);
}
}
2.在consumer服务中添加一个消费者,也监听simple.queue:
@RabbitListener(queues = "simple.queue")
public void listenWorkQueue1(String msg) throws InterruptedException {
System.out.println("消费者1接收到消息:【" + msg + "】" + LocalDateTime.now());
Thread.sleep(20);
}
@RabbitListener(queues = "simple.queue")
public void listenWorkQueue2(String msg) throws InterruptedException {
System.err.println("消费者2......接收到消息:【" + msg + "】" + LocalDateTime.now());
Thread.sleep(200);
}
运行后发现,工作效率较慢的消费者2拿到了和消费者1一样多的消息,导致整体消息处理完成时间过长。
原因是消息预取机制的存在。这个机制让消费者1和消费者2提前拿到了一样多的消息。
我们可以在配置中修改最大消息预取数来提高工作队列的效率
spring:
rabbitmq:
host: 192.168.153.100 #rabbitMQ的ip地址
port: 5672 #端口
username: mymq
password: 123456
virtual-host: "/"
listener:
simple:
prefetch: 1
将最大预取数改为1,就可以实现效率高的消费者1多拿到消息,而消费者2少拿到消息了。
FanoutExchange
1.声明队列,交换机并互相绑定
新建一个FanoutConfig类
@Configuration
public class FanoutConfig {
//my.fanout
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("my.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);
}
}
2.编写消费者方法
编写两个消费者方法分别监听两个队列
@RabbitListener(queues = "fanout.queue1")
public void listenFanoutQueue1(String msg){
System.out.println("消费者接收到fanout.queue1的消息:【" + msg + "】");
}
@RabbitListener(queues = "fanout.queue2")
public void listenFanoutQueue2(String msg){
System.out.println("消费者接收到fanout.queue2的消息:【" + msg + "】");
}
3.编写测试方法发送消息
@Test
public void testSendFanoutExchange() {
//交换机名称
String exchangeName = "my.fanout";
//消息
String msg = "hello every one!";
//发送消息
rabbitTemplate.convertAndSend(exchangeName,"",msg);
}
然后启动程序,可在RabbitMQ Management中看到fanout交换机已成功绑定两个队列
在控制台也可看到如下结果
总结
DirectExchange
1.实现directexchange
在consumer服务中,编写两个消费者方法,分别监听direct.queue1和direct.queue2, 并利用@RabbitListener声明Exchange、Queue、RoutingKey
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue1"),
exchange = @Exchange(name = "my.direct", type = ExchangeTypes.DIRECT),
key = {"red","blue"}
))
public void listenDirectQueue1(String msg){
System.out.println("消费者接收到direct.queue1的消息:【" + msg + "】");
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue2"),
exchange = @Exchange(name = "my.direct", type = ExchangeTypes.DIRECT),
key = {"red","yellow"}
))
public void listenDirectQueue2(String msg){
System.out.println("消费者接收到direct.queue2的消息:【" + msg + "】");
}
2.编写测试方法
@Test
public void testSendDirectExchange() {
//交换机名称
String exchangeName = "my.direct";
//消息
String msg = "hello red!";
//发送消息
rabbitTemplate.convertAndSend(exchangeName,"red",msg);
}
3.查看结果
在rabbit网站的exchange中已经能看到my.direct
绑定关系如下
启动测试方法后控制台如下:
总结
TopicExchange
1.声明listener
在consumer服务中,编写两个消费者方法,分别监听topic.queue1和topic.queue2, 并利用@RabbitListener声明Exchange、Queue、RoutingKey
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue1"),
exchange = @Exchange(name = "my.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 = "my.topic", type = ExchangeTypes.TOPIC),
key = "#.news"
))
public void listenTopicQueue2(String msg){
System.out.println("消费者接收到topic.queue2的消息:【" + msg + "】");
}
2.编写测试方法
@Test
public void testSendTopicExchange() {
//交换机名称
String exchangeName = "my.topic";
//消息
String msg = "今天天气不错!";
//发送消息
rabbitTemplate.convertAndSend(exchangeName,"china.weather",msg);
}
3.结果
网站中队列绑定成功
启动测试方法后控制台输出如下
消息转换器
在SpringAMQP的发送方法中,接收消息的类型是Object,也就是说我们可以发送任意对象类型的消息,SpringAMQP会帮我们序列化为字节后发送。这就是消息转换器的作用
下面测试一下发送object类型的消息
在consumer中声明一个队列
@Bean
public Queue objectQueue(){
return new Queue("object.queue");
}
然后在publisher中发送消息以测试
@Test
public void testSendObjectQueue() {
Map<String, Object> msg = new HashMap<>();
msg.put("name", "123");
msg.put("age", 16);
rabbitTemplate.convertAndSend("object.queue",msg);
}
rabbitmq management查看消息如下:
出现这种情况的原因是因为Spring的对消息对象的处理是由org.springframework.amqp.support.converter.MessageConverter来处理的。而默认实现是SimpleMessageConverter,基于JDK的ObjectOutputStream完成序列化。
如果要修改只需要定义一个MessageConverter 类型的Bean即可。推荐用JSON方式序列化,步骤如下:
引入依赖:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
在publisher服务中声明MessageConverter:
@Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
在consumer中也同样声明MessageConverter
然后定义一个消费者,监听object.queue队列并消费消息:
@RabbitListener(queues = "object.queue")
public void listenObjectQueue(Map<String, Object> msg) {
System.out.println("收到消息:【" + msg + "】");
}
最后启动服务,在控制台接收消息如下:
说明修改成功
总结
SpringAMQP中消息的序列化和反序列化是怎么实现的?
利用MessageConverter实现的,默认是JDK的序列化
注意发送方与接收方必须使用相同的MessageConverter