rabbitmq

1. rabbitmq安装(docker安装) 

注意:版本号中带有management的是带有管理端界面的,没有带的是访问不到15672端口的

docker run \
-e RABBITMQ_DEFAULT_USER=admin \
-e RABBITMQ_DEFAULT_PASS=123456 \
-v 服务器地址:/var/lib/rabbitmq \
-v mq-plugins(数据卷名称):/plugins \
--name rabbit \
--hostname rabbbitHost \
--network bridge \
-p 15672:15672 \
-p 5672:5672 \
rabbitmq:3.12-management

-e 声明环境变量 

-e RABBITMQ_DEFAULT_USER 设置rabbbitmq默认用户名

-e RABBITMQ_DEFAULT_PASS 设置rabbbitmq默认用户密码

-v 磁盘映射

-v 服务器地址:/var/lib/rabbitmq 持久化 rabbitmq 数据,数据映射到服务器的地址

--name 容器名称

--hostname 其他容器访问当前容器时,可通过hostname进行访问

--network 指定容器的网络 bridge网桥模式

-p 端口映射

  • 15672:RabbitMQ提供的管理控制台的端口
  • 5672:RabbitMQ的消息发送处理接口

 

  2.  RabbitMQ的整体架构和核心概念

  • . Virtual-host:虚拟主机,起到隔离作用,每个虚拟主机都是相互独立的,有各自的队列和交换机。默认的virtual-host是/
  • . Publisher:消息发送者
  • . Consumer:消息的 消费者
  • . Queue:队列,用来存储消息的,生产者投递的消息会暂存到队列中,等待消费者处理
  • . Exchange:交换机,负责路由消息的,生产者投递的消息由交换机决定投递到那个队列中。交换机没有存储消息的能力
  • . routingKey: 路由key

rabbitmq_第1张图片

  • 交换机分为:topic(通配符模式) fanout(广播模式) direct(订阅模式)
    1. fanout模式:将消息发送到所有与当前交换机绑定的队列中
    2. direct模式:将消息发送到与当前交换机绑定的指定的routingKey的队列
    3. topic模式:通配符模式,将消息发送到与交换机绑定的指定的routingkey(routingKey可以使用通配符,#匹配一个或者多个单词  *匹配一个单词)的队列中   (发送消息时,routingKey中不能使用通配符)
    4. headers模式:头匹配,基于MQ的消息头匹配

    ps:

        topic模式routingKey使用通配符时,一对多,一个routingKey可以监听多个不带通配符的routingKey

        direct模式routingKey是一对一的

交换机的作用:

  • 接受生产者发送的消息
  • 将消息按照规则(routingKey)路由到与之绑定的队列中
  • 不能存储消息,一旦路由失败则消息丢失

        

生产者发送消息时可直接发送到队列中或者发送到交换机中(交换机通过routingKey来将消息路由到正确的队列中) 

Work queues,任务模型。简单来说就是让多个消费者绑定到一个队列,共同消费队列中的消息,多个消费者绑定到同一队列,同一条消息只能被一个消费者消费,可以使用prefetch来控制预获取的消息数量 

 

3. rabbitMQ在springboot中的使用

rabbitmq采用AMQP的协议,因此它具备了跨语言的特性,任何语言只要尊徐AMQP协议来收发消息,都可以与rabbitmq互通,rabbitmq提供了java客户端编码,不过相对比较复杂,spring官方基于rabbitmq提供的了一套消息收发的模板工具:springAMQP. SpringAMQP提供了三个功能:

  • 自动声明交换机,列表及其绑定关系
  • 基于注解的监听模式,异步接收消息
  • 封装了RabbitTemplate工具,用来发送消息
# 引入springAMQP 

     org.springframework.boot
     spring-boot-starter-amqp

对rabbitMq中消息的序列化,默认是使用jdk序列化,众所周知,jdk序列化存在:数据体积过大,有安全漏洞,可读性查的问题,所以我们可以使用json的方式进行序列化或者反序列化,首先我们要引入jackson依赖,如果项目中引入了spring-boot-starter-web依赖,就无需要重新引入jackson依赖


    com.fasterxml.jackson.dataformat
    jackson-dataformat-xml
    2.9.10

需要在启动类型注入bean方法或者通过applicationContectAware两种方式实现

@Bean
    public Jackson2JsonMessageConverter jsonMessageConverter(){
        Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
        // 给每个消息的头部的Properties中添加message_id,可用于区分不同消息,在实现业务的幂等性中,可以通过唯一id来判断消息是否被处理
        jackson2JsonMessageConverter.setCreateMessageIds(true);
        return jackson2JsonMessageConverter;
    }
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;

/**
 * 描述:
 * @author
 */
@Slf4j
@Configuration
public class RabbitMqJsonConverter implements ApplicationContextAware {
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        RabbitTemplate bean = applicationContext.getBean(RabbitTemplate.class);
        Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
        jackson2JsonMessageConverter.setCreateMessageIds(true);
        // 设置对象序列化方式,默认使用的是jdk的序列化方式
        bean.setMessageConverter(jackson2JsonMessageConverter);
        // 设置publish return
        bean.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
            @Override
            public void returnedMessage(ReturnedMessage returnedMessage) {
                log.info("returnedMessage:exchange:{},message:{},replyCode:{},replyText:{},routingKey:{}",
                        returnedMessage.getExchange(),returnedMessage.getMessage(),
                        returnedMessage.getReplyCode(),returnedMessage.getReplyText(),
                        returnedMessage.getRoutingKey());
            }
        });

    }
}
生产者

