rabbitMQ的三种模式使用

5,634 阅读8分钟

最近的项目中使用到了rabbitMQ,巧合的是fanout,direct,topic这三种模式都接触到了,记录一下。

是什么?

RabbitMQ是由Erlang语言编写的实现了高级消息队列协议(AMQP)的开源消息代理软件(也可称为 面向消息的中间件)。支持Windows、Linux/Unix、MAC OS X操作系统和包括JAVA在内的多种编程语言。 AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受 客户端/中间件 不同产品,不同的开发语言等条件的限制
······然后还是说说和之前用的消息队列的区别吧,上个项目用过kafka。
关于这两种MQ的比较,网上查到的要点:

  • RabbitMq比kafka成熟,在可用性上,稳定性上,可靠性上,RabbitMq超过kafka
  • Kafka设计的初衷就是处理日志的,可以看做是一个日志系统,针对性很强,所以它并没有具备一个成熟MQ应该具备的特性
  • Kafka的性能(吞吐量、tps)比RabbitMq要强,这篇文章的作者认为,两者在这方面没有可比性。

干什么?

MQ就是消息中间件嘛,消息队列中间件是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题实现高性能,高可用,可伸缩和最终一致性架构 使用较多的消息队列有ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ。

怎么用?

标题上线,RabbitMQ中,所有生产者提交的消息都由Exchange来接受,然后Exchange按照特定的策略转发到Queue进行存储
RabbitMQ提供了四种Exchange:fanout,direct,topic,header
性能排序:fanout > direct > topic。比例大约为11:10:6
恰好前三个我都用到了,都分别写一下
对于rabbitMQ中使用的方法和参数详解阅读 [snowcoal.com/article/598…](Rabbitmp(java)对列 Client api介绍)

fanout模式

Fanout Exchange – 不处理路由键。你只需要简单的将队列绑定到交换机上。一个发送到交换机的消息都会被转发到与该交换机绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。Fanout交换机转发消息是最快的。

任何发送到Fanout Exchange的消息都会被转发到与该Exchange绑定(Binding)的所有Queue上。

1.可以理解为路由表的模式
2.这种模式不需要RouteKey
3.这种模式需要提前将Exchange与Queue进行绑定,一个Exchange可以绑定多个Queue,一个Queue可以同多个Exchange进行绑定。
4.如果接受到消息的Exchange没有与任何Queue绑定,则消息会被抛弃。

生产者

        Channel channel = null;
		try {
            // 连接工厂
            ConnectionFactory factory = new ConnectionFactory();
            // 创建连接
            Connection connection = factory.newConnection();
            // 获取通道
            channel = connection.createChannel();
			// 声明队列
			channel.queueDeclare(queueName, true, false, false, null);
			// 发布消息
			channel.basicQos(1);
			// 每次分发一个任务,MessageProperties.PERSISTENT_TEXT_PLAIN消息持久化
			channel.basicPublish("", queueName, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
		} catch (Exception e) {
			System.out.println(e.getStackTrace().toString());
		}

消费者

        // rabbitMQ连接参数
        boolean DURABLE_TRUE = true; // 持久化
        String queueName = ""; // 消费的队列 = this.getName();

        try {
            // 连接服务器
            Connection connection = MQCommons.getNewConnection();
            Channel channel = connection.createChannel();
            // 随机生成一个队列
            channel.queueDeclare(queueName, DURABLE_TRUE, false, false, null);
            // 每次只消费一个
            channel.basicQos(1);
            DefaultConsumer consumer = new DefaultConsumer(channel) {
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                           byte[] body) throws IOException {
                    long deliveryTag = envelope.getDeliveryTag();
                    String message = new String(body, "UTF-8");
                    // 处理消息
                    this.getChannel().basicAck(deliveryTag, false);
                }
            };
            // 需明确回复
            boolean autoAck = false;
            channel.basicConsume(queueName, autoAck, consumer);

        } catch (Exception e) {
            System.out.println("消费者线程停止");
            e.printStackTrace();
        }

