RabbitMQ入门

1.分布式消息队列架构设计分析

1.1. 分布式消息队列(MQ)应用场景

  • 异步缓冲
    有些业务操作可以进行异步的,只要做到最终一致性即可,不用强一致性,这种业务场景就可以使用MQ来做
    场景举例:用户注册以后,需要发送注册邮件和注册的提示短信。

    • 串行的方式
    • 并行方式
      (1) 串行方式:
      在这里插入图片描述

    (2) 并行方式
    在这里插入图片描述

    在正常情况下,我们的邮件和短信都需要等待一个确认返回,才知道整个过程是否完成,目前的解决方式还是强一致性的,如果发邮件或发短信服务出现问题,这个时候服务就无法确保正常了
    如果引入了消息队列:
    在这里插入图片描述

    正常情况下只要将后续消息发送到MQ,业务就认为完成了,与后续的操作完全解耦

  • 服务解耦

    • 强依赖:使用dubbo或springcloud进行服务的调用和连接都是强依赖
    • 弱依赖:可以选择使用MQ来做中间的连接
      • 不代表弱依赖就可以失败
      • 如果不能失败就要保证上游的消息发布端数据投递的可靠性
        场景举例:用户下单后,订单需要更新库存
        强依赖下会出现的问题:
        1)假如库存系统无法访问,则订单减库存失败,从而导致订单生成失败
        2)订单模块和库存模块是强耦合的
        3)如果启用一个线程做离线操作,只是做了异步访问,访问只是提升速度,是否正常调用成功是无法保证的
        通过弱依赖来解决以上问题:
        1)订单生产成功写入消息到消息队列(保证消息的可靠投递)
        2)库存系统通过订阅消息获取下单信息,库存系统根据下单信息进行库存操作
        3)如果库存系统出现异常,库存消费消息失败的情况下消息就重回队列了,等待下次发送
  • 削峰和填谷

    • 当我们下游服务处理不过来的时候,就可以将这些消息缓存在一个地方,逐步处理
    • 将短暂一段时间的业务积压在后面缓慢执行就是削峰和填谷的过程
      场景举例:比如我们的秒杀活动
  • 消息通讯

    • 点对点通讯
    • 发布订阅模式

1.2. 分布式消息队列应用需要思考的内容

如果业务场景需要使用MQ,需要关注以下几个方面

  • 生产端的可靠性投递
    • 如果消息和钱有关,这个消息一定不能丢失
    • 需要做到生产端100%投递,就需要和业务数据保证原子性
  • 消费端的幂等
    • 生产端如果要做到可靠性投递,可能会有重复投递
    • 消费端消费了两次或多次这个数据可能会不一致
    • 所以消费端一定要做到同一个请求消费多次得到的结果一样
  • MQ本身需要考虑的问题
    • HA:高可用
    • 低延迟
    • 可靠性:确保数据是完整的
    • 堆积能力:这是MQ能扛下你的业务量级的保证
    • 扩展性:是否能够天然支持横向扩展无感知扩容

1.3. 业界主流分布式消息队列分析

1.3.1.技术选型

需要关注的地方

  • 各个MQ的性能,优缺点,相应的业务场景
    • 比如ActiveMQ适合传统业务公司,不适合大型高并发海量数据的业务场景
    • 比如RabbitMQ的横向扩展能力就不是非常强
  • 集群架构模式的分析:分布式、可扩展、高可用、可维护性
  • 综合成本、集群规模、人员学习成本
    • 如果消息的可靠性要求和依赖不是特别高,就可以使用kafka
    • kafka可以在很廉价的机器上有着很高的吞吐量和性能表现
  • 未来的规划和方向思考