生产者配置:需要在application的配置文件中对rabbitMq进行配置

为了保证生产者一定能把消息发送到MQ可以使用:

  1. 生产者重试机制(即如果因为网络原因断开,可以进行重连)(因为springAMQP提供的重试机制是阻塞模式的,也就是说多次重试的等待过程中,当前线程是阻塞的,如果对业务性能有要求的可以关闭重试机制,如果要使用,需要合理的分配重试次数和等待时长。也可以考虑使用异步线程来执行发送消息的代码)
  2. 生产者确认机制(使用publisher return 和 Publisher confirm机制)
    1. 当消息发送到mq当时路由失败时,会通过publisher return返回异常信息,同时返回ack的确认信息,代表投递成功
    2. 临时消息发送到mq并且入队成功,返回ack
    3. 持久消息发送到mq并且入队和持久化成功后,返回ack
    4. 其他情况返回nack        

ack和nack是 Publisher confirm机制的,ack是投递成功;nack是投递失败。而return则属于Publisher Return机制。 默认两种机制都是关闭状态,需要通过配置文件来开启。每个RabbitTemplate只能配置一个ReturnCallback,在上边代码中RabbitMqJsonConverter,已对return进行设置

spring:
  rabbitmq:
    host: rabbit主机地址
    username:用户名
    password: 密码
    virtual-host: hmall
    port: 5673
    template: # 生产者重试机制 springAmqp的重试机制是阻塞的,所以建议使用异步线程来执行消息发送代码
      retry:
        initial-interval: 1000ms # 失败后的初始等待时间
        enabled: true # 开启重试机制
        multiplier: 1 # 等待时间倍数 失败后下次的等待时长倍数,下次等待时长 = initial-interval * multiplier+connection-timeout
        max-attempts: 3 # 最大重试次数
    connection-timeout: 1s # rabbitTemplate链接超时时间
    publisher-confirm-type: correlated # 开启publish confirm机制  correlated :mq异步回调返回回执  simple:mq同步回调返回回执 none关闭confirm机制
    publisher-returns: true # 开启publisher return机制

发送消息:

@Slf4j
@SpringBootTest
public class SpringAmqpTest {

    @Resource
    private RabbitTemplate rabbitTemplate;

    @Test
    public void test1() throws InterruptedException {
        // 生产者消息确认机制
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        correlationData.getFuture().addCallback(new ListenableFutureCallback() {
            @Override
            public void onFailure(Throwable ex) {
                // spring处理失败报错,与mq无关
                log.error("程序处理失败",ex);
            }

            @Override
            public void onSuccess(CorrelationData.Confirm result) {
                if (result.isAck()) {
                    log.debug("操作成功ack");
                }else{
                    log.error("操作失败nack{}",result.getReason());
                }
            }
        });
        // rabbitTemplate.convertAndSend("direct.hmall","direct","你好啊,direct");
        rabbitTemplate.convertAndSend("topic.exchange.hmall","topic.*","你好啊,topic.*",correlationData);

        Thread.sleep(300);
    }



    @Test
    public void sendObject(){
        Map map = new HashMap<>(2);
        map.put("name","刘星");
        map.put("sex","男");
        map.put("age",12);
        rabbitTemplate.convertAndSend("direct.hmall.queue1",map);
    }
}

在开启持久化机制以后,如果同时还开启了生产者确认,那么MQ会在消息持久化以后才发送ACK回执,进一步确保消息的可靠性。 不过出于性能考虑,为了减少IO次数,发送到MQ的消息并不是逐条持久化到数据库的,而是每隔一段时间批量持久化。一般间隔在100毫秒左右,这就会导致ACK有一定的延迟,因此建议生产者确认全部采用异步方式

消费者
消费者确认机制

rabbitmq消费者确认机制,当消费者处理消息结束后,应该向RabbitMQ发送一个回执,告知RabbitMQ自己消息处理状态。回执有三种可选值

  • ack:成功处理消息,RabbitMQ从队列中删除该消息
  • nack:消息处理失败,RabbitMQ需要再次投递消息
  • reject:消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息
logging:
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS
  level:
    com.itheima: debug
