Spring Boot消息队列之RabbitMQ

消息队列

什么是消息队列(message queue)

消息队列是应用系统之间通信的方法,本质是队列,具有先进先出(FIFO)的特点,队列的元素是消息,所以叫消息队列,是一种中间件。

应用场景

  1. 场景说明:用户注册后,需要发送邮件和短信,然后返回客户端完成验证,传统方式有串行和并行方式。(异步处理)
  • 串行方式:将注册信息写进数据库后,再发送邮件,然后发送短信,三个操作完成后才返回客户端,很慢。
  • 并行方式:将注册信息写进数据库后,再同时发送邮件和短信,三个操作完成后返回客户端,比较慢。
  • 消息队列:将注册信息写进数据库后,再写进消息队列,然后返回客户端。由消息队列进行异步操作。
  1. 场景说明:双十一购物节,用户下单后,订单系统调用库存系统的库存接口,传统做法时库存系统出现故障,订单就会失败,订单系统和库存系统高耦合,引入消息队列后:用户下单后,订单系统完成持久化处理,把消息写进消息队列返回到客户端,库存系统订阅下单消息进行库操作。(应用解耦)
  2. 场景说明:秒杀活动,服务器接收到请求后写进消息队列,假如超过消息队列长度则直接抛弃用户请求或者跳转到错误页面(流量削峰),否则才进入秒杀服务系统

mq选择

RabbitMQ的并发性最好,因为是由erlang语言编写的,综合来看也是RabbitMQ最好,基于AMQP协议。RabbitMQ默认的服务端口是5672,后台管理端口是15672。

基本概念

Message:消息,消息是没有名字的,由消息头和不透明的消息体组成,消息体包括routing-key、priority等属性

Publisher:消息生产者

Exchange:交换器,用来接收生产者发送的消息并且将消息按路由传给服务器中的队列。有四种交换器,跟不同的转发策略有关

Queue:消息队列,用于保存消息直到发送给消费者

Binding:绑定,指交换器和消息队列之间基于路由键的关联

Connection:网络连接,位于一个客户端和Broker之间的TCP连接

Channel:信道,用于复用TCP连接的虚拟通道,一个Connection可以包含多个Channel。RabbitMQ建议客户端线程之间不要共用Channel,尽量共用Connection

Consumer:消息消费者,就是客户端线程

Virtual Host:虚拟主机,mini版的mq服务器

Broker:消息队列的服务器主体

Spring Boot消息队列之RabbitMQ_第1张图片

交换器类型和转发策略

  • Direct Exchange和Direct策略

Publisher生产的Message的routing-key如果和Binding中的binding-key相同的话,Exchange就将消息转发到对应的队列中,是完全匹配、单播的模式

  • Fanout Exchange和Fanout策略

忽略掉routing-key,将消息分发到Exchange绑定的所有Queue上,是广播模式

  • Topic Exchange和Topic策略

Message的routing-key与Binding的binding-key进行模糊匹配,交换器转发匹配的消息,用.号分割单词,用#号匹配0个或多个单词,*匹配不多不少一个单词

  • Header Exchange和Header策略

忽略routing-key,Exchange通过Message的头部信息过滤转发,很少用

确保消息正确投递的方法

关于两个属性:durable和autoDelete

exchange和queue的durable属性代表是否将exchange或queue保存在磁盘持久化,在rabbitmq服务器关闭后,如果不持久化则丢失exchange或queue,持久化则不会丢失

queue的autoDelete代表在所有消费者断开连接后是否自动删除队列

exchange的qutoDelete代表在所有绑定队列都不在使用时,是否自动删除交换器

  • 生产者发送确认

    发送确认有两种方法:

    • 事务:重量级的、同步的,开销大
    • 发布确认机制:轻量级的,异步的,开销小

    选择发布确认机制需要做3步:

    1. CachingConnectionFactory的publisherConfirms属性为true
    2. Producer调用setConfirmCallback(ConfirmCallback callback),Confirms就会回调给生产者
    3. Consumer需考虑消息去重处理
  • 消费者接收确认

    接受确认可以通过ACK机制来确认消息是否被正确接收,可以手动ACK和自动ACK,在ACK后,消息会被队列删除。

    自动ACK在消息发送后立即ACK,假如消费者在消息接收过程中网络异常,消息就有丢失的可能。手动ACK则可以在接收到消息之后再进行确认。

    假如某服务忘记ACK,则RabbitMQ不再发送此类消息给消费者。

    默认是不确认,即默认消费者都消费成功

五种常用的消息队列

Spring Boot消息队列之RabbitMQ_第2张图片

  • 简单模式

Spring Boot消息队列之RabbitMQ_第3张图片

一个队列只有一个消费者,应用于单发邮件、短信

  • 工作模式