1.3.2. RabbitMQ集群架构原理解析

  • 主备模式
    master-slave结构,可以理解为热备份,master负责读写,master宕机后切换到slave
  • 镜像模式(Mirror)
    业界主流使用比较多的模式
    RabbitMQ集群非常经典的就是镜像模式,保证数据100%不丢失
    高可用、数据同步低延迟、奇数个节点
    在这里插入图片描述
    镜像队列集群的缺陷是无法进行很好的横向扩容,因为每个节点都是一个完整的互相复制的节点,并且镜像节点过多也会增加MQ的负担,一个数据写入就要复制到多个节点,吞吐量也会降低
  • 多活模式
    类似远程拷贝,做一个异地的容灾和双活,或者是数据转储的功能
    在这里插入图片描述

2. RabbitMQ应用实战

2.1. RabbitMQ核心概念的掌握

RabbitMQ是一个开源的消息代理和队列服务器,RabbitMQ是基于AMQP协议的,RabbitMQ的优点如下:

  • 采用Erlang语言进行开发作为底层语言实现:Erlang有着和原生Socket一样的延迟,所以性能非常高
  • 开源、性能优秀,稳定性保障
  • 提供可靠性消息投递模式(confirm)、返回模式(return)
  • 与SpringAMQP完美整合,API丰富
  • 集群模式比较丰富,表达式配置,HA模式,镜像队列模型
  • 保证数据不丢失的前提做到高可靠性,可用性

高级消息队列协议-AMQP(Advanced Message Queuing Protocol)

AMQP核心部件的组成结果如下
在这里插入图片描述
消息的生产者将消息投递到Server上,经过Virtual host到Exchange就可以了,消息者只需要和Message Queue进行监听和绑定,从而实现了队列级别的解耦,生产者不需要关心消息到哪个队列,只需要将消息投递给Exchange,消费者只需要监听队列即可,他们之间的关系是通过路由来进行关联的,Exchage和Message Queue之间绑定关系通过Routing key进行关联,消息发送到Exchange上然后通过某种路由规则把消息路由到某个Message Queue上

AMQP的专有名词解释

  • Server:又称为Broker
  • Connection:连接,应用程序和broker之间的网络连接
  • Channel:网络信道,一个网络会话的任务
  • Message:消息
  • Virtual host:虚拟地址,用于进行逻辑隔离,最上层的消息路由,一个Virtual host里面可以有若干个Exchage和Queue,但同一个Virtual host里面不能有相同名称的Exchage和Queue
  • Exchange:交换机,接收消息,根据路由键转发消息到绑定队列
  • Binding:Exchage和Queue之间的虚拟连接,binding中可以包含routing key
  • Routing key:一个路由规则
  • Queue:保存具体的消息的容器

RabbitMQ消息的流转
在这里插入图片描述
主要定义

  • Broker: 简单来说就是消息队列服务器实体
  • Exchange: 消息交换机,它指定消息按什么规则,路由到哪个队列
  • Queue: 消息队列载体,每个消息都会被投入到一个或多个队列
  • Binding: 绑定,它的作用就是把exchange和queue按照路由规则绑定起来
  • Routing Key: 路由关键字,exchange根据这个关键字进行消息投递
  • VHost: 虚拟主机,一个broker里可以开设多个vhost,用作不同用户的权限分离。
  • Producer: 消息生产者,就是投递消息的程序
  • Consumer: 消息消费者,就是接受消息的程序
  • Channel: 消息通道,在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务

由Exchange、Queue、RoutingKey三个才能决定一个从Exchange到Queue的唯一的线路。

工作模式:

1、简单模式 HelloWorld : 一个生产者、一个消费者,不需要设置交换机(使用默认的交换机)

2、工作队列模式 Work Queue: 一个生产者、多个消费者(竞争关系),不需要设置交换机(使用默认的交换机)

3、发布订阅模式 Publish/subscribe: 需要设置类型为fanout的交换机,并且交换机和队列进行绑定,当发送消息到交换机后,交换机会将消息发送到绑定的队列

4、路由模式 Routing: 需要设置类型为direct的交换机,交换机和队列进行绑定,并且指定routing key,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列

