一,引言
- 为什么引入rabbitMQ:
模块之间的耦合度多高,导致一个模块宕机后,全部功能都不能用了,(同步操作存在问题)
并且同步通讯的成本过高,用户体验差。(多浪费时间)
上图的解释:1》我们每一个通知都是同步操作的,就是说你在通知缓存模块是就要等他回复你后才能进行下一步的操作。同样就会出现这样一个问题就是你在通知缓存模块时他宕机了,客户端就要等待你好了给我一个结果才能静一步操作。2》就算你每一个都成功了你本来要0.1秒就能通知三个模块,而上面的springboot模式就要每一个都话费0.2秒,多话费0.6秒。rabbitmq就可以解决这些问题。
2.rabbitmq的架构图
-
Publisher - 生产者:发布消息到RabbitMQ中的Exchange
-
Consumer - 消费者:监听RabbitMQ中的Queue中的消息
-
Exchange - 交换机:和生产者建立连接并接收生产者的消息
-
Queue - 队列:Exchange会将消息分发到指定的Queue,Queue和消费者进行交互
-
Routes - 路由:交换机以什么样的策略将消息发布到Queue
二、RabbitMQ安装(linux系统下)
-
1。RabbitMQ和之前的radis,nginx的安装方式一样,在docker环境下安装.
》opt目录下下创建mkdir docker_RabbitMQ文件夹,进入文件夹编写yml、文件(如下)》docker-compose -d启动即可
version: "3.1"
services:
rabbitmq:
image: daocloud.io/library/rabbitmq:management
restart: always
container_name: rabbitmq
ports:
- 5672:5672
- 15672:15672
volumes:
- ./data:/var/lib/rabbitmq
- 2。查看图形化界面并创建一个Virtual Host
》登录图形化界面:我的linux ip:15672【首次登录的用户名密码是quest】
创建一个自己的超级管理员
步骤:admin->add a user->指定为超级管理员->添加后给该用户添加virtual host(右侧user下拉框)->点击创建好的用户去管理virtual host (只要写这个host的名字;记得前面加 / 后边两个框不要填)
三,RabbitMQ的通讯方式(五种)
- 首先连接导入依赖创建连接
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.6.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
创建连接
@Test
public static Connection getConnection() {
ConnectionFactory factory=new ConnectionFactory();//connection工厂对象
factory.setHost("192.168.32.137");//rabbitmq的地址
factory.setUsername("wjl");//扥路名
factory.setPassword("root");//密码
factory.setPort(5672);//端口号
factory.setVirtualHost("/wjl");//虚拟机
Connection connection=null;
try {
connection=factory.newConnection();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
// System.in.read();
return connection;
}
一定要注意导包是这几个
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import connect.RabbitMQConnection;
import org.junit.Test;
import java.io.IOException;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.TimeoutException;
1. Hello-World
(一个信息给一个消费者)
一个生产者,一个默认的交换机,一个队列,一个消费者
@Test
public void publish() throws Exception {
//1. 获取Connection
Connection connection = RabbitMQClient.getConnection();
//2. 创建Channel
Channel channel = connection.createChannel();
//3. 发布消息到exchange,同时指定路由的规则
String msg = "Hello-World!";
// 参数1:指定exchange,使用""。
// 参数2:指定路由的规则,使用具体的队列名称。
// 参数3:指定传递的消息所携带的properties,使用null。
// 参数4:指定发布的具体消息,byte[]类型
channel.basicPublish("","HelloWorld",null,msg.getBytes());
// Ps:exchange是不会帮你将消息持久化到本地的,Queue才会帮你持久化消息。
System.out.println("生产者发布消息成功!");
//4. 释放资源
channel.close();
connection.close();
}
@Test
public void consume() throws Exception {
//1. 获取连接对象
Connection connection = RabbitMQClient.getConnection();
//2. 创建channel
Channel channel = connection.createChannel();
//3. 声明队列-HelloWorld
//参数1:queue - 指定队列的名称
//参数2:durable - 当前队列是否需要持久化(true)
//参数3:exclusive - 是否排外(conn.close() - 当前队列会被自动删除,当前队列只能被一个消费者消费)
//参数4:autoDelete - 如果这个队列没有消费者在消费,队列自动删除
//参数5:arguments - 指定当前队列的其他信息
channel.queueDeclare("HelloWorld",true,false,false,null);
//4. 开启监听Queue
DefaultConsumer consume = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("接收到消息:" + new String(body,"UTF-8"));
}
};
//参数1:queue - 指定消费哪个队列
//参数2:autoAck - 指定是否自动ACK (true,接收到消息后,会立即告诉RabbitMQ)
//参数3:consumer - 指定消费回调
channel.basicConsume("HelloWorld",true,consume);
System.out.println("消费者开始监听队列!");
// System.in.read();
System.in.read();
//5. 释放资源
channel.close();
connection.close();
}
Work
(一个对列的多条信息平均分给多个消费者,但是可以设置不平分)
一个生产者,一个默认的交换机,一个队列,两个消费者
和helloworld方式不同的地方》消费者指定Qoa(设置消费者一次消费多少信息)和手动ack(false)。
手动ACK的范式,修改默认的平均分配:重写的方法内部添加手动ACK
//设置消费者一次消费多少信息(设置这个要改为手动ACK为false)
channel.basicQos(1);
//监听之前
try {
Thread.sleep(200);//通过设置睡眠时间分配消费比例(消费能力 根据睡眠时间设置一个设100,一个设200)
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("接收到消息:" + new String(body, "UTF-8"));
//手动ACK
channel.basicAck(envelope.getDeliveryTag(),false);
//监听方法的参数改为false
channel.basicConsume("work", false, consume);
Publish/Subscribe
(一个交换机的10条信息共享给两个消费者一人10条=开黑语音)
一个生产者,一个交换机,两个队列,两个消费者
和helloworld方式不同的地方》
声明一个Fanout类型的exchange,并且将exchange和queue绑定在一起,绑定的方式就是直接绑定。–使用的queueBind方法
让生产者创建一个exchange并且指定类型,和一个或多个队列绑定到一起。
//3. 创建exchange - 绑定某一个队列
//参数1: exchange的名称
//参数2: 指定exchange的类型 FANOUT - pubsub , DIRECT - Routing , TOPIC - Topics
channel.exchangeDeclare("pubsub-exchange", BuiltinExchangeType.FANOUT);
channel.queueBind("pubsub-queue1","pubsub-exchange","");
channel.queueBind("pubsub-queue2","pubsub-exchange","");
Routing
(路由类型:通过路由去过滤符合该路由规则(队列名)的发布信息)
发布信息是生产者行为。
一个生产者,一个交换机,两个队列,两个消费者
和helloworld方式不同的地方》
生产者在创建DIRECT类型的exchange后,根据RoutingKey去绑定相应的队列,并且在发送消息时,指定消息的具体RoutingKey即可。–使用的queueBind加basicPublish方法。
// 指定交换机名称(s2:表示过滤类型)
channel.queueBind("routing-queue-1","routing-exchange","hello");
channel.queueBind("routing-queue-2","routing-exchange","haha");
//4. 发布消息到exchange,同时指定路由的规则
// String msg = "我是Subscribe数据!"+i;
// 参数1:指定exchange,使用""。
// 参数2:指定路由的规则,使用具体的队列名称。也指RoutingKey
// 参数3:指定传递的消息所携带的properties,使用null。
// 参数4:指定发布的具体消息,byte[]类型
channel.basicPublish("routing-exchange","hello",null,"hello".getBytes());
channel.basicPublish("routing-exchange","haha",null,"hahahahha".getBytes());
channel.basicPublish("routing-exchange","hello",null,"hello".getBytes());
channel.basicPublish("routing-exchange","haha",null,"hahahahha".getBytes());
channel.basicPublish("routing-exchange","hello",null,"hello".getBytes());
channel.basicPublish("routing-exchange","haha",null,"hahahahha".getBytes());
Topic
(根据匹配规则过滤,符合该路由规则(队列名)的所有RoutingKey内容==只关注与一个对象的某个属性)
一个生产者,一个交换机,两个队列,两个消费者
和helloworld方式不同的地方》
生产者创建Topic的exchange并且绑定到队列中,这次绑定可以通过*和#关键字,对指定RoutingKey内容,编写时注意格式 xxx.xxx.xxx去编写, * -> 一个xxx,而# -> 代表多个xxx.xxx,在发送消息时,指定具体的RoutingKey到底是什么。
channel.exchangeDeclare("topics-exchange", BuiltinExchangeType.TOPIC);
//动物的信息<speed><color><what>
//*.red.* *:占位符代表一个xxx
// dog.# #:通配符代表多个xxx.xxx
// 3.1 指定交换机名称
channel.queueBind("topics-queue1","topics-exchange","*.red.*");
channel.queueBind("topics-queue2","topics-exchange","dog.#");
// 4,将交换机绑定队列
channel.basicPublish("topics-exchange","rabbit.red.apeed",null,"红快兔子".getBytes());
channel.basicPublish("topics-exchange","dog.red.slow",null,"大黑狗".getBytes());
channel.basicPublish("topics-exchange","dog.black.slow",null,"大狼狗".getBytes());
channel.basicPublish("topics-exchange","monkey.red.apeed",null,"大红土".getBytes());
三,RabbitMQ整合SpringBoot【重点
】
6.1 SpringBoot整合RabbitMQ
6.1.1 创建SpringBoot工程
6.1.2 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
6.1.3 编写配置文件
spring:
rabbitmq:
host: 192.168.32.137
port: 5672
username: wjl
password: root
virtual-host: /wjl
6.1.4 声明exchange、queue
创建config文件(配置spring容器把交换机和队列放入其中)
package com.example.bootrabbitmq.config;
import com.sun.org.apache.regexp.internal.RE;
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 RabbitMQConfig {
//注入交换机
@Bean//用topic是因为这样的方式是最为灵活的
public TopicExchange gettTopicExchange() {//交换机的name尽量不要和之前的重复(冲突)
return new TopicExchange("boot-topic-exchange", true, false);
}
// 注入队列
@Bean
public Queue getQueue() {
return new Queue("boot-topic-queue", true, false, false);
}
//交换机和队列绑定在一起
@Bean
public Binding getBinding(Queue queue, TopicExchange topicExchange) {
return BindingBuilder.bind(queue).to(topicExchange).with("*.black.*");//因为是采用topic的方式,所有要绑定好信息参数。
}
}
6.1.5 发布消息到RabbitMQ
@SpringBootTest
class BootRabbitmqApplicationTests {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test//测试的范式创建一个生产者发一个消息
void contextLoads() throws IOException {//指定交换机,key,和信息
rabbitTemplate.convertAndSend("boot-topic-exchange","dog.black.speed","大黑狗泡的快");
System.in.read();//消息发送成功会放到监控界面的队列中,等待消费者去消费
}
}
6.1.6 创建消费者监听消息
@Component
@Component
public class TestCustomer {
@RabbitListener(queues = "boot-topic-queue")//监听界面,要拿到创建的队列
public void customer(String msg, Channel channel, Message message) throws IOException {
System.out.println("我是消费者,我收到信息了:" + msg);
// 手动Ack - 指定是否自动ACK (true,接收到消息后,会立即告诉RabbitMQ)
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
}
6.2 手动Ack(就是消费完之后再告诉RabbitMQ而不是刚消费一个消息自动的就告诉他了,这样如果存在异常,消费者消费失败但rabbitmq却是显示消费成功)
6.2.1 添加配置文件
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: manual
6.2.2 手动ack
@RabbitListener(queues = "boot-queue")
public void getMessage(String msg, Channel channel, Message message) throws IOException {
System.out.println("接收到消息:" + msg);
int i = 1 / 0;
// 手动ack
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
》》》手动ACK的模式下:
没有回馈消息时,在你消费者消费过后但是没有告诉rubbitmq所以队列里面还是有个消息在等待消费
添加了回馈消息之后就没有消息等待消费。 【 加了channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);】
四,rabbitMQ的其他操作(事务)
就是解决生产者和rabbitMq之间的问题。
》1.如果消息已经到达了RabbitMQ,但是RabbitMQ岩机了,消息是不是就丢了?
不会RabbitMQ得到Queue有持久化机制。
》2消责者在消赛消息时,如果执行一半。消费者宕机了怎么办?
手动ACK.
》3.生产者发送消息时,由于网络问题,导致消息没发送到RabbitMQ?,生产者认为他发送了但是消费者却没收到!
RabbitMQ提供了事务操作,和Confirm,httpst
1 消息的可靠性采用confirm机制
RabbitMQ的事务:事务可以保证消息100%传递,可以通过事务的回滚去记录日志,后面定时再次发送当前消息。事务的操作,效率太低,加了事务操作后,比平时的操作效率至少要慢100倍。
RabbitMQ除了事务,还提供了Confirm的确认机制,这个效率比事务高很多
2 消息的可靠性采用return机制
Confirm只能保证消息到达exchange,无法保证消息可以被exchange分发到指定queue。
而且exchange是不能持久化消息的,queue是可以持久化消息。
采用Return机制来监听消息是否从exchange送到了指定的queue中
五消息的重复消费问题
产生重复消费的原因:当两个消费者去消费一个消息时,先给消费者一,但是没有给一消费者一,一个ACK,rabbitMQ就认为这个消息没有消费成功,这样他就会吧消息在给消费者2去消费。
重复消费消息,会对非幂等行操作造成问题
重复消费消息的原因是,消费者没有给RabbitMQ一个ack
为了解决消息重复消费的问题,可以采用Redis,在消费者消费消息之前,现将消息的id放到Redis中,
id-0(正在执行业务)
id-1(执行业务成功)
如果ack失败,在RabbitMQ将消息交给其他的消费者时,先执行setnx,如果key已经存在,获取他的值,如果是0,当前消费者就什么都不做,如果是1,直接ack。
极端情况:第一个消费者在执行业务时,出现了死锁,在setnx的基础上,再给key设置一个生存时间。
生产者,发送消息时,指定messageId