direct模式

Direct Exchange - 处理路由键。需要将一个队列绑定到交换机上,要求该消息与一个特定的路由键完全匹配。这是一个完整的匹配。如果一个队列绑定到该交换机上要求路由键 “dog”,则只有被标记为“dog”的消息才被转发,不会转发dog.puppy,也不会转发dog.guard,只会转发dog。

任何发送到Direct Exchange的消息都会被转发到RouteKey中指定的Queue。
1.一般情况可以使用rabbitMQ自带的Exchange:”"(该Exchange的名字为空字符串,下文称其为default Exchange)。
2.这种模式下不需要将Exchange进行任何绑定(binding)操作
3.消息传递时需要一个“RouteKey”,可以简单的理解为要发送到的队列名字。
4.如果vhost中不存在RouteKey中指定的队列名,则该消息会被抛弃。

生产者

public void send(String message, String exchangeName, String routeKey) {
        Channel channel = null;
        try {
            // 连接工厂
            ConnectionFactory factory = new ConnectionFactory();
            // 创建连接
            Connection connection = factory.newConnection();
            // 获取通道
            channel = connection.createChannel();
            // 交换机声明 true:持久化
            channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT,true,false,null);
            // 声明队列
            channel.queueDeclare(queueName, true, false, false, null);
            // 发布消息
            channel.basicQos(1);
            // 发布消息  MessageProperties.PERSISTENT_TEXT_PLAIN消息持久化
            channel.basicPublish(exchangeName, routeKey, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
        } catch (Exception e) {
            e.printStackTrace();
        }
}

消费者

        // rabbitMQ连接参数
        threadName = getName();

        try {
            // 连接服务器
            Connection connection = MQCommons.getNewConnection();
            Channel channel = connection.createChannel();

            //交换机声明(参数为:交换机名称;交换机类型)
            channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT,true,false,null);
            // 声明队列
            channel.queueDeclare(queueName, DURABLE_TRUE, false, false, null);
            // 队列与交换机绑定(参数为:队列名称;交换机名称;密匙-routingKey)
            channel.queueBind(queueName, exchangeName, routingKey);
            // 每次分发一个任务,MessageProperties.PERSISTENT_TEXT_PLAIN消息持久化
            channel.basicQos(1);
            DefaultConsumer consumer = new DefaultConsumer(channel) {
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                           byte[] body) throws IOException {
                    long deliveryTag = envelope.getDeliveryTag();
                    String message = new String(body, "UTF-8");
                    log.info("message内容={}", message);
                    // 处理消息
                    dealMessage(message);
                    // 应答:已处理(不做判断是否异常,不仍会消息队列)
                    getChannel().basicAck(deliveryTag, false);
                }
            };
            // 需明确回复
            boolean autoAck = false;
            channel.basicConsume(queueName, autoAck, consumer);

        } catch (Exception e) {
            log.info("消费线程处理异常", e);
        }

topic模式

Topic Exchange – 将路由键和某模式进行匹配。此时队列需要绑定要一个模式上。符号“#”匹配一个或多个词,符号“”匹配不多不少一个词。因此“audit.#”能够匹配到“audit.irs.corporate”,但是“audit.” 只会匹配到“audit.irs”
任何发送到Topic Exchange的消息都会被转发到所有关心RouteKey中指定话题的Queue上
1.这种模式较为复杂,简单来说,就是每个队列都有其关心的主题,所有的消息都带有一个“标题”(RouteKey),Exchange会将消息转发到所有关注主题能与RouteKey模糊匹配的队列。
2.这种模式需要RouteKey,也许要提前绑定Exchange与Queue。
3.在进行绑定时,要提供一个该队列关心的主题,如“#.log.#”表示该队列关心所有涉及log的消息(一个RouteKey为”MQ.log.error”的消息会被转发到该队列)。
4.“#”表示0个或若干个关键字,“”表示一个关键字。如“log.”能与“log.warn”匹配,无法与“log.warn.timeout”匹配;但是“log.#”能与上述两者匹配。
5.同样,如果Exchange没有发现能够与RouteKey匹配的Queue,则会抛弃此消息。