5、通配符模式 Topic: 需要设置类型为topic的交换机,交换机和队列进行绑定,并且指定通配符方式的routing key,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列

2.2. RabbitMQ的安装和使用

操作系统centOS 7.x

# 先修改主机名称
vi /etc/hostname
HOST232
# 在hosts里增加主机和当前ip的映射
vi /etc/hosts
192.168.0.232 HOST232
# 重启服务器:reboot
# 1.安装需要的辅助工具
yum -y install build-essential openssl openssl-devel unixODBC unixODBC-devel make gcc gcc-c++ kernel-devel m4 ncurses-devel tk tc xz
# 2.下载安装包
wget www.rabbitmq.com/releases/erlang/erlang-18.3-1.el7.centos.x86_64.rpm
wget http://repo.iotti.biz/CentOS/7/x86_64/socat-1.7.3.2-5.el7.lux.x86_64.rpm
wget www.rabbitmq.com/releases/rabbitmq-server/v3.6.5/rabbitmq-server-3.6.5-1.noarch.rpm
# 3.按照顺序安装rpm,使用rpm一键安装
rpm -ivh erlang-18.3-1.el7.centos.x86_64.rpm
rpm -ivh socat-1.7.3.2-5.el7.lux.x86_64.rpm
rpm -ivh rabbitmq-server-3.6.5-1.noarch.rpm
# 4.修改用户登录和连接心跳,使用rabbitMQ自己的管理控制台
vi /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.5/ebin/rabbit.app
将 loopback_users 中的 <<"guest">>,修改为 guest
将 {heartbeat, 60} 修改为 {heartbeat, 10} # 用于进行心跳的间隔
# 5.启动rabbitmq的服务端
/etc/init.d/rabbitmq-server start | stop | status | restart
rabbitmq-server start &
# 6.查看MQ端口是否启用:yum -y install lsof
lsof -i:5672
# 7.安装控制台插件
/etc/init.d/rabbitmq-plugins list # 查看都有哪些插件
rabbitmq-plugins enable rabbitmq_management # 启用插件
lsof -i:15672 # 查看管理后台是否启动
# 8.登录管理控制台
http://39.103.140.196:15672/ # 登录的用户名密码都是guest

在这里插入图片描述Disc就是指磁盘存储消息,如果想要以内存方式存储在启动rabbitMQ时加上 --ram 即可,能够提升效率
常用命令

# 关闭应用
rabbitmqctl stop_app
# 启动应用
rabbitmqctl start_app
# 节点状态
rabbitmqctl status
# 添加用户密码
rabbitmqctl add_user username password
# 修改用户密码
rabbitmqctl change_password username password
# 列出所有用户
rabbitmqctl list_users
# 删除用户
rabbitmqctl delete_user username
# 列出用户权限
rabbitmqctl list_user_permissions username
# 清除用户权限
rabbitmqctl clear_permissions -p vhostpath username
# 设置用户权限
# 三个*对应:configure write read
rabbitmqctl set_permissions -p vhostpath username ".*" ".*" ".*"
rabbitmqctl set_permissions -p / gavin ".*" ".*" ".*"
# 列出所有虚拟主机
rabbitmqctl list_vhosts
# 创建虚拟主机
rabbitmqctl add_vhost vhostpath
# 列出虚拟主机的权限
rabbitmqctl list_permissions -p vhostpath
# 删除虚拟主机
rabbitmqctl delete_vhost vhostpath
# 查看所有队列
rabbitmqctl list_queues
# 清除队列里的消息
rabbitmqctl -p vhostpath purge_queue queueName
# 清除所有数据
rabbitmqctl reset # 这个动作最好在MQ服务停掉后操作

3. RabbitMQ整合Springboot

  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

建立yaml配置

spring:
  rabbitmq:
    host: 39.103.140.196
    username: guest
    password: guest
    virtual-host: /
    connection-timeout: 15000

3.2. 发送一个消息
1、先创建一个要发送的实体对象


