一、RabbitMQ工作原理
1.RabbitMQ支持协议
RabbitMQ支持AMQP,STOMP,MQTT,HTTP,WebSockets协议
2.RabbitMQ工作模型
1)模型绘图
- Broker主机:当消费者消费消息,生产者发送消息都需要和Broker主机进行tcp长连接,但每次发送消息都建立长连接过于浪费主机资源,因此出现了Channel信道这个概念
- VHost虚拟主机:用来实现资源隔离,不同的业务可以用不同的虚拟主机,只需要用不同的用户与不同的虚拟主机进行绑定,就可以实现此功能。
- Channel信道:信道实际上是一个虚拟连接,目的是为了减少建立长连接时的资源和时间的消耗
- Exchange交换机:起到分发消息的作用,生产者的消息将不会直接发送给队列,而是发送到交换机中,交换机将与一个到多个Queue队列有Bingding绑定,当消息发送过来后,由交换机根据相应的规则和对应的绑定分发给对应队列,起到消息分发的作用,减少了生产者调用发送消息的资源浪费
- Queue队列:用来存储消息,是消费者和生产者的关联纽带,实际上它使用了一个数据库对消息进行存储,只有队头的消息被消费,此消息才会从数据库中删除,在实际运用中,建议一个消费者只从一个队列里拉取消息,这样才能保证消息的消费不出现混乱情况,如果消息积压过多,也可以使用负载均衡进行消费。
- 消费模型:即消费者消费消息的方式,在RabbitMQ中,提供了两种消费模型,一种为pull,是消费者主动去Broker主机拉取消息,接口为basicGet,一种为push,是Broker主机推送消息,接口为basicConsume。
- Bingding绑定:将一个Exchange交换机和一个或多个Queue队列绑定起来,每一个从生产者发送来的消息都有特殊标识,Exchange交换机将根据特殊标识和Bingding绑定建立的绑定关系确定如何将消息发送给对应的Queue队列
2)常见交换机类型
- direct:直连,使用明确的绑定建,适用与业务明确的场景。
- fanout:广播,无需绑定键,适用于通用类业务消息。
- topic:主题,使用支持通配符的绑定键,适用于根据业务主题过滤消息的场景。
通配符:* ,代表一个单词, *.jvm的匹配路由为abc.jvm,def.jvm。abc.def.jvm则不行。
通配符:#,代表任意单词,jdk.#的匹配路由为任意jdk开头的路由。 - headers:根据请求头对消息进行分配,不常用。
二、RabbitMQ代码实例
1.原生代码
需要在项目中引入rabbitmq的jar包才能使用相关功能
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.6.0</version>
</dependency>
消费端代码
//创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 连接IP
factory.setHost("127.0.0.1");
// 默认监听端口
factory.setPort(5672);
// 虚拟机
factory.setVirtualHost("/");
// 设置访问的用户
factory.setUsername("guest");
factory.setPassword("guest");
// 建立连接
Connection conn = factory.newConnection();
// 创建消息通道
Channel channel = conn.createChannel();
// 声明交换机
// String exchange(交换机名称)
//String type(交换机类型)
// boolean durable(是否持久化)
//boolean autoDelete(无人连接时是否自动删除)
// Map<String, Object> arguments
channel.exchangeDeclare(EXCHANGE_NAME,"direct",false, false, null);
// 声明队列
// String queue(队列名称)
//boolean durable(是否持久化)
//boolean exclusive(是否排外,一:当连接关闭时该队列是否会自动删除;二:该队列是否是私有的private)
//boolean autoDelete(无人连接时是否自动删除)
//Map<String, Object> arguments
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列和交换机
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"my.tets");
// 创建消费者
Consumer 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");
}
};
// 开始获取消息
// String queue, boolean autoAck, Consumer callback
channel.basicConsume(QUEUE_NAME, true, consumer);
}
生产端代码
ConnectionFactory factory = new ConnectionFactory();
// 连接IP
factory.setHost("127.0.0.1");
// 连接端口
factory.setPort(5672);
// 虚拟机
factory.setVirtualHost("/");
// 用户
factory.setUsername("guest");
factory.setPassword("guest");
// 建立连接
Connection conn = factory.newConnection();
// 创建消息通道
Channel channel = conn.createChannel();
// 发送消息
String msg = "Hello world, Rabbit MQ";
// String exchange, String routingKey, BasicProperties props, byte[] body
channel.basicPublish(EXCHANGE_NAME, "my.tets", null, msg.getBytes());
channel.close();
conn.close();
2.springboot中的应用
需要引入jar包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
1)消费端代码
连接rabbitmq,创建交换机和队列,并绑定
@Configuration
@PropertySource("classpath:rabbitmq.properties")
public class RabbitConfig {
@Value("${firstqueue}")
private String firstQueue;
@Value("${secondqueue}")
private String secondQueue;
@Value("${thirdqueue}")
private String thirdQueue;
@Value("${fourthqueue}")
private String fourthQueue;
@Value("${directexchange}")
private String directExchange;
@Value("${topicexchange}")
private String topicExchange;
@Value("${fanoutexchange}")
private String fanoutExchange;
// 创建四个队列
@Bean("firstQueue")
public Queue getFirstQueue(){
return new Queue(firstQueue);
}
@Bean("secondQueue")
public Queue getSecondQueue(){
return new Queue(secondQueue);
}
@Bean("thirdQueue")
public Queue getThirdQueue(){
return new Queue(thirdQueue);
}
@Bean("fourthQueue")
public Queue getFourthQueue(){
return new Queue(fourthQueue);
}
// 创建三个交换机
@Bean("directExchange")
public DirectExchange getDirectExchange(){
return new DirectExchange(directExchange);
}
@Bean("topicExchange")
public TopicExchange getTopicExchange(){
return new TopicExchange(topicExchange);
}
@Bean("fanoutExchange")
public FanoutExchange getFanoutExchange(){
return new FanoutExchange(fanoutExchange);
}
// 定义四个绑定关系
@Bean
public Binding bindFirst(@Qualifier("firstQueue") Queue queue, @Qualifier("directExchange") DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("test.one");
}
@Bean
public Binding bindSecond(@Qualifier("secondQueue") Queue queue, @Qualifier("topicExchange") TopicExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("*.test.*");
}
@Bean
public Binding bindThird(@Qualifier("thirdQueue") Queue queue, @Qualifier("fanoutExchange") FanoutExchange exchange){
return BindingBuilder.bind(queue).to(exchange);
}
@Bean
public Binding bindFourth(@Qualifier("fourthQueue") Queue queue, @Qualifier("fanoutExchange") FanoutExchange exchange){
return BindingBuilder.bind(queue).to(exchange);
}
/**
* 在消费端转换JSON消息
* 监听类都要加上containerFactory属性
* @param connectionFactory
* @return
*/
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(new Jackson2JsonMessageConverter());
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
factory.setAutoStartup(true);
return factory;
}
}
创建消费者,@RabbitListener注解:添加监听对应的队列,可设置多个,用逗号分隔开。
@Component
@PropertySource("classpath:rabbitmq.properties")
@RabbitListener(queues = "${firstqueue}", containerFactory="rabbitListenerContainerFactory")
public class FirstConsumer {
@RabbitHandler
public void process(@Payload Merchant merchant){
System.out.println("First Queue received msg : " + merchant.getName());
}
}
@Component
@PropertySource("classpath:gupaomq.properties")
@RabbitListener(queues = "${com.gupaoedu.secondqueue}", containerFactory="rabbitListenerContainerFactory")
public class SecondConsumer {
@RabbitHandler
public void process(String msgContent,Channel channel, Message message) throws IOException {
System.out.println("Second Queue received msg : " + msgContent );
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
@Component
@PropertySource("classpath:gupaomq.properties")
@RabbitListener(queues = "${com.gupaoedu.thirdqueue}", containerFactory="rabbitListenerContainerFactory")
public class ThirdConsumer {
@RabbitHandler
public void process(String msg){
System.out.println("Third Queue received msg : " + msg);
}
}
@Component
@PropertySource("classpath:gupaomq.properties")
@RabbitListener(queues = "${com.gupaoedu.fourthqueue}", containerFactory="rabbitListenerContainerFactory")
public class FourthConsumer {
@RabbitHandler
public void process(String message) throws IOException {
System.out.println("Fourth Queue received msg : " + message);
}
}
其中第二个消费者略有不同,可以主动发送ACK信息。
这样就可以自动创建全部的交换机和队列,并自动建立绑定关系。
2)生产端代码
@Configuration
public class RabbitConfig {
/**
* 所有的消息发送都会转换成JSON格式发到交换机
* @param connectionFactory
* @return
*/
@Bean
public RabbitTemplate amqpTemplate(final ConnectionFactory connectionFactory) {
final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
return rabbitTemplate;
}
}
@Component
@PropertySource("classpath:rabbitmq.properties")
public class RabbitSender {
@Value("${directexchange}")
private String directExchange;
@Value("${topicexchange}")
private String topicExchange;
@Value("${fanoutexchange}")
private String fanoutExchange;
@Value("${directroutingkey}")
private String directRoutingKey;
@Value("${topicroutingkey1}")
private String topicRoutingKey1;
@Value("${topicroutingkey2}")
private String topicRoutingKey2;
// 自定义的模板,所有的消息都会转换成JSON发送,此为实现了amqp协议的模板
@Autowired
AmqpTemplate amqpTemplate;
//也可以使用此rabbitmq专用模板,但仅支持rabbitmq
@Autowired
RabbitTemplate rabbitTemplate;
public void send() throws JsonProcessingException {
Merchant merchant = new Merchant(1001,"a direct msg : 中原镖局","汉中省解放路266号");
amqpTemplate.convertAndSend(directExchange,directRoutingKey, merchant);
amqpTemplate.convertAndSend(topicExchange,topicRoutingKey1, "a topic msg : shanghai.test.teacher");
amqpTemplate.convertAndSend(topicExchange,topicRoutingKey2, "a topic msg : changsha.test.student");
// 发送JSON字符串
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(merchant);
System.out.println(json);
amqpTemplate.convertAndSend(fanoutExchange,"", json);
}
}
3.延时推送消息的实现
1)方案一:死信队列
死信队列,rabbitmq-delayed-message-exchange,可以用来实现延时推送。
当一个消息始终没人消费,等到过期之后,就会进入死信交换机,之后进入死信队列,若是有消费者监听死信队列,则可以获得此过时消息,即实现延时推送,延时时间为过期时间。
死信为何产生:
- 一个消息,过期之后,即为死信。
- 当队列里的消息存储空间或者存储个数满了,排在队列中队头的消息(即此队列中第一个进入的消息)将成为死信
- 消费者拒绝此消息,而且没有设置重回队列
生产者代码示例:
// 设置属性,消息10秒钟过期
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.deliveryMode(2) // 持久化消息
.contentEncoding("UTF-8")
.expiration("10000") // TTL
.build();
// 发送消息
channel.basicPublish("", "USE_QUEUE", properties, msg.getBytes());
此处交换机命名为空,因为RabbitMQ含有一个默认为空名字的交换机,它会默认找到一个和后面的routingKey内容一样名字的队列进行连接。
消费者代码示例:
// 声明死信交换机
channel.exchangeDeclare("DEAD_LETTER_EXCHANGE","topic", false, false, false, null);
// 声明死信队列
channel.queueDeclare("DEAD_LETTER_QUEUE", false, false, false, null);
// 绑定
channel.queueBind("DEAD_LETTER_QUEUE","DEAD_LETTER_EXCHANGE","#");
// 创建消费者
Consumer 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");
}
};
// 开始获取消息
// String queue, boolean autoAck, Consumer callback
channel.basicConsume("DEAD_LETTER_QUEUE", true, consumer);
绑定键为#,意味着是无条件绑定,任意都可以。
这样就实现完成了。
但是这样的延时发送有一个问题,消息本身可以设置一个过期时间,队列也可以设置一个统一的过期时间,这两个时间同时存在时,是以少的那个时间为准,因此每一个不同的延时发送时间都需要单独创建一个队列,这非常麻烦。
2)方案二:Delayed Message 插件
使用Delayed Message 插件后(需要单独下载插件)即可声明一个名为x-delayed-messgaed的特殊类型交换机,该交换机可以直接实现延时发送功能。
生产者代码示例
// 延迟的间隔时间,目标时刻减去当前时刻
Map<String, Object> headers = new HashMap<String, Object>();
headers.put("x-delay", delayTime.getTime() - now.getTime());
AMQP.BasicProperties.Builder props = new AMQP.BasicProperties.Builder()
.headers(headers);
channel.basicPublish("DELAY_EXCHANGE", "DELAY_KEY", props.build(),
msg.getBytes());
消费者代码示例
// 声明x-delayed-message类型的exchange
Map<String, Object> argss = new HashMap<String, Object>();
argss.put("x-delayed-type", "direct");
channel.exchangeDeclare("DELAY_EXCHANGE", "x-delayed-message", false,
false, argss);
// 声明队列
channel.queueDeclare("DELAY_QUEUE", false,false,false,null);
// 绑定交换机与队列
channel.queueBind("DELAY_QUEUE", "DELAY_EXCHANGE", "DELAY_KEY");
// 创建消费者
Consumer 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");
SimpleDateFormat sf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
System.out.println("收到消息:[" + msg + "]\n接收时间:" +sf.format(new Date()));
}
};
// 开始获取消息
// String queue, boolean autoAck, Consumer callback
channel.basicConsume("DELAY_QUEUE", true, consumer);
4.消息控制
假如消息存满了,应该如何呢?最简单的方式当然是重启,但显然这不是一个可以经常使用的方法。
服务端控制
一般情况下,我们在服务端可以通过三种设置来对消息存储的数量进行控制,这个控制与死信产生的机制也有关系
- 队列长度:x-max-length或x-max-length-bytes
- 内存大小:Conn vm_memory_high_watermark
- 磁盘空间:disk_free_limit.relative或disk_free_limit.absolute
消费端控制
prefetch count预取数量,即限制消费端同时处理消息的数量
// 声明队列(默认交换机AMQP default,Direct)
// String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 创建消费者,并接收消息
Consumer 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");
channel.basicAck(envelope.getDeliveryTag(), true);
}
};
//非自动确认消息的前提下,如果一定数目的消息(通过基于consume或者channel设置Qos的值)未被确认前,不进行消费新的消息。
channel.basicQos(2);
channel.basicConsume(QUEUE_NAME, false, consumer);
basicQos方法控制了同时消费消息的最大数量,basicAck方法消费完消息后向服务端发送ACK,假如消费端它没有发送ACK数量等于最大消费消息数量,服务端就不会继续发送消息给它,这样就实现了对消费端的控制,直到它返回一条ACK为止。
除此之外,当有多个消费端消费消息时,一定是遵循“能者多劳”,处理速度快的将会获得更多的消息以提升整体性能。