消息队列
- 常见的消息队列
- 为什么使用MQ
- AMQP 和 JMS
- AMQP
- JMS
- AMQP 与 JMS 区别
- RabbitMQ
- 相关定义:
- Spring Boot整合RabbitMQ
- 使用代码创建队列和交换器
- 消息的可靠性(发送可靠)
- confim机制(保证发送可靠)
- Return机制(保证发送可靠)
- 编写配置文件
- 开启Confirm和Return
- 手动Ack(保证接收可靠)
- 避免消息重复消费
常见的消息队列
- ActiveMQ:基于JMS实现, 比较均衡, 不是最快的, 也不是最稳定的.
- ZeroMQ:基于C语言开发, 目前最好的队列系统.
- RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好, 数据基本上不会丢失
- RocketMQ:基于JMS,阿里巴巴产品, 目前已经捐献给apahce, 还在孵化器中孵化.
- Kafka:类似MQ的产品;分布式消息系统,高吞吐量, 目前最快的消息服务器, 不保证数据完整性.
为什么使用MQ
**(1) 异步操作: **
任务异步处理将不需要同步处理的并且耗时长的操作由消息队列通知消息接收方进行异步处理。提高了应用程序的响应时间。
**(2) 解耦: **
应用程序解耦合MQ相当于一个中介,生产方通过MQ与消费方交互,它将应用程序进行解耦合
**(3) 削峰: **
在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见;如果为以能处理这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用MQ能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。
**(4) 可恢复性: **
系统的一部分组件失效时,不会影响到整个系统。MQ降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。
AMQP 和 JMS
MQ是消息通信的模型;实现MQ的大致有两种主流方式:AMQP、JMS。
AMQP
AMQP是一种协议,更准确的说是一种binary wire-level protocol(链接协议)。这是其和JMS的本质差别,AMQP不从API层进行限定,而是直接定义网络交换的数据格式。
JMS
JMS即Java消息服务(JavaMessage Service)应用程序接口,是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。
AMQP 与 JMS 区别
JMS是定义了统一的接口,来对消息操作进行统一;AMQP是通过规定协议来统一数据交互的格式JMS限定了必须使用Java语言;AMQP只是协议,不规定实现方式,因此是跨语言的。JMS规定了两种消息模式;而AMQP的消息模式更加丰富.
RabbitMQ
RabbitMQ是由erlang语言开发,基于AMQP(Advanced Message Queue 高级消息队列协议)协议实现的消息队列,它是一种应用程序之间的通信方法,消息队列在分布式系统开发中应用非常广泛。
相关定义:
- Broker: 简单来说就是消息队列服务器实体
- Exchange: 消息交换机,它指定消息按什么规则,路由到哪个队列
- Queue: 消息队列载体,每个消息都会被投入到一个或多个队列
- Binding: 绑定,它的作用就是把exchange和queue按照路由规则绑定起来
- Routing Key: 路由关键字,exchange根据这个关键字进行消息投递
- VHost: 虚拟主机,一个broker里可以开设多个vhost,用作不同用户的权限分离。
- Producer: 消息生产者,就是投递消息的程序
- Consumer: 消息消费者,就是接受消息的程序
- Channel: 消息通道,在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务
由Exchange、Queue、RoutingKey三个才能决定一个从Exchange到Queue的唯一的线路。
Spring Boot整合RabbitMQ
在spring boot项目中只需要引入对应的amqp启动器依赖可,方便的使用RabbitTemplate发送消息,使用注解接收消息。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
生产者工程:
- application.yml文件配置RabbitMQ相关信息;
- 在生产者工程中编写配置类,用于创建交换机和队列,并进行绑定
- 注入RabbitTemplate对象,通过RabbitTemplate对象发送消息到交换机
消费者工程:
- application.yml文件配置RabbitMQ相关信息
- 创建消息处理类,用于接收队列中的消息并进行处理
配置文件
spring:
rabbitmq:
host: 192.168.200.129
port: 5672
virtual-host: /qf
username: test
password: test
工作模式:
1、简单模式 HelloWorld:
一个生产者、一个消费者,不需要设置交换机(使用默认的交换机)
2、工作队列模式 Work Queue: 一个生产者、多个消费者(竞争关系),不需要设置交换机(使用默认的交换机)
3、发布订阅模式 Publish/subscribe:
需要设置类型为fanout的交换机,并且交换机和队列进行绑定,当发送消息到交换机后,交换机会将消息发送到绑定的队列
4、路由模式 Routing:
需要设置类型为direct的交换机,交换机和队列进行绑定,并且指定routing key,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列
5、通配符模式 Topic:
需要设置类型为topic的交换机,交换机和队列进行绑定,并且指定通配符方式的routing key,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列
使用代码创建队列和交换器
初始化队列和交换器类放在消费方和生产方都可以
@Configuration
public class RabbitMQConfig {
/**
* 1. 创建exchange - topic
* 第一个参数: 交换器名称
* 第二个参数: 交换器是否持久化, 也就是服务器重启交换器是否自动删除
* 第三个参数: 如果没有消费者, 交换器是否自动删除
*/
@Bean
public TopicExchange getTopicExchange(){
return new TopicExchange("boot-topic-exchange",true,false);
}
/**
* 2. 创建queue
* 第一个参数: 队列名称
* 第二个参数: 队列是否持久化, 也就是服务器重启队列是否自动删除
* 第三个参数: 是否排外的,有两个作用,
* 1.当连接关闭时该队列是否会自动删除;
* 2.该队列是否是私有的private,如果不是排外的,
* 可以使用两个消费者都访问同一个队列,没有任何问题,如果是排外的,
* 会对当前队列加锁,其他通道channel是不能访问的
* 第四个参数: 队列是否自动删除, 也就是当没有消费者时, 队列是否自动删除
* 第五个参数: 队列参数, 比如是否设置为延时队列等参数.
*/
@Bean
public Queue getQueue(){
return new Queue("boot-queue",true,false,false,null);
}
/**
* 3. 队列和交换器绑定在一起
*/
@Bean
public Binding getBinding(TopicExchange topicExchange,Queue queue){
return BindingBuilder.bind(queue).to(topicExchange).with("*.red.*");
}
}
发布消息到RabbitMQ
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testContextLoads() {
rabbitTemplate.convertAndSend("boot-topic-exchange","slow.red.dog","红色大狼狗!!");
}
创建消费者监听消息
@Component
public class Consumer {
@RabbitListener(queues = "boot-queue")
public void getMessage(Object message){
System.out.println("接收到消息:" + message);
}
}
消息的可靠性(发送可靠)
confim机制(保证发送可靠)
RabbitMQ的事务:事务可以保证消息100%传递,可以通过事务的回滚去记录日志,后面定时再次发送当前消息。事务的操作,效率太低,加了事务操作后,比平时的操作效率至少要慢100倍。
RabbitMQ除了事务,还提供了Confirm的确认机制,这个效率比事务高很多。
Return机制(保证发送可靠)
Confirm只能保证消息到达exchange,无法保证消息可以被exchange分发到指定queue。
而且exchange是不能持久化消息的,queue是可以持久化消息。
采用Return机制来监听消息是否从exchange送到了指定的queue中
编写配置文件
spring:
rabbitmq:
host: 192.168.200.129
port: 5672
virtual-host: /qf
username: test
password: test
publisher-confirms: true
publisher-returns: true
开启Confirm和Return
@Component
public class PublisherConfirmAndReturnConfig implements RabbitTemplate.ConfirmCallback ,RabbitTemplate.ReturnCallback{
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct // init-method
public void initMethod(){
//指定 ConfirmCallback
rabbitTemplate.setConfirmCallback(this);
//指定 ReturnCallback
rabbitTemplate.setReturnCallback(this);
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if(ack){
System.out.println("消息已经送达到Exchange");
}else{
System.out.println("消息没有送达到Exchange");
}
}
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("消息没有送达到Queue");
}
}
手动Ack(保证接收可靠)
在消费方application.yml文件添加下面配置, 改为手动应答机制.
spring:
rabbitmq:
host: 192.168.200.129
port: 5672
virtual-host: /qf
username: test
password: test
listener:
simple:
acknowledge-mode: manual
手动ack
@Component
public class Consumer {
@RabbitListener(queues = "boot-queue")
public void getMessage(String msg, Channel channel, Message message) throws IOException {
System.out.println("接收到消息:" + msg);
try {
int i = 1 / 0;
/**
* 消费者发起成功通知
* 第一个参数: DeliveryTag,消息的唯一标识 channel+消息编号
* 第二个参数:是否开启批量处理 false:不开启批量
* 举个栗子: 假设我先发送三条消息deliveryTag分别是5、6、7,可它们都没有被确认,
* 当我发第四条消息此时deliveryTag为8,multiple设置为 true,会将5、6、7、8的消息全部进行确认。
*/
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
e.printStackTrace();
/**
* 返回失败通知
* 第一个参数: DeliveryTag,消息的唯一标识 channel+消息编号
* 第二个boolean true所有消费者都会拒绝这个消息,false代表只有当前消费者拒绝
* 第三个boolean true消息接收失败重新回到原有队列中
*/
channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,true);
}
}
}
避免消息重复消费
重复消费消息,会对非幂等行操作造成问题
重复消费消息的原因是,消费者没有给RabbitMQ一个ack
- 为了解决消息重复消费的问题,可以采用Redis,在消费者消费消息之前,先将消息的id放到Redis中,
- id-0(正在执行业务)
- id-1(执行业务成功)
- 然后使用ack给RabbitMQ返回消息
- 如果RabbitMQack失败,在RabbitMQ将消息交给其他的消费者时,先执行setnx,如果key已经存在,获取他的值,如果是0,当前消费者就什么都不做,如果是1,直接ack。
- 极端情况:第一个消费者在执行业务时,出现了死锁,在setnx的基础上,再给key设置一个生存时间。
修改生产者
@Test
public void contextLoads() throws IOException {
CorrelationData messageId = new CorrelationData(UUID.randomUUID().toString());
//第四个参数: 设置消息唯一id
rabbitTemplate.convertAndSend("boot-topic-exchange","slow.red.dog","红色大狼狗!!",messageId);
System.in.read();
}
修改消费者
/**
* java中的方法叫做setIfAbsent, redis中的命令叫做setnx
* 作用:
* 如果为空就set值,并返回1, true
* 如果存在(不为空)不进行操作,并返回0, false
*/
@Component
public class Consumer {
@Autowired
private StringRedisTemplate redisTemplate;
@RabbitListener(queues = "boot-queue")
public void getMessage(String msg, Channel channel, Message message) throws IOException {
//0. 获取MessageId, 消息唯一id
String messageId = (String) message.getMessageProperties().getHeaders().get("spring_returned_message_correlation");
//1. 设置key到Redis
if(redisTemplate.opsForValue().setIfAbsent(messageId,"0", 10, TimeUnit.SECONDS)) {
//2. 消费消息
System.out.println("接收到消息:" + msg);
//3. 设置key的value为1
redisTemplate.opsForValue().set(messageId,"1",10,TimeUnit.SECONDS);
//4. 手动ack
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}else {
//5. 获取Redis中的value即可 如果是1,手动ack
if("1".equalsIgnoreCase(redisTemplate.opsForValue().get(messageId))){
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
}
}
}