@Data
public class OrderInfo implements Serializable {
    private String id;
    private String order_name;
    //消息id是用来生成一个消息的唯一id,通过消息id能找到这个消息的业务信息
    private String message_id;
}

2、编写发送类

@Component
public class OrderSender {

    @Autowired
    RabbitTemplate rabbitTemplate;

    public void sendOrder(OrderInfo orderInfo) throws Exception{
        //4个参数
        //exchagen:发送消息的交换机
        //routingkey
        //object:具体的消息对象
        //correlationData:消息唯一id
        CorrelationData correlationData = new CorrelationData();
        correlationData.setId(orderInfo.getMessage_id());
        rabbitTemplate.convertAndSend("order-exchange","order.abc",orderInfo,correlationData);
    }
}

这个时候是否可以发送消息了?

不行,因为order-exchange和他绑定的队列还没有创建

  • 去rabbitmq的控制台创建order-exchange
    在这里插入图片描述
    Exchange的相关属性说明

  • Name:exchange的名称

  • Type类型说明

    • direct:exchange在和queue进行binding时会设置routingkey,只有routingkey完全相同,exchange才会将消息转发到对应的queue上,相当于点对点
    • fanout:直接将消息路由到所有绑定的队列中,无需对消息的routingkey进行匹配操作,因为不绑定routingkey,所有也是消息转发最快的(广播方式)
    • topic:此类型的exchange和direct差不多,但direct类型要求routingkey完全相同,而topic可以使用通配符:‘’,‘#’
      其中‘
      ’表示匹配一个单词,‘#’则表示匹配没有或者多个单词
    • header:路由规则是根据header来判断
    • 总结:一般direct和topic用来具体的路由信息,如果用广播就使用fanout,header类型用的比较少
  • Durability:Durable是持久化到磁盘的意思

  • Auto Delete:如果设置为yes,那么最后一个绑定到exchange上的队列被删除后,exchange也会自动删除

  • Internal:如果为yes则表明该exchange是rabbitmq内部使用,不提供给外部系统应用,一般是在自己编写erlang语言做定制化扩展时使用

  • Arguments这个是扩展AMQP协议时自定义使用的内容

去rabbitmq的控制台创建order-queue
在这里插入图片描述Exchange和Queue通过Binding关联,由routingkey进行路由
在exchange里和queue里都可设置binding,在exchange里设置一下
在这里插入图片描述

* : routingkey就可以写成 order.*,order.abc / order.123 / order.dfg 这三个都可以路由到order-queue,但是order.123.abc就不行,因为order后面的单词又多个
# : 代表匹配了多个单词,order.abc / order.123.abc 这两个都可以匹配

3 测试

@RestController
public class RabbitController {

    @Autowired
    OrderSender orderSender;

    @GetMapping("sender")
    public String sender(){
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setId("10001");
        orderInfo.setMessage_id("ms10001");
        orderInfo.setOrder_name("测试的消息");

        try {
            orderSender.sendOrder(orderInfo);
        }catch (Exception ex){
            ex.printStackTrace();
        }
        return "--消息发送成功--";
    }
}

在这里插入图片描述
注意:

  • 一个exchange可以绑定多个queue,只要routingkey一样,一个消息就会发送到多个queue上
  • exchange绑定一个queue,无论binding多少个routingkey,只要符合这个routingkey规则的消息都会发送到这个队列中,接收的时候无论从哪个routingkey过来的消息,连接这个队列的消费端都会消费掉,相当于多个消息规则对应一个队列

3.3. 接收一个消息

spring:
  rabbitmq:
    host: 39.103.170.186
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    connection-timeout: 15000
    listener:
      simple:
        concurrency: 5 # 初始化并发数
        max-concurrency: 10 # 最大并发数
        auto-startup: true # 自动开启监听
        prefetch: 1 # 每个连接同一时间最多处理几个消息,限流设置
        acknowledge-mode: manual # 签收模式为手动签收

接收消息的实现类

