- RabbitMq、ActiveMq、RocketMq、kafaka
-
应用解耦
订单系统写入消息队列,然后返回用户下单成功,
库存系统去订阅消息,进行入库 -
流量销峰
应用场景:秒杀活动,一般会因为流量过大,导致流量暴增,应用挂掉。为解决这个问题,一般需要在应用前端加入消息队列。用户的请求,服务器接收后,首先写入消息队列。假如消息队列长度超过最大数量,则直接抛弃用户请求或跳转到 错误页面
秒杀业务根据消息队列中的请求信息,再做后续处理
通过消息队列缓解服务器压力. -
异步处理
RocketMq:
通信协议【TCP/HTTP】从SDK中找到
架构图:
在实际的部署过程中,Broker是实际存储消息的数据节点,Nameserver则是服务发现节点,Producer发送消息到某一个Topic,并给到某个Consumer用于消费的过程中,需要先请求Nameserver拿到这个Topic的路由信息,即Topic在哪些Broker上有,每个Broker上有哪些队列,拿到这些请求后再把消息发送到Broker中;相对的,Consumer在消费的时候,也会经历这个流程。
https://www.jianshu.com/p/95ab928960b3
https://www.cnblogs.com/linlinismine/p/9184073.html
topic下默认为4个队列:
轮询监控队列
-
NameServer是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。
-
Broker部署相对复杂,Broker分为Master与Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave的对应关系通过指定相同的BrokerName,不同的BrokerId来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。每个Broker与NameServer集群中的所有节点建立长连接,定时注册Topic信息到所有NameServer。
-
Producer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer取Topic路由信息,并向提供Topic服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署,天然支持集群。
-
Consumer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,订阅规则由Broker配置决定,天然支持集群。
- 消息读写:从文件到客户端
消息存储:
刷盘机制:
同步【在分布式系统中,所有的Broker消息都刷盘完,才通知发送消息已经成功】 、异步
RocketMq : 如何保证消息不被丢失(消息可靠性)
三处丢失
Producer : 同步发送、异步回调、重试机制。
Borker:刷盘策略,同步刷盘、异步刷盘。集群化部署
Consumer: 手动确认机制。
分布式事务:事务消息
在本地事务中睡眠一定时间,消息回查会1min回查一次。上图可以看到最后一次回查成功了。
集群模式、广播模式消费:
指定一个类型就OK 了。
顺序消费:
将消息发往同一个Broker下的同一个Topic下的同一个队列中,因为顺序消费只能保证局部顺序,不能保证全局消费。
消费者不变:
消息堆积问题:
推拉方式:
一般是push模式。
消息确认机制:
RabbitMQ:
1.rabbitMQ监控系统的端口:15672
2.rabbitMQ服务调用端口:5672
1.0 如何保证消息队列的顺序执行
场景:
由于拿到每个数据后,消费每个数据的速度不一样,导致了入数据库的数据顺序有问题。
解决方案:(RabbitmQ)
1、多个队列将消息往一个队列发
2、消费者内部采用内存队列来消费
解决方案(ActiveMq的):
有消息组
https://blog.csdn.net/YAOQINGGG/article/details/82563304
2.0如何保证消息不丢失?
三处消息丢失:
生产者:confirm机制。
rabbitmq:开启持久化 2处 元数据 和 队列数据。
消费者:手动ack告诉rabbitmq 手动确认机制。
一:rabbitmq的事务,阻塞的,回滚,然后重试;confirm机制,消息队列回传你ack确认(针对提供者断线了导致的数据丢失),如果没能收到会回调你nack接口告诉你失败了
二:持久化数据开启(针对ActiveMq挂了)
三:消费者采取手动确认机制 消费结束的时候手动ack 到消息队列。
参考:https://www.cnblogs.com/756623607-zhang/p/10507267.html
3.0如何处理消费者消费失败?
加入一个死信队列,后台启动线程重新消费
4.0 如何保证消息队列消息不被重复消费
答案:一般消息队列都有自己的确认机制(正常情况下)。如果网络原因导致消费端没有确认到消息队列,也就是队列没有收到消息,那就导致了重复消费。
如何解决?
这个问题针对业务场景来答,分以下三种情况:
(1)比如,你拿到这个消息做数据库的insert操作,那就容易了,给这个消息做一个唯一的主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。
(2)再比如,你拿到这个消息做redis的set的操作,那就容易了,不用解决,因为你无论set几次结果都是一样的,set操作本来就算幂等操作。
(3)如果上面两种情况还不行,上大招。准备一个第三方介质,来做消费记录。以redis为例,给消息分配一个全局id,只要消费过该消息,将<id,message>以K-V形式写入redis.那消费者开始消费前,先去redis中查询有没有消费记录即可。
消息队列模式:
1.简单模式
工作原理:
当客户端(生产者)将消息写入消息队列中时,消息队列中信息的数量加1.
消费者实时监听消息队列,当消息队列中有消息时,则获取消息,之后执行业务逻辑.同时消息队列的数量减一
实现:
<!-- 消息队列 -->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>3.5.1</version>
</dependency>
消息生产者
public class Test_1_simple_provider {
@Test
public void provider() throws Exception
{
//1,建立连接
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.216.202");
factory.setPort(5672);
factory.setUsername("jtadmin");
factory.setPassword("jtadmin");
factory.setVirtualHost("/jt");
Connection connection=factory.newConnection();
//2,建立通道
Channel channel=connection.createChannel();
//3,定义队列
//durable true 持久化,重启服务器后,数据还有
//exclusive true,只能通过当前连接消费 false
//autoDelete true 队列中消息处理完后,自动删除队列
//arguments 参数
channel.queueDeclare("orderQueue", true, false, false, null);
//4,发送消息,routingKey必须与queue一致
String msg="msg1";
channel.basicPublish("", "orderQueue", null, msg.getBytes());
//5,关闭
channel.close();
connection.close();
System.out.println("发送数据成功");
}
}
消息消费者:
public class Test_1_simple_consumer {
@Test
public void consumer() throws Exception
{
//1,建立连接
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.216.202");
factory.setPort(5672);
factory.setUsername("jtadmin");
factory.setPassword("jtadmin");
factory.setVirtualHost("/jt");
Connection connection=factory.newConnection();
//2,建立通道
Channel channel=connection.createChannel();
//3,定义队列
//durable true 持久化,重启服务器后,数据还有
//exclusive true,只能通过当前连接消费 false
//autoDelete true 队列中消息处理完后,自动删除队列
//arguments 参数
channel.queueDeclare("orderQueue", true, false, false, null);
//4,创建消费者
QueueingConsumer consumer=new QueueingConsumer(channel);
//autoAck:自动回复消息
channel.basicConsume("orderQueue", true, consumer);
//5,取消息
while(true)
{
Delivery delivery=consumer.nextDelivery();
byte[] data=delivery.getBody();
String mString=new String(data);
System.out.println("消费者取到:"+mString);
}
}
}
2.工作模式
说明:
由一个生产者负责消息写入队列,但是如果有一个消费者负责消费,可能会造成消息的积压.所以准备多个消费者共同消费一个队列中的消息.
实现:
生产者:
public class Test_2_work_provider {
@Test
public void provider() throws Exception {
// 1,建立连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.216.202");
factory.setPort(5672);
factory.setUsername("jtadmin");
factory.setPassword("jtadmin");
factory.setVirtualHost("/jt");
Connection connection = factory.newConnection();
// 2,建立通道
Channel channel = connection.createChannel();
// 3,定义队列
// durable true 持久化,重启服务器后,数据还有
// exclusive true,只能通过当前连接消费 false
// autoDelete true 队列中消息处理完后,自动删除队列
// arguments 参数
channel.queueDeclare("orderQueue", true, false, false, null);
// 4,发送消息
for (int i = 3; i < 10; i++) {
String msg = "msg" + i;
channel.basicPublish("", "orderQueue", null, msg.getBytes());
}
// 5,关闭
channel.close();
connection.close();
System.out.println("发送数据成功");
}
}
消费者1
public class Test_2_work_consumer1 {
@Test
public void consumer() throws Exception
{
//1,建立连接
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.216.202");
factory.setPort(5672);
factory.setUsername("jtadmin");
factory.setPassword("jtadmin");
factory.setVirtualHost("/jt");
Connection connection=factory.newConnection();
//2,建立通道
Channel channel=connection.createChannel();
//3,定义队列
//durable true 持久化,重启服务器后,数据还有
//exclusive true,只能通过当前连接消费 false
//autoDelete true 队列中消息处理完后,自动删除队列
//arguments 参数
channel.queueDeclare("orderQueue", true, false, false, null);
//4,创建消费者
QueueingConsumer consumer=new QueueingConsumer(channel);
//autoAck:自动回复消息
channel.basicConsume("orderQueue", true, consumer);
//5,取消息
System.out.println("消费者1启动");
while(true)
{
Delivery delivery=consumer.nextDelivery();
byte[] data=delivery.getBody();
String mString=new String(data);
System.out.println("消费者1取到:"+mString);
}
}
}
消费者2
public class Test_2_work_consumer2 {
@Test
public void consumer() throws Exception
{
//1,建立连接
ConnectionFactory factory=new ConnectionFactory();
factory.setHost("192.168.216.202");
factory.setPort(5672);
factory.setUsername("jtadmin");
factory.setPassword("jtadmin");
factory.setVirtualHost("/jt");
Connection connection=factory.newConnection();
//2,建立通道
Channel channel=connection.createChannel();
//3,定义队列
//durable true 持久化,重启服务器后,数据还有
//exclusive true,只能通过当前连接消费 false
//autoDelete true 队列中消息处理完后,自动删除队列
//arguments 参数
channel.queueDeclare("orderQueue", true, false, false, null);
//4,创建消费者
QueueingConsumer consumer=new QueueingConsumer(channel);
//autoAck:自动回复消息
channel.basicConsume("orderQueue", true, consumer);
//5,取消息
System.out.println("消费者2启动");
while(true)
{
Delivery delivery=consumer.nextDelivery();
byte[] data=delivery.getBody();
String mString=new String(data);
System.out.println("消费者2取到:"+mString);
}
}
}
说明:
消息被两个消费者消费
3.发布订阅模式
特点:如果生产者发送消息,那么订阅的全部消费者都会执行消息.
后期添加的消费者得不到以前的消息。需要先启动消费者
生产者:
public class Test_3_publish_p {
// 定义生产者
@Test
public void provider() throws IOException {
System.out.println("开始发布消息");
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.216.202");
factory.setPort(5672);
factory.setUsername("jtadmin");
factory.setPassword("jtadmin");
factory.setVirtualHost("/jt");
Connection connection = factory.newConnection();
// 定义通道
Channel channel = connection.createChannel();
// 定义交换机名称
String exchange_name = "E1";
// fanout是定义发布订阅模式 direct是 路由模式 topic是主题模式
channel.exchangeDeclare(exchange_name, "fanout");
String msg = "order1" ;
channel.basicPublish(exchange_name, "", null, msg.getBytes());
channel.close();
connection.close();
}
}
消费者1:
public class Test_3_publish_c1 {
@Test
public void consumer1() throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.216.202");
factory.setPort(5672);
factory.setUsername("jtadmin");
factory.setPassword("jtadmin");
factory.setVirtualHost("/jt");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
String exchange_name = "E1";
//定义交换机模式
channel.exchangeDeclare(exchange_name, "fanout");
String queue_name = UUID.randomUUID().toString();
System.out.println("队列名称"+queue_name);
//定义队列
channel.queueDeclare(queue_name, true, false, false, null);
//将队列和交换机绑定 key:表示接收数据标识
channel.queueBind(queue_name, exchange_name, "");
//定义消费数量
channel.basicQos(1);
//定义消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
//将消费者和队列绑定,并且需要手动返回
channel.basicConsume(queue_name, false, consumer);
System.out.println("消费者1");
while(true){
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String msg = new String(delivery.getBody());
System.out.println(msg+"入库");
//false表示一个一个返回
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
消费者2:
@Test
public void consumer1() throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.216.202");
factory.setPort(5672);
factory.setUsername("jtadmin");
factory.setPassword("jtadmin");
factory.setVirtualHost("/jt");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
String exchange_name = "E1";
//定义交换机模式
channel.exchangeDeclare(exchange_name, "fanout");
String queue_name = UUID.randomUUID().toString();
System.out.println("队列名称"+queue_name);
//定义队列
channel.queueDeclare(queue_name, true, false, false, null);
//将队列和交换机绑定 key:表示接收数据标识
channel.queueBind(queue_name, exchange_name, "");
//定义消费数量
channel.basicQos(1);
//定义消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
//将消费者和队列绑定,并且需要手动返回
channel.basicConsume(queue_name, false, consumer);
System.out.println("消费者2");
while(true){
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String msg = new String(delivery.getBody());
System.out.println(msg+"给用户发短信");
//false表示一个一个返回
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
说明:
两个消费者都能消费消息
4.路由模式
说明:路由模式是发布订阅模式的升级,通过定义不同的路由key使得程序将消息发送到不同的队列中.
5.主题模式
说明:
可以通过路由key将消息发送到一类相同的key中 使用通配符实现
符号说明:
#号:表示任意字符(任意个.)
*号:任意单个字符或者词组(单个.)
ActiveMq:
1、点对点模式:
只有一个消费者可以接收到消息
不能重复消费
生产者:
$queneName = "/queue/userReg";
消费者:
$stomp->subscribe('/queue/userReg');
2、发布/订阅模型特点:
多个消费者都可以收到消息
能重复消费
生产者:
$queneName = "/topic/userReg";
消费者:
$stomp->subscribe('/topic/userReg');
几种消息队列的比较:
例如:
面试:公司是怎么处理消息被消费者消费失败的问题的
引入了死信队列,将失败的消息放到死信队列中,重新消费。