spring:
  rabbitmq:
    host: 47.96.6.18
    port: 5672
    virtual-host: hmall
    username: hmallUser
    password: 123456
    listener:
      simple:
        prefetch: 1 # 消费者每次只可以获取一条消息,只有处理完当前的消息后,
                    # 才可以获取下一条消息。如果未设置,则当前队列的所有消费者会平分队列中消息条数即
                    #(队列会轮询分配消息到与之绑定的消费者上,不管消费者是否处理完毕)
        acknowledge-mode: auto  # 消费者确认机制
                                  # **none**:不处理。即消息投递给消费者后立刻ack,消息会立刻从MQ删除。非常不安全,不建议使用
                                  # **manual**:手动模式。需要自己在业务代码中调用api,发送ack或reject,存在业务入侵,但更灵活
                                  # **auto**:自动模式。SpringAMQP利用AOP对我们的消息处理逻辑做了环绕增强,当业务正常执行时则自动返回ack. 当业务出现异常时,根据异常判断返回不同结果:
                                  # 如果是业务异常,会自动返回nack;
                                  # 如果是消息处理或校验异常,自动返回reject;
        retry:
          # 消费者在失败后消息没有重新回到MQ无限重新投递,而是在本地重试了3次
          # 本地重试3次以后,默认的失败处理策略会抛出了AmqpRejectAndDontRequeueException异常。查看RabbitMQ控制台,发现消息被删除了,说明最后SpringAMQP返回的是reject
          enabled: true # 开启消费者重试机制,当消费者出现异常时利用本地重试,而并不是无限制的requeue到mq中
          initial-interval: 1s # 初次失败等待时长为1秒
          max-attempts: 3 # 最大重试次数
          multiplier: 1 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
          stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false
          max-interval: 1000ms
                      
消息处理策略 

Spring允许我们自定义重试次数耗尽后的消息处理策略,这个策略是由MessageRecovery接口来定义的,它有3个不同实现:

  • RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式
  • ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队
  • RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机

 RepublishMessageRecoverer的实现


/**
 * 描述:消息处理异常时,本地重试次数用尽后,将失败消息投递到指定交换机
 * 当spring.rabbitmq.listener.simple.retry.enabled为true时,才会启用当前类中的bean注册
 * @author guomengsi
 */
@Configuration
@ConditionalOnProperty(name = "spring.rabbitmq.listener.simple.retry.enabled",havingValue = "true")
public class MqFailTacticsConfiguration {


    @Resource
    private RabbitTemplate rabbitTemplate;

    @Bean
    public Queue failQueue(){
        return new Queue("MQ_FAIL_QUEUE");
    }


    /**
     * 返回类型需要是具体的交换机类型
     * @return FanoutExchange 广播类型
     */
    @Bean
    public DirectExchange failExchange(){
        return new DirectExchange("MQ_FAIL_EXCHANGE");
    }


    /**
     * 交换机和队列可以通过参数传递,即参数名使用上边bean的函数名,也可以直接在bind(failQueue()).to(failExchange())
     * 因为被bean注释的函数只会实例化一次,下次再使用时,会判断是否被实例化,如果实例化则直接使用
     * @param failQueue 队列
     * @param failExchange 交换机
     * @return Binding
     */
    @Bean
    public Binding exchangeBindToQueue(Queue failQueue,DirectExchange failExchange){
         return BindingBuilder.bind(failQueue).to(failExchange).with("fail");
    }


    @Bean
    public RepublishMessageRecoverer republishMessageRecoverer(){
        return new RepublishMessageRecoverer(rabbitTemplate,"MQ_FAIL_EXCHANGE","fail");
    }
}
消费者如何保证消息一定被消费
  • 开启消费者确认机制,由spring确认消息处理成功后返回ack,异常返回nack

  • 开启消费者失败重试机制,并设置messageRecoverer,多次本地重试,失败后,会将消息投递到异常交换机,由人工处理

兜底方案 

        不能保证mq百分百的可以处理成功,我们可以通过定时任务去主动查询

 业务的幂等性

        业务的幂等性是指在程序开发中,同一个业务执行一次或者多次对业务的影响状态是一样的。处理业务幂等性的方法有两种:

  • 唯一消息id
  • 业务状态判断
唯一消息id 
  1. 每一条消息都生成一个唯一的id,与消息一起投递给消费者。
  2. 消费者接收到消息后处理自己的业务,业务处理成功后将消息ID保存到数据库
  3. 如果下次又收到相同消息,去数据库查询判断是否存在,存在则为重复消息放弃处理
业务判断 

业务判断就是基于业务本身的逻辑或状态来判断是否是重复的请求或消息,不同的业务场景判断的思路也不一样。 例如,处理消息的业务逻辑是把订单状态从未支付修改为已支付。因此我们就可以在执行业务时判断订单状态是否是未支付,如果不是则证明订单已经被处理过,无需重复处理。 

延迟消息

在一段时间后才会被执行的任务(例如,订单超时未支付,需要更改订单状态,库存等),称为延迟任务,而实现延迟任务最简单的方法则是MQ中的延迟消息

在RabbitMQ中实现延迟消息的两种方法:

  • 私信交换机+ttl
  • 延迟消息插件

你可能感兴趣的:(rabbitmq,分布式)