Spring Boot消息队列之RabbitMQ_第4张图片

一个队列有多个消费者,应用于抢红包,抢券

上面两种主要描述的是消费者的结构,下面三种模式主要是描述生产者的结构

  • 发布订阅(fanout交换器)

Spring Boot消息队列之RabbitMQ_第5张图片

生产者生产的消息发送fanout交换器,fanout交换器把消息发送给所有队列,应用于群发广告

  • 路由模式(direct交换器)
    Spring Boot消息队列之RabbitMQ_第6张图片

生产者生产的消息发送direct交换器,direct交换器把消息发送给对应的某个队列,应用于聊天对话

  • 主题模式(topic交换器)
    Spring Boot消息队列之RabbitMQ_第7张图片

生产者生产的消息发送topic交换器,topic交换器把消息发送给对应的某个队列,应用于按类分派

以下为示例代码

/*
* pom.xml
*/
<!-- rabbitmq -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
/*
* application.yml
*/
#=================== rabbitmq配置 ======================
rabbitmq:
host: 127.0.0.1
port: 5672
virtual-host: testHost
username: guest
password: guest
/*
* RabbitMQConfig.java
*/

@Slf4j
@Configuration
@ConfigurationProperties(prefix = "spring.rabbitmq")
public class RabbitMQConfig {

    /**
     * 主机地址
     */
    private String host;

    /**
     * 端口
     */
    private Integer port;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 虚拟主机
     */
    private String virtualHost;

    //getter and setter..

    @Bean
    public ConnectionFactory rabbitmqConnectionFactory(){

        //ConnectionFactory首选CachingConnectionFactory,可缓存生产者和消费者
        CachingConnectionFactory  cachingConnectionFactory = new CachingConnectionFactory(host,port);
        cachingConnectionFactory.setUsername(username);
        cachingConnectionFactory.setPassword(password);
        cachingConnectionFactory.setVirtualHost(virtualHost);
        //发布确认机制确保消息正确投递
        //启用PublisherConfirms
        //producer发送message给exchange,回调ConfirmCallback的confirm方法
        //如果message到达exchange,则ack=true
        //如果message没到达exchange,则ack=false
        cachingConnectionFactory.setPublisherConfirms(true);
        //启用PublisherReturns
        //exchange传递消息给queue失败时,回调ReturnCallback的returnedMessage方法
        //PublisherReturns需启用RabbitTemplate的Mandatory,消息才会返回给生产者,否则直接丢弃
        cachingConnectionFactory.setPublisherReturns(true);
        return cachingConnectionFactory;
    }

    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory rabbitmqConnectionFactory){

        RabbitTemplate rabbitTemplate = new RabbitTemplate(rabbitmqConnectionFactory);
        //设置MessageConverter
        rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
        //设置接收确认为manual手动
        //启用Mandatory
        rabbitTemplate.setMandatory(true);
        //重写ConfirmCallback的confirm方法
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                if(ack){
                    System.out.println(String.format("消息发送成功:correlationData(%s) , ack(%b) , cause(%s)",correlationData,ack,cause));
                    log.info("消息发送成功:correlationData({}) , ack({}) , cause({})",correlationData,ack,cause);
                }else{
                    System.out.println(  String.format("消息发送失败:correlationData(%s) , ack(%b) , cause(%s)",correlationData,ack,cause));
                    log.info("消息发送失败:correlationData({}) , ack({}) , cause({})",correlationData,ack,cause);
                }
            }
        });
        //重写ReturnCallback的returnedMessage方法
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                System.out.println(  String.format("消息丢失:message(%s) , replyCode(%b) , replyText(%s) , exchange(%s) , routingKey(%s)",message,replyCode,replyText,exchange,routingKey));
                log.info("消息丢失:message({}) , replyCode({}) , replyText({}) , exchange({}) , routingKey({})",message,replyCode,replyText,exchange,routingKey);
            }
        });
        return rabbitTemplate;
    }

    @Bean
    public SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory(ConnectionFactory rabbitmqConnectionFactory){
        SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory = new SimpleRabbitListenerContainerFactory();
        //关联rabbitmq连接工厂
        simpleRabbitListenerContainerFactory.setConnectionFactory(rabbitmqConnectionFactory);
        //设置消息序列化器
        simpleRabbitListenerContainerFactory.setMessageConverter(new Jackson2JsonMessageConverter());
        //设置接收消息确认模式为手动确认
        simpleRabbitListenerContainerFactory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        return simpleRabbitListenerContainerFactory;
    }
}

/*
* RabbitMQService.java
*/
public interface RabbitMQService {

    /**
     * 创建单播交换器
     * @param exchangeName
     */
    void createDirectExchange(String exchangeName);

