SpringBoot整合RabbitMQ消息中间件及多种设计模式
1、介绍与安装下载
1.1、不同MQ特点
1.2、下载
1.3、打开网址
RabbitMQ成功运行本地打开网址
账号密码都:guest
2、使用可视化配置
2.1、创建交换机
2.2、创建队列
队列要绑定交换机
删除队列
2.3、来宾用户详情
2.4、创建来宾用户
2.5、用户与虚拟主机进行绑定
2.6、创建虚拟主机
3、五种常用的模式
3.1、导入依赖
<!-- 导入rabbitmq依赖-->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.7.2</version>
</dependency>
3.2、连接工具类
public class RabbitMqUtils {
private static ConnectionFactory factory;
static {
factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setVirtualHost("/ems");
factory.setUsername("ems");
factory.setPassword("ems");
}
/**
* 定义提供连接对象的方法
* @return
*/
public static Connection getConnection() {
try {
Connection connection = factory.newConnection();
return connection;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 关闭连接 注意:一般消费者不用关闭连接 只用于生产者关闭连接
*
* @param channel
* @param connection
*/
public static void closeConnection(Channel channel, Connection connection) {
try {
if (channel != null) {
channel.close();
}
if (connection != null) {
connection.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
3.3、第一种模式(HelloWorld)
//生产消息
public void testSendMessage() throws IOException, TimeoutException {
/*
//创建连接mq的连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置连接rabbitmq主机
connectionFactory.setHost("127.0.0.1");
//设置端口号
connectionFactory.setPort(5672);
//设置连接那个虚拟主机
connectionFactory.setVirtualHost("/ems");
//设置访问虚拟主机的用户名和密码
connectionFactory.setUsername("ems");
connectionFactory.setPassword("ems");
//获取连接对象
Connection connection = connectionFactory.newConnection();
*/
//通过工具类获取连接对象
Connection connection = RabbitMqUtils.getConnection();
//获取连接中通道
Channel channel = connection.createChannel();
//通道绑定对应消息队列(通道中存放的是消息)
//参数1:队列名称,如果队列不存在则自动创建
//参数2:用来定义队列特征是否要持久化
//参数3:是否独占队列(true 就只能被当前连接使用)
//参数4:是否在消费完成后自动删除队列
//参数5:额外附加参数
channel.queueDeclare("hello", false, false, false, null);
//发布消息
//参数1:交换机名称
//参数2:队列的名称
//参数3:传递消息额外设置
//参数4:发布消息时的具体内容
channel.basicPublish("", "hello", null, "hello rabbitmq".getBytes());
/*channel.close();
connection.close();*/
//使用工具类关闭
RabbitMqUtils.closeConnection(channel,connection);
}
//消费者
public static void main(String[] args) throws IOException, TimeoutException {
/*
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/ems");
connectionFactory.setUsername("ems");
connectionFactory.setPassword("ems");
Connection connection = connectionFactory.newConnection();
*/
Connection connection = RabbitMqUtils.getConnection();
Channel channel = connection.createChannel();
//生产者和消费者的参数要严格对应上
channel.queueDeclare("hello", false, false, false, null);
//消费消息
//参数1:消费那个队列的消息,消息名称
//参数2:开始消息的自动确认机制
//参数3:消费时的回调接口
channel.basicConsume("hello", true, new DefaultConsumer(channel) {
//最后一个参数:消息队列中取出的消息
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("new String(body) = " + new String(body));
}
});
//为了保持运行不能关闭
//channel.close();
//connection.close();
}
3.4、第二种模式(work quene)
Work queues,也被称为(Task queues),任务模型。当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。此时就可以使用work模型:让多个消费者绑定到一个队列,共同消费队列中的消息。队列中的消息一旦消费,就会消失,因此任务是不会被重复执行的。
- 开发生产者
public static void main(String[] args) throws IOException {
//获取连接
Connection connection = RabbitMqUtils.getConnection();
//获取通道对象
Channel channel = connection.createChannel();
//通过通道声明队列
channel.queueDeclare("work", true, false, false, null);
for (int i = 0; i < 10; i++) {
//生产消息
channel.basicPublish("", "work", null, (i+"hello work quene").getBytes());
}
//关闭资源
RabbitMqUtils.closeConnection(channel, connection);
}
- 开发消费者-1
public static void main(String[] args) throws IOException {
//获取连接
Connection connection = RabbitMqUtils.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("work", true, false, false, null);
channel.basicConsume("work", true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者-1:" + new String(body));
}
});
}
- 开发消费者-2
public static void main(String[] args) throws IOException {
//获取连接
Connection connection = RabbitMqUtils.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("work", true, false, false, null);
channel.basicConsume("work", true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者-2:" + new String(body));
}
});
}
- 测试结果
总结:默认情况下,RabbitNQ将按顺序将每个消息发送给下一个使用者。平均而言,每个消费者都会收到相同数量的消息。这种分发消息的方式称为循环。
3.4.1、消息确认机制和能者多劳的实现
public class Customer1 {
public static void main(String[] args) throws IOException {
//获取连接
Connection connection = RabbitMqUtils.getConnection();
final Channel channel = connection.createChannel();
//每一次只消费一个消息
channel.basicQos(1);
channel.queueDeclare("work", true, false, false, null);
//参数1:队列名称,参数2:消息自动确认 true 消费者自动向rabbitmq确认消息消费 false 不会自动确认消息(即使被消费了但是队列中没被消费)
channel.basicConsume("work", false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者-1:" + new String(body));
//手动确认 参数1:手动确认消息标识 参数2:false每次确认一个
//参数1:确认队列中那个具体消息,参数2:是否开启多个消息同时确定
channel.basicAck(envelope.getDeliveryTag(), false);
}
});
}
}
public class Customer2 {
public static void main(String[] args) throws IOException {
//获取连接
Connection connection = RabbitMqUtils.getConnection();
final Channel channel = connection.createChannel();
channel.basicQos(1);
channel.queueDeclare("work", true, false, false, null);
channel.basicConsume("work", false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者-2:" + new String(body));
//手动确认 参数1:手动确认消息标识 参数2:false每次确认一个
channel.basicAck(envelope.getDeliveryTag(), false);
}
});
}
}
3.5、第三种模式(fanout)
fanout扇出也称广播
在广播模式下,消息发送流程是这样的:
可以有多个消费者
每个消费者有自己的queue (队列)
每个队列都要绑定到Exchange (交换机)
生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定。
交换机把消息发送给绑定过的所有队列
队列的消费者都能拿到消息。实现一条消息被多个消费者消费
- 开发生产者
public class Provider {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMqUtils.getConnection();
Channel channel = connection.createChannel();
//将通道声名指定交换机 //参数1:交换机名称,参数2:交换机类型 fannout 广播类型
channel.exchangeDeclare("logs","fanout");
//发送消息,交换机名称,路由key此时没任何意义,持久化特性
channel.basicPublish("logs","",null,"fanout type message".getBytes());
RabbitMqUtils.closeConnection(channel,connection);
}
}
- 消费者1、2、3
public class Customer1 {
public static void main(String[] args) throws IOException {
//获取链接对象
Connection connection = RabbitMqUtils.getConnection();
Channel channel = connection.createChannel();
//通道绑定交换机
channel.exchangeDeclare("logs", "fanout");
//临时队列
String queueName = channel.queueDeclare().getQueue();
//绑定交换机和队列
channel.queueBind(queueName, "logs", "");
//消费信息
channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者1:" + new String(body));
}
});
}
}
3.6、第四种模式(Routing)
3.6.1、Routing之订阅模型-Direct(直连)
在Fanout模式中,一条消息,会被所有订阅的队列都消费。但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。
在Direct模型下:
- 队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
- 消息的发送方在向Exchange发送消息时,也必须指定消息的 RoutingKey 。
- Exchange不再把消息交给每一个绑定的队列,而是根据消息的Routing Key进行判断,只有队列的Routing key与消息的Routing key完全一致,才会接收到消息
流程
图解 - P:生产者,向Exchange发送消息,发送消息时,会指定一个routing key。
- X: Exchange (交换机),接收生产者的消息,然后把消息递交给与routing key完全匹配的队列
- c1∶消费者,其所在队列指定了需要routing key 为error的消息
- C2∶消费者,其所在队列指定了需要routing key为info、error、warning 的消息
- 开发生产者
public class Provider {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMqUtils.getConnection();
Channel channel = connection.createChannel();
String exchangeName = "logs_direct";
//通过通道声名交换机 参数1:交换机名称 参数2:direct 路由模式
channel.exchangeDeclare(exchangeName, "direct");
//发送信息
String routingkey = "error";
channel.basicPublish(exchangeName, routingkey, null, ("这是direct模型发布的基于route key:[" + routingkey + "]").getBytes());
//关闭资源
RabbitMqUtils.closeConnection(channel, connection);
}
}
- 消费者1
public class Customer1 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMqUtils.getConnection();
Channel channel = connection.createChannel();
String exchangeName = "logs_direct";
//通道声明交换机以及交换的类型
channel.exchangeDeclare(exchangeName, "direct");
//创建一个临时队列
String queueName = channel.queueDeclare().getQueue();
//基于route key绑定队列和交换机,队列的名字,交换机,路由key
channel.queueBind(queueName, exchangeName, "error");
//获取消费的消息
channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者1:" + new String(body));
}
});
}
}
- 消费者2
public class Customer2 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMqUtils.getConnection();
Channel channel = connection.createChannel();
String exchangeName="logs_direct";
//通道声明交换机以及交换的类型
channel.exchangeDeclare(exchangeName, "direct");
//创建一个临时队列
String queueName = channel.queueDeclare().getQueue();
//基于route key绑定队列和交换机,队列的名字,交换机,路由key
channel.queueBind(queueName, exchangeName, "info");
channel.queueBind(queueName, exchangeName, "error");
channel.queueBind(queueName, exchangeName, "warning");
//获取消费的消息
channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者2:" + new String(body));
}
});
}
}
结果截图
3.7、第五种模式(Topic)
Topic类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key的时候使用通配符!这种模型Routingkey一般都是由一个或多个单词组成,多个单词之间以""分割,例如: item.insert
#统配符
* (star) can substitute for exactly one word.匹配不多不少恰好1个词
# (hash) can substitute for zero or more words.匹配一个或多个词
#如:
audit.# 匹配audit.irs.corporate或者audit.irs等
audit. 只能匹配audit.irs
- 生产者
public class Provider {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMqUtils.getConnection();
Channel channel = connection.createChannel();
//声明交换机以及交换机类型 topic
channel.exchangeDeclare("topics","topic");
//发布消息
String routekey ="user";
channel.basicPublish("topics",routekey,null,("这里是topic动态路由模型,routekey:["+routekey+"]").getBytes());
//关闭资源
RabbitMqUtils.closeConnection(channel,connection);
}
}
- 消费者1
public class Customer1 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMqUtils.getConnection();
Channel channel = connection.createChannel();
//声明交换机以及类型
channel.exchangeDeclare("topics","topic");
//创建一个临时队列
String queue = channel.queueDeclare().getQueue();
//绑定队列和交换机,动态通配符形式route key
channel.queueBind(queue,"topics","*.user.*");
//消费消息
channel.basicConsume(queue,true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者1:"+new String(body));
}
});
}
}
- 消费者2
public class Customer2 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMqUtils.getConnection();
Channel channel = connection.createChannel();
//声明交换机以及类型
channel.exchangeDeclare("topics","topic");
//创建一个临时队列
String queue = channel.queueDeclare().getQueue();
//绑定队列和交换机,动态通配符形式route key
channel.queueBind(queue,"topics","#.user.#");
//消费消息
channel.basicConsume(queue,true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者1:"+new String(body));
}
});
}
}
结果截图
4、SpringBoot整合RabbitMQ
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
配置文件
spring.application.name=rabbitmq-springboot
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=ems
spring.rabbitmq.password=ems
spring.rabbitmq.virtual-host=/ems
4.1、第一种HelloWord
RabbitTemplate 用来简化操作RabbitMQ,使用的时候直接在项目中注入即可
@Component//持久化,非独占,不是自动删除队列
@RabbitListener(queuesToDeclare = @Queue(value = "hello"))
public class HelloCustomer {
//代表从队列中取出消息的回调方法
@RabbitHandler
public void receivel(String message) {
System.out.println("message=" + message);
}
}
测试
@SpringBootTest
class RabbitmqSpringbootApplicationTests {
//注入rabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
//hello world
@Test
void contextLoads() {
//队列的名称,发送的消息
rabbitTemplate.convertAndSend("hello","hello world");
}
}
4.2、第二种Work
生产者
@SpringBootTest
class RabbitmqSpringbootApplicationTests {
//注入rabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
//work,公平分配
@Test
public void testWork() {
for (int i = 0; i < 10; i++) {
rabbitTemplate.convertAndSend("work", "work模型");
}
}
}
消费者
@Component
public class WorkCustomer {
@RabbitListener(queuesToDeclare = @Queue("work"))//可以加载类上和方法上,直接加载方法上,这个方法会处理当前rabbitlistener所监听的队列
public void receivel(String message){
System.out.println("message1 ="+message);
}
@RabbitListener(queuesToDeclare = @Queue("work"))
public void receive2(String message){
System.out.println("message2 ="+message);
}
}
说明:默认在Spring AMOP实现中Work这种方式就是公平调度,如果需要实现能者多劳需要额外配置
4.3、第三种Publish/Subscribe
生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定。
交换机把消息发送给绑定过的所有队列
生产者
@SpringBootTest
class RabbitmqSpringbootApplicationTests {
//注入rabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
//fanout 广播
@Test
public void testFanout(){
//交换机名字,路由key没关系
rabbitTemplate.convertAndSend("logs","","Fanout的模型发送的消息");
}
}
消费者
@Component
public class FanoutCustomer {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,//创建临时队列
exchange = @Exchange(value = "logs", type = "fanout")//绑定的交换机
)
})
public void receive1(String message) {
//先创建一个临时队列绑定交换机
System.out.println("message1 =" + message);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,//创建临时队列
exchange = @Exchange(value = "logs", type = "fanout")//绑定的交换机
)
})
public void receive2(String message) {
//先创建一个临时队列绑定交换机
System.out.println("message2 =" + message);
}
}
4.4、第四种Route
生产者
@SpringBootTest
class RabbitmqSpringbootApplicationTests {
//注入rabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
//route路由模式
@Test
public void testRoute(){
//交换机名字,路由的key
rabbitTemplate.convertAndSend("directs","info","发送info的key的路由信息");
}
}
消费者
@Component
public class RouteCustomer {
/**
* 先创建一个临时队列绑定交换机
* @param message
*/
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,//创建临时队列
exchange = @Exchange(value = "directs", type = "direct"),//自定义交换机名称和类型,类型默认direct
key = {"info", "error", "warn"}
)
})
public void receive1(String message) {
System.out.println("message1 =" + message);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,//创建临时队列
exchange = @Exchange(value = "directs", type = "direct"),//自定义交换机名称和类型,类型默认direct
key = {"error",}
)
})
public void receive2(String message) {
System.out.println("message2 =" + message);
}
}
效果图
4.5、第五种Route
生产者
@SpringBootTest
class RabbitmqSpringbootApplicationTests {
//注入rabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
//topic 动态路由 订阅模式
@Test
public void testTopic(){
//交换机名字,路由的key
rabbitTemplate.convertAndSend("topics","user.save","user.save 路由消息");
rabbitTemplate.convertAndSend("topics","user.save.delete","user.save.delete 路由消息");
rabbitTemplate.convertAndSend("topics","order.save","order.save 路由消息");
rabbitTemplate.convertAndSend("topics","order.save.delete","order.save.delete 路由消息");
}
}
消费者
@Component
public class TopicCustomer {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,
exchange = @Exchange(type = "topic",name = "topics"),
key = {"user.save","user.*"}
)
})
public void receive1(String message){
System.out.println("message1 =" + message);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,
exchange = @Exchange(type = "topic",name = "topics"),
key = {"order.#","produce.#","user.*"}
)
})
public void receive2(String message){
System.out.println("message2 =" + message);
}
}
效果图
5、MQ的应用场景
5.1、异步处理
5.2、应用解耦
5.3、流量削峰
6、集群架构
6.1、普通集群
默i认情况下:RabitK0代理操作所需的所有数据/状态都将跨所有节点复制。这方面的一个例外是消息队列,默认情况下,消息队列位于一个节点上,尽管它们可以从所有节点看到和访问
架构图(有点像主备架构)
核心解决问题:当集群中某一时刻master节点宕机,可以对Quene中信息,进行备份
集群搭建
# 1.集群规划
node1 : 10.15.0.3 mq1 master 主节点
node2 : 10.15.0.4 mq2 repl1 副本节点
node3 : 10.15.8.5 mq3 repl2 副本节点
# 2.克隆三台机器主机名和ip映射(三台机器都要)
vim /etc/hosts加入:
10.15.8.3 mq1
18.15.0.4 mq2
10.15.0.5 mq3
node1 : vim letc/hostname 加入:mq1
node2 : vim /etc/hostname 加入:mq2
node3 : vim /etc/hostname 加入:mq3
# 3.三个机器安装rabbitmq,并同步cookie文件,在node1上执行:
scp /var/lib/rabbitmq/.erlang.cookie root@mq2 : / var/lib/rabbitmq/
scp /var/lib/rabbitmq/.erlang.cookie root@mq3 : / var/lib/rabbitmq/
# 4.后台启动rabbitmq所有节点执行如下命令,启动成功访问管理界面:
rabbitmq-server -detached
# 5.在node2和node3执行加入集群命令:
1.关闭 rabbitmqctl stop_app
2.加入集群 rabbitmqctl join_cluster rabbitemq1
3.启动服务 rabbitmqctl start_app
# 6.查看集群状态,任意节点执行:
rabbitmqctl cluster_status
# 7.如果出现如下显示,集群搭建成功:
Cluster status of node rabbit@mq3 ...
[ {nodes , [ idisc , [ rabbit@mq1 , rabbit@mq2 , rabbit@mq3] 》]}.
frunning_nodes , [ rabbit@mq1 , rabbitemq2 , rabbit@mq3 ]},
{cluster_name , <<"rabbit@mq1">>},
{partitions, [ ]},
{alarms, [ {rabbit@mq1 , []} , { rabbit@mq2,[ ]} , {rabbit@mq3,[]}H]
安装rabbitmq
(修改rabbitmq.config61行取消对{handshake_timeout,10000}}的注释,去掉行结尾逗号)
# 7.执行如下命令,启动rabbitmq中的插件管理
rabbitmq-plugins enable rabbitmq_management
#8.启动RabbitMQ的服务
systemctl start rabbitmq-server
systemctl restart rabbitmq-server
systemctl stop rabbitmq-server
安装包
集群的搭建
6.2、镜像集群(用的多)
解决了高可用,把主队列信息同步到不同的rabbit服务器
镝像队刭列机制就是将队列在三个节点之间设置主从关系,消息会在三个节点之间进行自动同步,且如果其中一个节点不可用,并不会导致消息丢失或服务不可用的情况,提升NO集群的整体高可用性。