@Component
public class OrderReceiver {

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = "order-queue",durable = "true"),
            exchange = @Exchange(value = "order-exchange",durable = "true",type = "topic"),
            key = "order.*"
        )
    )
    @RabbitHandler
    public void onOrderMessage(@Payload OrderInfo orderInfo,
                               @Headers Map<String,Object> headers,
                               Channel channel) throws Exception{
        //消费者操作
        System.out.println("-----消息收到开始消费-----");
        System.out.println("Order Name:"+orderInfo.getOrder_name());
        Long deliveryTag = (Long)headers.get(AmqpHeaders.DELIVERY_TAG);
        //因为是手动签收,所以需要进行ACK一下
        //deliveryTag是签收标识,false是不支持批量签收
        channel.basicAck(deliveryTag,false);
    }
}

4. 消息接收的ACK和NACK机制

  • ACK刚刚在上面的代码里已经体验过了,如果把消息消费设置成手工模式,在MQ没有接收到ACK信息时消息就是unacked状态的,在这个状态下消费端不会再次接收这个消息,如果因为在执行过程中出现异常而没有ACK的动作,消息时不会重试
  • 如果系统出现宕机导致没有ACK操作,消息仍停留在MQ中,消息不会发给消费端的,只有消费端服务重启后才会再次接收到这个消息
  • NACK就是业务过程中因业务异常而未执行成功的消息就可以通过NACK重回队列的队首,这个可以用在重试机制中,如果业务出错异常就调用NACK,必须要设置重回队列的次数,记录重回超过N次后,就进行ACK操作不要在重回了,不重回之后将消息放入其他补偿队列中,后续进行人工补偿,如果不设置重试次数则会导致队列一直处在NACK的状态导致后面的消息阻塞。

5. 消息队列的TTL

什么是TTL

  • TTL是Time To Live的缩写,也就是生存时间
  • RabbitMQ支持消息的过期时间,在消息发送时可以进行指定
  • RabbitMQ也支持队列的过期时间,从消息进入这个队列开始计算,只要消息存活时间超过了队列配置的超时时间,消息就会自动清除

实现消息队列内部的过期时间等
在这里插入图片描述

  • x-message-ttl:单位毫秒,该消息队列里所有消息从进入开始计算时间,打到配置的值后自动清除
  • x-expires:单位毫秒,队列处于不活跃状态多少毫秒后将自动删除
  • x-max-length:队列最多存放多少条消息,如果达到阈值,新消息进入会将最早的一条删除
  • x-max-length-bytes:队列最大存放的容量,如果达到阈值,新消息进入后将会删除最早一条,如果还存不下则继续删除第二条

实现消息本身的TTL

@Component
public class OrderSender {

    @Autowired
    RabbitTemplate rabbitTemplate;

    public void sendOrder(OrderInfo orderInfo) throws Exception{
        //4个参数
        //exchagen:发送消息的交换机
        //routingkey
        //object:具体的消息对象
        //correlationData:消息唯一id
        CorrelationData correlationData = new CorrelationData();
        correlationData.setId(orderInfo.getMessage_id());

        MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                message.getMessageProperties().setExpiration("5000");
                return message;
            }
        };

        rabbitTemplate.convertAndSend("order-exchange","order.abc",orderInfo,messagePostProcessor,correlationData);
    }
}

6. 死信队列的详解和触发机制

首先看一下什么是死信:当一个消息无法被消费时就会变成死信,死信怎么形成的需要分析一下

什么是死信队列:DLX,Dead-Letter-Exchange

当消息在一个队列中无法被消费时就会成为死信,这个时候能被重新publish到新的Exchange就是DLX死信队列

通过控制台创建队列并指定成为死信队列
在这里插入图片描述
这个时候order-queue这个队列绑定的死信已经有了,但具体的死信交换机和死信队列还没有创建,需要创建一下