    /**
     * 创建广播交换器
     * @param exchangeName
     */
    void createFanoutExchange(String exchangeName);

    /**
     * 创建模式匹配交换器
     * @param exchangeName
     */
    void createTopicExchange(String exchangeName);

    /**
     * 创建队列
     * @param queueName
     */
    void createQueue(String queueName);

    /**
     * 创建交换器和队列的绑定
     * @param exchangeName
     * @param queueName
     * @param routingKey
     */
    void bindExchangeQueue(String exchangeName, String queueName,String routingKey);
}
/*
* RabbitMQServiceImpl.java
*/
@Service
public class RabbitMQServiceImpl implements RabbitMQService {

    @Autowired
    private AmqpAdmin amqpAdmin;


    @Override
    public void createDirectExchange(String exchangeName) {
        amqpAdmin.declareExchange(new DirectExchange(exchangeName,true,false));
    }

    @Override
    public void createFanoutExchange(String exchangeName) {
        amqpAdmin.declareExchange(new FanoutExchange(exchangeName,true,false));
    }

    @Override
    public void createTopicExchange(String exchangeName) {
        amqpAdmin.declareExchange(new TopicExchange(exchangeName,true,false));
    }

    /**
     * 创建队列
     * @param queueName
     */
    @Override
    public void createQueue(String queueName) {
        amqpAdmin.declareQueue(new Queue(queueName,true,false,false));
    }

    /**
     * 声明交换器和队列间的绑定
     * @param exchangeName
     * @param queueName
     */
    @Override
    public void bindExchangeQueue(String exchangeName, String queueName,String routingKey) {
        amqpAdmin.declareBinding(new Binding(queueName,Binding.DestinationType.QUEUE,exchangeName,routingKey,null));
    }
}
/*
* ProducerService.java
*/
public interface ProducerService {

    void send();
}
/*
* ProducerServiceImpl.java
*/
@Service
public class ProducerServiceImpl implements ProducerService {

    @Resource
    private RabbitTemplate rabbitTemplate;

    @Override
    public void send() {

        for(int i=0;i<200;i++){

            Map<String,Object> map = new HashMap<>();
            map.put("msg","这是第【"+i+"】个消息");
            map.put("data", Arrays.asList("helloworld",123,true));
            //exchange:交换器名字
            //routing-key:消息的路由键
            //object:消息对象,自动序列化
            rabbitTemplate.convertAndSend("exchange.direct","atguigu.news",map);
        }

    }
}
/*
* ConsumerService.java
*/
public interface ConsumerService {

    void receive(Message message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag , @Headers Map<String,Object> headers);

}
/*
* ConsumerServiceImpl.java
*/
@RabbitListener(queues = "atguigu.news" ,  containerFactory = "simpleRabbitListenerContainerFactory")
@Service
public class ConsumerServiceImpl implements ConsumerService {

    @Resource
    private RabbitTemplate rabbitTemplate;

    @Override
    @RabbitHandler
    public void receive(Message message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag , @Headers Map<String,Object> headers) {

        if(headers.get("error") == null){
            //正确接收
            try{
                Object obj = rabbitTemplate.getMessageConverter().fromMessage(message);
                Map<String,Object> map = (Map<String,Object>)obj;
                System.out.println("消费者【1】接收到消息:"+map);
                //手动确认
                //deliveryTag:当消费者向rabbitmq注册后,会建立一个channel,这个channel内的消息标志就是tag,自增
                //multiple:是否一次确认deliveryTag<=tag的所有消息
                channel.basicAck(tag,false);
                //可批量拒绝的basicNack
                //deliveryTag,multiple,requeue:是否重新入队列
                //channel.basicNack(tag,false,true);
                //单个拒绝的basicReject
                //deliveryTag,requeue
                //channel.basicReject(tag,false);
            }catch (Exception e){
                e.printStackTrace();
            }
        }else{
            try {
                //错误接收
                //单个拒绝的basicReject
                //deliveryTag,requeue
                channel.basicReject(tag,false);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}
/*
* ProducerServiceImplTest.java
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class ProducerServiceImplTest {

    @Autowired
    private ProducerServiceImpl producerService;

    @Test
    public void send() {
        producerService.send();
        try {
            //假如没有停止2秒的话,测试程序结束马上关闭线程了,此时消息不一定能发送到交换器或者队列
            Thread.sleep(10000);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
/*
* ConsumerServiceImplTest.java
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class ConsumerServiceImplTest {

    @Autowired
    @Qualifier(value = "consumerServiceImpl")
    private ConsumerService consumerService1;

    private ConsumerService consumerService2;

    @Test
    public void receiveMsg() {
        try{
            Thread.sleep(2000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

你可能感兴趣的:(JAVA)