上一篇讲了如何在Mac上安装rabbit mq,现在在springboot框架下使用rabbit mq,初次尝试消息队列的使用。
MQ全称(Message Queue)又名消息队列,是一种异步通讯的中间件。我们可以将它理解成邮局,发送者将消息传递到邮局,然后由邮局帮我们发送给具体的消息接收者(消费者),具体发送过程与时间我们无需关心,它也不会干扰我进行其它事情。常见的MQ有kafka、activemq、zeromq、rabbitmq 等等…
我们先来了解下消息队列中涉及的一些术语的含义。
Broker:简单来说就是消息队列服务器实体。
Exchange:消息交换机,它指定消息按什么规则,路由到哪个队列。
Queue:消息队列载体,每个消息都会被投入到一个或多个队列。
Binding:绑定,它的作用就是把exchange和queue按照路由规则绑定起来
Routing Key:路由关键字,exchange根据这个关键字进行消息投递。
vhost:虚拟主机,一个broker里可以开设多个vhost,用作不同用户的权限分离。
producer:消息生产者,就是投递消息的程序。
consumer:消息消费者,就是接受消息的程序。
channel:消息通道,在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务。
消息队列中最重要的三个概念是,生产者,消息队列,消费者是。生产者发消息到消息队列中去,消费者监听指定的消息队列,并且当消息队列收到消息之后,接收消息队列传来的消息,并且给予相应的处理。消息队列常用于分布式系统之间互相信息的传递。
对于RabbitMQ来说,除了这三个基本模块以外,还添加了一个模块,即交换机(Exchange)。
它使得生产者和消息队列之间产生了隔离,生产者将消息发送给交换机,而交换机则根据调度策略把相应的消息转发给对应的消息队列。那么RabitMQ的工作流程如下所示:
左侧 P 代表 生产者,也就是往 RabbitMQ 发消息的程序。
中间即是 RabbitMQ,其中包括了 交换机 和 队列。
右侧 C 代表 消费者,也就是往 RabbitMQ 拿消息的程序。
交换机的主要作用:是接收相应的消息并且绑定到指定的队列.交换机有四种类型,分别为Direct, topic, headers, Fanout.
Direct:RabbitMQ默认的交换机模式,也是最简单的模式。即创建消息队列的时候,指定一个BindingKey。当发送者发送消息的时候,指定对应的Key,当Key和消息队列的BindingKey一致的时候,消息将会被发送到该消息队列中。
Topic:转发信息主要是依据通配符,队列和交换机的绑定主要是依据一种模式(通配符+字符串),而当发送消息的时候,只有指定的Key和该模式相匹配的时候,消息才会被发送到该消息队列中。
Headers:也是根据一个规则进行匹配,在消息队列和交换机绑定的时候会指定一组键值对规则,而发送消息的时候也会指定一组键值对规则,当两组键值对规则相匹配的时候,消息会被发送到匹配的消息队列中。
Fanout:是路由广播的形式,将会把消息发给绑定它的全部队列,即便设置了key,也会被忽略。
接下来,我们看在springboot中如何使用Rabbit MQ消息队列(以Direct 模式为例)。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
### RabbitMQ 相关配置
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /
#开启发送确认
publisher-confirms: true
#开启发送失败退回
publisher-returns: true
#开启ack
listener:
simple:
acknowledge-mode: manual
ps:
这里端口是5672,不是15672…15672是管理端的端口!
@Configuration
public class RabbitConfig {
private static final Logger log = LoggerFactory.getLogger(RabbitConfig.class);
@Bean
public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(new Jackson2JsonMessageConverter());
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
return factory;
}
@Bean
public RabbitTemplate rabbitTemplate(CachingConnectionFactory connectionFactory,MessageConverter messageConverter) {
connectionFactory.setPublisherConfirms(true);
connectionFactory.setPublisherReturns(true);
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
//消息发送失败后回到队列,yml配置 publisher-returns: true
rabbitTemplate.setMandatory(true);
//消息确认, yml配置 publisher-confirms: true
rabbitTemplate.setConfirmCallback((correlationData, ack, cause)
-> log.info("消息发送成功:correlationData({}),ack({}),cause({})", correlationData, ack, cause));
//消息确认, yml配置 publisher-returns: true
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey)
-> log.info("消息丢失:exchange({}),route({}),replyCode({}),replyText({}),message:{}", exchange, routingKey, replyCode, replyText, message));
rabbitTemplate.setMessageConverter(messageConverter);
return rabbitTemplate;
}
@Bean
public MessageConverter messageConverter() {
return new ContentTypeDelegatingMessageConverter(new Jackson2JsonMessageConverter());
}
}
ps:
如果你设置了手动ACK,不仅要在application.yml中开启manual,也一定要在配置类中添加:factory.setAcknowledgeMode(AcknowledgeMode.MANUAL); 不然会double ACK,会导致channel error。
@Service("rabbitmqSender")
public class RabbitmqSender {
private static final Logger logger = LoggerFactory.getLogger(RabbitmqSender.class);
@Autowired
private RabbitTemplate rabbitTemplate;
public void send (String exchange, String routingKey, Object message){
logger.info("RabbitmqSender: 使用消息队列发送: "+"发送时间:"+new Date()+" 发送内容:" +message);
rabbitTemplate.convertAndSend(exchange, routingKey, message);
}
}
@Component
@RabbitListener(queues = "student")
public class RabbitmqReciever {
private static final Logger log = LoggerFactory.getLogger(RabbitmqReciever.class);
@RabbitListener(queues = "student")
public void listenerAutoAck(Message message, Channel channel) {
// TODO 如果手动ACK,消息会被监听消费,但是消息在队列中依旧存在,如果 未配置 acknowledge-mode 默认是会在消费完毕后自动ACK掉
final long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
log.info("消息队列消费者监听到消息::"+new String(message.getBody(),"utf-8"));
// TODO 通知 MQ 消息已被成功消费,可以ACK了
channel.basicAck(deliveryTag, false);
} catch (IOException e) {
try {
// TODO 处理失败,重新压入MQ
log.error(e.getMessage());
channel.basicRecover();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
ps:
我这里监听的是student队列,我并没有在代码中创建exchange和queue,而是在rabbit mq的管理平台上创建了对应的exchange 和 queue 还有绑定的key值,这样,我们在项目中需要使用到消息队列推送消息的时候,就把对应的exchange,queue,key的值传入即可。
@Override
public Student findByName(String name) {
/* String key = "stu_name"+name;
ValueOperations operations = redisTemplate.opsForValue();
boolean hasKey = redisTemplate.hasKey(key);
Student student = new Student();
if(hasKey){
student = operations.get(key);
log.info("从缓存中获取数据!");
}else{
student = studentDao.findByName(name);
log.info("从数据库中获取数据,并加入缓存!");
//插入缓存
operations.set(key,student,5,TimeUnit.MINUTES);
}*/
Student student = new Student();
student = studentDao.findByName(name);
String message = name + "学生已经找到!";
rabbitmqSender.send("student.exchange","student.key",message);
return student;
}