消息队列解决了什么问题
异步处理
我们以用户注册为例,当用户注册成功后,我们首先将用户注册信息写入数据库,我们再给他发送一封邮件,再给他发送一条短信,当都执行成功后,我们再给用户响应,需要 150 ms
为了优化程序,我们想到了第二种方式,将用户注册信息写入数据库后,我们采用多线程的方式给用户发送邮件和短信,合起来响应 100 ms
这时候我们的消息队列就派上用场了,我们将用户注册信息写入数据库后,采用消息队列的方式,将信息写入到消息队列中,立马返回,用户相当于在 55 ms 后得到响应,发送邮件和短信这些服务可以通过异步读取的方式自己从消息队列中取出用户信息,给用户发邮件和短信
应用解耦
传统的订单系统和库存系统是耦合在一起的,当我们下单系统完成之后再去调用库存系统,但是一旦我们的库存系统宕机,就会出现数据的丢失
现在我们引入消息队列,订单系统将下单信息发送到消息队列中,库存系统从消息队列订阅消息,这样即使库存系统宕机,也可以在重新恢复服务后从消息队列中读取消息
流量削峰
一个典型的例子是秒杀系统,当用户的请求进来后,我们直接发送到消息队列中,假设请求有 10 万个,我们给消息队列设置一个定长,比如 1 万,10 万个请求谁快谁进队,剩下的 9 万个响应秒杀失败,然后我们的秒杀业务逻辑再从队列中读取消息
日志处理
简介
RabbitMQ 是一个由 Erlang 开发的 AMQP(Advanved Message Queue Protocol) 的开源实现
核心概念
Message
消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括 routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等
Publisher
消息的生产者,也是一个向交换器发布消息的客户端应用程序
Exchange
交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列
Exchange 有 4 种类型:direct(默认),fanout,topic 和 headers,不同类型的 Exchange 转发消息的策略有所区别
Queue
消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走
Binding
绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表
Exchange 和 Queue 的绑定可以是多对多的关系
Connection
网络连接,比如一个 TCP 连接
Channel
信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的 TCP 连接内的虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接
Consumer
消息的消费者,表示一个从消息队列中取得消息的客户端应用程序
Virtual Host
虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP 概念的基础,必须在连接时指定,RabbitMQ 默认的 vhost 是 /
Broker
表示消息队列服务器实体
Rabbit MQ 安装与配置
Rabbit MQ 的安装比较繁琐,建议直接在 Linux 上用 docker 安装
添加用户
virtual hosts 管理
virtual hosts 相当于 MySQL 的 db
一般以 / 开头
我们得对用户进行授权
简单队列
模型
P:消息的生产者
红色的:消息队列
C:消费者
三个对象
生产者
队列 Rabbit MQ
消费者
获取 MQ 连接
public class ConnectionUtils {
/**
* 获取 Rabbit MQ 的连接
* @return
*/
public static Connection getConnection() throws IOException, TimeoutException {
// 定义一个连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 设置服务地址
factory.setHost("47.104.218.84");
// AMQP 5672
factory.setPort(5672);
// vhost
factory.setVirtualHost("/vhost_ml");
// 用户名
factory.setUsername("user_ml");
// 密码
factory.setPassword("maolong");
return factory.newConnection();
}
}
生产者生产消息
public class Send {
private static final String QUEUE_NAME = "simple_queue";
public static void main(String[] args) throws IOException, TimeoutException {
// 获取一个连接
Connection connection = ConnectionUtils.getConnection();
// 从连接中获取一个通道
Channel channel = connection.createChannel();
// 创建队列声明
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
String msg = "hello simple!";
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
System.out.println("--send msg:" + msg);
channel.close();
connection.close();
}
}
消费者接受消息
public class Receive {
private static final String QUEUE_NAME = "simple_queue";
@SuppressWarnings("deprecation")
public static void main(String[] args) throws IOException, TimeoutException {
// 获取一个连接
Connection connection = ConnectionUtils.getConnection();
// 从连接中获取一个通道
Channel channel = connection.createChannel();
// 创建队列声明
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 定义消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "utf-8");
System.out.println("[recv] msg:" + msg);
}
};
// 监听队列
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
简单队列的不足
耦合性高,生产者一一对应消费者(如果我想有多个消费者消费队列中消息,这时候就不行了),队列名变更,这时候得同时变更
Worker queues 工作队列
模型
为什么会出现工作队列
Simple 队列是一一对应的,而且我们实际开发,生产者发送消息是毫不费力的,而消费者一般是要跟业务相结合的,消费者接收到消息之后就需要处理,可能需要花费时间,这时候队列就会积压了很多消息
生产者
public class Send {
private final static String QUEUE_NAME = "queue_work";
public static void main(String[] args) throws Exception {
// 获取一个连接
Connection connection = ConnectionUtils.getConnection();
// 从连接中获取一个通道
Channel channel = connection.createChannel();
// 创建队列声明
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
for (int i = 0; i < 50; i++) {
// 消息内容
String message = "" + i;
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("[x] Send '" + message + "'");
Thread.sleep(i * 20);
}
channel.close();
connection.close();
}
}
消费者1
public class Receive1 {
private final static String QUEUE_NAME = "queue_work";
public static void main(String[] args) throws Exception {
// 获取一个连接
Connection connection = ConnectionUtils.getConnection();
// 从连接中获取一个通道
Channel channel = connection.createChannel();
// 创建队列声明
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
/**
* 获取到到达的消息
* @param consumerTag
* @param envelope
* @param properties
* @param body
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "utf-8");
System.out.println("[1] recv msg:" + msg);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("[1] done");
}
}
};
// 监听队列
boolean autoAck = true;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
}
}
消费者2
public class Receive2 {
private final static String QUEUE_NAME = "queue_work";
public static void main(String[] args) throws Exception {
// 获取一个连接
Connection connection = ConnectionUtils.getConnection();
// 从连接中获取一个通道
Channel channel = connection.createChannel();
// 创建队列声明
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
/**
* 获取到到达的消息
* @param consumerTag
* @param envelope
* @param properties
* @param body
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "utf-8");
System.out.println("[2] recv msg:" + msg);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("[2] done");
}
}
};
// 监听队列
boolean autoAck = true;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
}
}
现象
消费者 1 和消费者 2 处理的消息是一样的
消费者 1:偶数
消费者 2:奇数
这种方式叫做轮询分发(round-robin),结果就是不管谁忙活着谁清闲,都不会多给一个消息
公平分发 fair dispatch
生产者
public class Send {
private final static String QUEUE_NAME = "queue_work";
public static void main(String[] args) throws Exception {
// 获取一个连接
Connection connection = ConnectionUtils.getConnection();
// 从连接中获取一个通道
Channel channel = connection.createChannel();
// 创建队列声明
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
/**
* 每个消费者发送确认消息之前,消息队列不发送下一个消息到消费者,一次只处理一个消息
* 限制发送给同一个消费者不得超过一条数据
*/
int prefetchCount = 1;
channel.basicQos(prefetchCount);
for (int i = 0; i < 50; i++) {
// 消息内容
String message = "" + i;
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("[x] Send '" + message + "'");
Thread.sleep(i * 20);
}
channel.close();
connection.close();
}
}
消费者1
public class Receive1 {
private final static String QUEUE_NAME = "queue_work";
public static void main(String[] args) throws Exception {
// 获取一个连接
Connection connection = ConnectionUtils.getConnection();
// 从连接中获取一个通道
Channel channel = connection.createChannel();
// 创建队列声明
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 保证一次只分发一个
channel.basicQos(1);
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
/**
* 获取到到达的消息
* @param consumerTag
* @param envelope
* @param properties
* @param body
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "utf-8");
System.out.println("[1] recv msg:" + msg);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("[1] done");
// 手动回执
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
// 监听队列,自动应答改为 false
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
}
}
消费者2
public class Receive2 {
private final static String QUEUE_NAME = "queue_work";
public static void main(String[] args) throws Exception {
// 获取一个连接
Connection connection = ConnectionUtils.getConnection();
// 从连接中获取一个通道
Channel channel = connection.createChannel();
// 创建队列声明
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 保证一次只分发一个
channel.basicQos(1);
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
/**
* 获取到到达的消息
* @param consumerTag
* @param envelope
* @param properties
* @param body
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "utf-8");
System.out.println("[2] recv msg:" + msg);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("[2] done");
// 手动回执
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
// 监听队列,自动应答改为 false
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
}
}
现象
消费者 2 处理的消息比消费者 1 多,能者多劳