一、为什么需要MQ
学习一个工具,习惯思考其使用的场景,没有使用价值,也没有学习的必要。要了解MQ的适用场景,可以先看一个业务场景。
商品的原始数据都存储在数据库中,增删改查都在数据库中完成,搜索索引为ES,前端页面为静态页面,不会随着数据库的数据变化而自动变化。
当修改数据库中数据时,比如价格,需要及时的反馈到ES和前端页面。怎么来设计这个框架?比较差的
(1)数据库端开放接口,供ES和前端页面不停的查询——延迟先不讨论,前端、ES和数据库模块强耦合,还会造成接口查询的压力和资源的浪费。
(2)前端页面和ES开放接口,供数据库处理模块每次需改数据后的调用——模块间耦合。
引入MQ消息队列,数据库端处理完,只需向MQ发送一个消息,前端页面和ES消费订阅消费消息,根据消息内容,自主处理,解耦,并且消息队列的强大,延迟基本忽略。
二、MQ消息队列
MQ全称Message Queue,即消息队列,是在消息传输过程中存储消息的容器。生产者(上例中的数据库端)和消费者(上例中的ES和前端页面)只关注消息的发送和接收,没有业务逻辑的入侵,实现生产者和消费者的解耦。
三、MQ使用场景
3.1 异步处理任务
高并发环境使用的比较多,由于来不及处理,请求往往会阻塞,响应延迟。通过消息队列,将不需要立即处理的请求,可以异步处理请求,请求达到MQ,就可以给出响应。然后MQ在通知消息接收模块,达到异步处理,减少系统响应时间。
3.2 应用程序解耦
MQ充当中介角色,应用程序通过中介与需要交互的程序通信。
四、MQ分类
实现消息队列,现在主流的方式有:AMQP和JMS。
- JMS定义了统一接口,来对消息进行统一操作;AMQP是通过规定协议来统一数据交换格式。
- JMS必须使用JAVA语言;AMQP只是协议,不规定实现方式,因此是跨语言的。
- JMS规定两种消息模型;AMQP消息模型更丰富。
主流的MQ产品有:
- ActiveMQ:基于JMS
- RabbitMQ:基于AMQP,erlang语言开发,稳定性好
- RocketMQ:基于JMS,阿里巴巴产品
- Kafka:分布式消息系统,高吞吐量。
五、RabbitMQ的工作原理
5.1 RabbitMQ工作原理结构图
- Broker:消息队列服务进程,此进程包括Exchange(交换机)和Queue(队列)
- Exchange:消息队列交换机,按一定的规则,将消息转发到某个队列,对消息进行过滤。
- Queue:消息队列,存储消息的队
- Producer:消息生产者,生产方客户端,将消息发送到Exchange
- Consumer:消费者,消费者客户端,接收MQ转发的消息。
5.2 Exchange类型
交换机,生产者所有发出的消息都是直接发给交换机,交换机转发到相应的队列。RabbitMQ提供默认的交换机,只需在publish发布消息时,交换机参数填写空,就可以使用RabbitMQ提供的默认交换机。
当然也可以根据自己业务的需要,自己定义交换机,交换机类型如下:
- Fanout:广播,将消息交给所有绑定到交换机的队列
- Direct:定向,把消息交给符合routing key的队列
- Topic:通配符,把消息交给符合routing pattern(路由模式)队列
5.3 生产者发送消息流程
- 生产者和Broker建立TCP连接
- 生产者和Broker建立通道Channel
- 生产者通过通道发送消息给Broker
- Exchange转发消息给指定的Queue
5.4 消费者接收消息流程
- 消费者和Broker建立TCP连接
- 消费者和Broker建立通道Channel
- 消费者监听队列Queue
- 当有消息到达队列Queue时,Broker默认将消息推送给消费者
- 消费者接收到消息
- ack回复
六、代码案例实现
6.1 基本消息模型
一个生产者一个队列,一个消费者。
首先,使用maven项目,手动写创建Broker的Connection(Spring boot集成RabbitMQ后面再说),新建两个maven工程,producer和consumer,引入RabbitMQ的Maven依赖。
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.7.1</version>
</dependency>
</dependencies>
连接工具类,调用rabbitMQ的ConnectionFactory类,工厂方式创建连接
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class ConnectionUtil {
public static Connection getConnection() throws Exception{
//定义工厂类
ConnectionFactory factory = new ConnectionFactory();
//设置服务地址
factory.setHost("10.21.70.43");
//端口
factory.setPort(5672);
//设置账号信息
factory.setUsername("guest");
factory.setPassword("guest");
return factory.newConnection();
}
}
在工程producer中,生产者端代码
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ProducerSend {
private static final String QUEUE_NAME="simple_queue";
public static void main(String[] args) {
//获取连接
Connection connection = null;
//2.从连接中创建通道
Channel channel = null;
try {
connection = ConnectionUtil.getConnection();
channel = connection.createChannel();
// 3、声明(创建)队列
//参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
/**
* 参数明细
* 1、queue 队列名称
* 2、durable 是否持久化,如果持久化,mq重启后队列还在
* 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
* 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
* 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//4.消息
String msg = "Hello my friend!";
//向指定队列中发送消息
//参数:String exchange, String routingKey, BasicProperties props, byte[] body
/**
* 参数明细:
* 1、exchange,交换机,如果不指定将使用mq的默认交换机(设置为"")
* 2、routingKey,路由key,交换机根据路由key来将消息转发到指定的队列,如果使用默认交换机,routingKey设置为队列的名称
* 3、props,消息的属性
* 4、body,消息内容
*/
channel.basicPublish("", QUEUE_NAME,null,msg.getBytes());
System.out.println("send '"+msg+"'");
}catch (Exception e){
e.printStackTrace();
} finally {
if (channel != null) {
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
consumer工程中消费者接收消息代码,连接工具类使用producer工程中的。
import com.cc.consumer.util.ConnectionUtil;
import com.rabbitmq.client.*;
import java.io.UnsupportedEncodingException;
public class ConsumerRecv {
private static final String QUEUE_NAME = "simple_queue";
public static void main(String[] args) throws Exception {
//1.获取connection
Connection connection = ConnectionUtil.getConnection();
//2.获取通道Channel,生产者和MQ的通信都在channel中完成
Channel channel = connection.createChannel();
//3.声明队列
//参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
/**
* 参数明细
* 1、queue 队列名称
* 2、durable 是否持久化,如果持久化,mq重启后队列还在
* 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
* 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
* 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
*/
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 UnsupportedEncodingException {
//交换机
String exchange = envelope.getExchange();
//消息id,消息在channel中传输用来标识消息的id,可用于消息的确认接收
long deliveryTag = envelope.getDeliveryTag();
//body
String msg = new String(body,"UTF-8");
System.out.println("receive msg '"+msg+"'");
}
};
//监听队列,第二个参数是否自动进行消息确认
channel.basicConsume(QUEUE_NAME,true,consumer);
}
}
手动确认消息模式
自动确认模式,Broker会在将消息发送到Channel时,就确认消息成功消费,如果消费者端因为代码原因,消息没有正常处理,将会丢失该消息。所以需要手动确认消息,如果业务逻辑失败,会将消息重新放回Broker队列,等待下次消费。
消费者端代码稍作改动即可。
6.2 work消息模型
一个队列对应两个消费者,队列中消息只能被一个消费者消费,竞争消费者模式。
这种模式比较适合分压的场景,在不设置通道Channel的BasicQos对象的prefetchCount属性时, RabbitMQ会平分所有任务给两个消费者,示例代码如下,在6.1示例上修改,不贴出全部代码,只贴出核心代码的截图
生产者循环发送50个消息到队列
消费者C1,实现和6.1中一样,C2中在手动确认前休眠1秒,模拟处理比较慢的场景。
两个队列的输出如下
发现两个Consumer消费消息的数量是一致的,所以可以知道,默认情况,RabbitMQ是平分两个消费者的工作量的。
需要处理快的Consumer多处理消息,可以设置channel的BasicQos对象的prefetchCount属性,设置成1,即表示一个消费者在同一个时间点只处理一个消息,也就是说,必须收到消息确认后,才会接收处理第二条消息。
prefetchCount只在手动确认模式下生效,其实也很好理解,自动确认,刚到Channel,就确认了,跟业务处理快慢没有关系了。
可以看到输出结果的差别,快的Consumer消费了更多的msg
以上都是使用了RabbitMQ默认的交换机,下面将从交换机三种类型来说明,交换机类型在5.2小节已经说明
6.3 publish/subscribe(广播)
实现生产者消息被多个消费者同时消费,模型示意图如下:
Producer生产者,不再声明队列,而是声明交换机,转发所有Consumer消费者绑定到声明的交换机上的队列,示例代码如下:
Producer代码:
import com.cc.producer.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class ProducerBroadcast {
private static final String EXCHANGE_NAME = "test_fanout_exchange";
public static void main(String[] args) {
Connection connection = null;
Channel channel = null;
try {
connection = ConnectionUtil.getConnection();
//获取通道
channel = connection.createChannel();
//声明交换机,
/**
* 广播模式,不在绑定指定队列,转发所有队列
*/
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
//消息内容
String msg = "注册成功";
//发布消息到exchange
channel.basicPublish(EXCHANGE_NAME,"",null,msg.getBytes(StandardCharsets.UTF_8));
System.out.println("[生产者] send '"+msg+"'");
} catch (Exception e){
e.printStackTrace();
} finally {
if (channel != null) {
try {
channel.close();
} catch (IOException | TimeoutException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
示例中设定两个Consumer,代码基本一致,就是队列 名不一样,所以这边就贴出一份代码
import com.cc.consumer.util.ConnectionUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class ConsumerBroadcast1 {
private static final String EXCHANGE_NAME = "test_fanout_exchange";
private static final String QUEUE_NAME = "fanout_exchange_queue_sms1";
public static void main(String[] args) throws Exception {
//获取连接
Connection connection = ConnectionUtil.getConnection();
//获取通道
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//绑定队列到交换机
//routingKey为空
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"");
//消费者
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//交换机
String exchange = envelope.getExchange();
//消息id,消息在channel中传输用来标识消息的id,可用于消息的确认接收
long deliveryTag = envelope.getDeliveryTag();
//body
String msg = new String(body, StandardCharsets.UTF_8);
System.out.println("[消费者1] received '"+msg+"'");
//手动进行消息确认
/*
* void basicAck(long deliveryTag, boolean multiple) throws IOException;
* deliveryTag:用来标识消息的id
* multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息。
*/
channel.basicAck(deliveryTag,false);
}
};
channel.basicConsume(QUEUE_NAME,false,consumer);
}
}
可以发现Producer执行发送消息后,两个Consumer都能消费到信息。
Tips:
publish/subscribe模式和work queues模式区别?
区别:
- work queues不用定义交换机,用RabbitMQ默认交换机,而publish/subscribe模式需要定义交换机
- publish/subscribe时生产者向交换机发送消息,work queues模式,看起来是直接发送到queue,其实也是发送到默认交换机,有交换机直接转发到指定队列。
- publish/subscribe模式,在消费者端需要设置队列与交换机的绑定,work queue将队列绑定到默认的交换机上。
相同点:
其实从底层实现来看,publish/subscribe模式和work queues模式是一样的,都是将消息发送给交换机,由交换机转发队列,publish/subscribe模式的示例,将两个消费者的队列名设置成一样,就是work queues模式,只不过交换机由默认换成自定义的一个交换机。
6.4 Routing路由模式(Direct定向交换机)
队列和路由绑定,根据routing key直接找到指定队列的模式,模型示意图如下:
- 生产者向Exchange发送消息时,会指定一个routing key
- Exchange将消息转发给和routing key完全匹配的队列,和Routing pattern模式的主要区别
- 消费者声明队列时,指定了需要消费的队列routing key
import com.cc.producer.util.ConnectionUtil;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class ProducerDirect {
private static final String EXCHANGE_NAME = "test_direct_exchange";
public static void main(String[] args){
Connection connection = null;
Channel channel = null;
try {
connection = ConnectionUtil.getConnection();
channel = connection.createChannel();
//声明交换机exchange,指定为direct类型
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
String msg = "注册成功,请回复短信[T]退订";
//发送消息,并且指定routing key为"sms"
channel.basicPublish(EXCHANGE_NAME,"sms",null,msg.getBytes(StandardCharsets.UTF_8));
System.out.println("Send '"+msg+"'");
}catch (Exception e){
e.printStackTrace();
}finally {
if (channel == null) {
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
消费者端代码,关注绑定Exchange部分
import com.cc.consumer.util.ConnectionUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class ConsumerDirect {
private static final String EXCHANGE_NAME = "test_direct_exchange";
private static final String QUEUE_NAME = "direct_exchange_queue_sms";
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//绑定到交换机,并且指定routing key为"sms"
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"sms");
//消费者
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//交换机
String exchange = envelope.getExchange();
//消息id,消息在channel中传输用来标识消息的id,可用于消息的确认接收
long deliveryTag = envelope.getDeliveryTag();
//body
String msg = new String(body, StandardCharsets.UTF_8);
System.out.println("[消费者] received '"+msg+"'");
//手动进行消息确认
/*
* void basicAck(long deliveryTag, boolean multiple) throws IOException;
* deliveryTag:用来标识消息的id
* multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息。
*/
channel.basicAck(deliveryTag,false);
}
};
channel.basicConsume(QUEUE_NAME,false,consumer);
}
}
6.5 通配符模式(Topics类型交换机)
routing key为通配符方式,可对比6.4小节,direct模式,原理示意图如下:
通配符规则:
#:匹配一个或者多个单词
*:只能匹配一个单词
样例说明通配规则,
audit.#:能够匹配audit.irs.corporate或者audit.irs
audit.*:只能匹配audit.irs
示例代码,跟direct类似,只粘贴出重要部分
生产者代码和direct基本类似,直接贴消费者端代码