其实我们的死信队列就是一个正常队列,参数x-dead-letter-exchange,x-dead-letter-routing-key这些都只是绑定死信一个关系而已,实质上死信队列相关的:Exchange、queue、binding、routingkey都要自己创建

通过代码创建队列的同时关联死信队列

@Component
public class OrderReceiver {

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = "order-queue",durable = "true",autoDelete = "false",arguments = {
                    @Argument(name = "x-dead-letter-exchange",value = "dead-exchange"),
                    @Argument(name = "x-dead-letter-routing-key",value = "dead.key")
            }),
            exchange = @Exchange(value = "order-exchange",durable = "true",type = "topic"),
            key = "order.*"
        )
    )
    @RabbitHandler
    public void onOrderMessage(@Payload OrderInfo orderInfo,
                               @Headers Map<String,Object> headers,
                               Channel channel) throws Exception{
        //消费者操作
        System.out.println("-----消息收到开始消费-----");
        System.out.println("Order Name:"+orderInfo.getOrder_name());
        Long deliveryTag = (Long)headers.get(AmqpHeaders.DELIVERY_TAG);
        //因为是手动签收,所以需要进行ACK一下
        //deliveryTag是签收标识,false是不支持批量签收
        if("5".equals(orderInfo.getId())){
            //消费失败消息重回队列
            channel.basicNack(deliveryTag,false,true);
        }else {
            channel.basicAck(deliveryTag, false);
        }
    }
}

7. 镜像集群模式的构建

需要配置和搭建三台RabbitMQ的服务,每台机器上都需要

  • RabbitMQ服务
  • 管理控制台服务

三台服务器RabbitMQ服务和控制台都启动起来,看一下三个节点的overview都是单机的

# 0.搭建单机服务需要注意以下几点
vi /etc/hostname # 修改主机名称
vi /etc/hosts # 配置三台机器的映射
192.168.0.233 RMQ233
192.168.0.234 RMQ234
192.168.0.235 RMQ235
# 记得重启一下机器reboot
# 1.先停止三个节点的服务
rabbitmqctl stop
# 2.进行文件同步操作
# 将一台机器选择作为master,这里使用RMQ233,将它的服务中的cookie文件同步到另外两台机器上
# cookie文件在/var/lib/rabbitmq目录下
scp /var/lib/rabbitmq/.erlang.cookie root@192.168.0.234:/var/lib/rabbitmq/
scp /var/lib/rabbitmq/.erlang.cookie root@192.168.0.235:/var/lib/rabbitmq/
# 3.启动集群,所有机器都要执行启动
rabbitmq-server -detached
# 4.slave加入集群操作,相当于寻址,slave加入master
slave1: rabbitmqctl stop_app
slave1: rabbitmqctl join_cluster rabbit@RMQ233
slave1: rabbitmqctl start_app
slave2: rabbitmqctl stop_app
slave2: rabbitmqctl join_cluster rabbit@RMQ233
slave2: rabbitmqctl start_app
# 5.移除slave节点
# 在需要移除的节点上停掉节点
rabbitmqctl stop_app
# 在master节点上执行移除
rabbitmqctl forget_cluster_node rabbit@RMQ234
# 6.设置集群的名称
# 在集群的任意节点可以进行设置
rabbitmqctl set_cluster_name rabbitmq_cluster_0812
# 7.查看集群状态
# 在集群的任意节点可以
rabbitmqctl cluster_status

如果要把集群节点设置成内存的方式

# 将235设置成ram形式
slave235:rabbitmqctl stop_app
master:rabbitmqctl forget_cluster_node rabbit@RMQ235
slave235:rabbitmqctl join_cluster --ram rabbit@RMQ233
slave235:rabbitmqctl start_app

最后一步就是配置镜像队列

  • 目的是将所有队列设置为镜像队列,即队列会被复制到各个节点,各个节点状态一致
  • 消息发送到master也会同步所有slave
# 在任意节点
rabbitmqctl set_policy ha-all "^" '{"ha-mode":"all"}'
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值