生产者

    public void sendTopic(String message, String exchangeName, String routeKey) {
        log.info("交换机={},routeKey={},消息={}", exchangeName, routeKey, message);
        try {
            //交换机声明 true:持久化
            channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC, true, false, null);
            channel.queueBind("queueName", exchangeName, routeKey);
            // 发布消息  MessageProperties.PERSISTENT_TEXT_PLAIN消息持久化
            channel.basicPublish(exchangeName, routeKey, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
        } catch (Exception e) {
            log.error("发送消息队列消息异常", e);
        }
    }

消费者

基本同上面的direct消费者

交换机绑定改一下
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC,true,false,null);

补充- rabbitmq实现延时队列(死信队列)

采用rabbitmq的消息时效ttl的特性实现延时重试机制。
前面对exchange,queue,routingKey有过介绍。延时队列的实现就是把消息放到死信交换机的死信队列,设置超时时间,等超时未处理,消息再次根据routingKey返回原有队列。消费者判断消费次数重复消费消息。

队列中的消息在以下三种情况下会变成死信 (1)消息被拒绝(basic.reject 或者 basic.nack),并且requeue=false; (2)消息的过期时间到期了; (3)队列长度限制超过了。 当队列中的消息成为死信以后,如果队列设置了DLX那么消息会被发送到DLX。通过x-dead-letter-exchange设置DLX,通过这个x-dead-letter-routing-key设置消息发送到DLX所用的routing-key,如果不设置默认使用消息本身的routing-key


参考实现:my.oschina.net/xiaominmin/…
文中提供了两种方式,我采用的第一种
spring的简单实现:www.cnblogs.com/lori/archiv…

public void sendDelay(String message,String exchangeName,String queueName,String routeKey,int ttl,BuiltinExchangeType exchangeType,String deathExchange){
        log.info("延时队列,交换机={},队列名={},routeKey={},deathExchange={},消息={},队列类型={},TTL={}", exchangeName, queueName, routeKey, deathExchange, message, exchangeType, ttl);
        try{
            Map<String, Object> arguments = new HashMap<String, Object>();
            //设置死信交换机
            arguments.put("x-dead-letter-exchange", deathExchange);
            //延时30秒
            arguments.put("x-message-ttl", ttl);
            //设置死信routingKey
            arguments.put("x-dead-letter-routing-key", routeKey);

            channel.exchangeDeclare(exchangeName, exchangeType,true,false,null);
            channel.queueDeclare(queueName, DURABLE_TRUE, false, false, arguments);
            channel.queueBind(queueName, exchangeName, routeKey);
            channel.basicPublish(exchangeName, routeKey, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
        }catch (Exception e){
            log.error("发送消息异常",e);
        }
    }

业务场景: 消息消费过程中可能因为一些原因导致消费出错,比如请求其他系统接口超时,或者第三方系统挂掉,这种情况如果立即重试大部分情况还是一样的处理结果。最开始我想的解决方案,是把错误消息存库,然后设置定时任务隔一段时间重试,最后达到最大重试次数后记为任务失败。但是定时任务的方式是十分消耗系统资源的,此时发现rabbitMQ中可以使用延时队列实现定时任务这个功能。
实现代码: 先写着些,肯定有人觉得我这么写麻烦,我这个项目没有用springboot,如果是springboot感觉会简单的多。别人的springboot项目 www.jianshu.com/p/0d400d309…,有空我也用springboot试一下。

分享一个好用的软件 这是谷歌的http请求的模拟插件类似于postman,优点就是小,方便,当然经常用的话还是推荐postman,七天有效再要留言。
链接:pan.baidu.com/s/1zYQSdhH5… 提取码:mubu