消息队列整理

一、消息队列

MQ的选择

rabbitmq和kafka都支持高可用,持久化,但是kafka吞吐量性能更高一些,一般用于日志采集和流式计算,支持10万级别的吞吐量,rocketmq和rabbitmq支持万级。

MQ的使用目的

异步、消峰、解耦

RabbitMQ

AMQP messaging 中的基本概念

  1. Broker: 接收和分发消息的应用,RabbitMQ Server就是Message Broker。
  2. Virtual host: 出于多租户和安全因素设计的,把AMQP的基本组件划分到一个虚拟的分组中,类似于网络中的namespace概念。当多个不同的用户使用同一个RabbitMQ server提供的服务时,可以划分出多个vhost,每个用户在自己的vhost创建exchange/queue等。
  3. Connection: publisher/consumer和broker之间的TCP连接。断开连接的操作只会在client端进行,Broker不会断开连接,除非出现网络故障或broker服务出现问题。
  4. Channel: 如果每一次访问RabbitMQ都建立一个Connection,在消息量大的时候建立TCP Connection的开销将是巨大的,效率也较低。Channel是在connection内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的channel进行通讯,AMQP method包含了channel id帮助客户端和message broker识别channel,所以channel之间是完全隔离的。Channel作为轻量级的Connection极大减少了操作系统建立TCP connection的开销。
  5. Exchange: message到达broker的第一站,根据分发规则,匹配查询表中的routing key,分发消息到queue中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)。
  6. Queue: 消息最终被送到这里等待consumer取走。一个message可以被同时拷贝到多个queue中。
  7. Binding: exchange和queue之间的虚拟连接,binding中可以包含routing key。Binding信息被保存到exchange中的查询表中,用于message的分发依据。
    在这里插入图片描述

交换机类型

  1. fanout类型
    Exchange路由规则非常简单,它会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中。
  2. direct类型
    Exchange路由规则也很简单,它会把消息路由到那些binding key与routing key完全匹配的Queue中。(支持多重绑定,两个队列使用一个路由键,绑定一个交换机)
  3. topic类型
    Routing Key必须与Binding Key相匹配的时候才将消息传送给Queue

Rabbitmq可靠性

生产者可靠性保证

a. rabbitmq发送消息,如果不进行特殊设置(设置为事务模式或者确认模型),则生产者可靠性无法保证,默认rabbitmq不会返回任何信息给生产者。
b.事务模式的缺点,事务模式本身为同步机制,性能上比确认模式要差,确认模式为异步模式(与mqserver连接的信道也为双向信道)。

mandatory参数与备份交换机

mandatory和备份交换器一起使用,mandatory参数无效, 备份交换机的使用是为了减少生产者代码复杂度。用了mandatory参数就要增加addReturnListener监听器。

mandatory参数设置为true
package com.rabbitmq.test;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;


public class ProducerV5 {
    static String EXCHANGE_NAME = "exchange_name";
    static String QUEUE_NAME = "queue_name";
    static String ROUTING_KEY = "root_key";
    public static void main(String[] args) throws Exception{
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUsername("****");
        factory.setPassword("****");
        factory.setVirtualHost("/");
        factory.setHost("**.**.**.**");
        factory.setPort(5672);
        Connection conn = null;
        final Channel channel;
        try {
            conn = factory.newConnection();
            //注意conn不关闭时,程序不停止。
            channel = conn.createChannel();
            channel.exchangeDeclare(EXCHANGE_NAME,"direct", true, false, null);
            channel.queueDeclare(QUEUE_NAME, true, false, false, null);
            channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
            //当rabbimq无法将消息正确路由到队列时,触发。
            channel.addReturnListener(new ReturnListener() {
                public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    System.out.println("replyCode:"+replyCode);
                    System.out.println("replyText:"+replyText);
                    System.out.println("exchange:"+exchange);
                    System.out.println("routingKey:"+routingKey);
                    String string = new String(body);
                    System.out.println("basic return"+string);
                }
            });
            int i=1;
            while (i<20){
                String string = "hello world"+i;
                //rabbitmq无法找到EXCHANGE_NAME时,会触发异常。无法找到queue时,mq会发送Basic.Return命令给生产者,addReturnListener进行监听。
                channel.basicPublish(EXCHANGE_NAME,  ROUTING_KEY+"uuuu", true, MessageProperties.PERSISTENT_TEXT_PLAIN,
                        string.getBytes());
                i++;
                System.out.println(i+"ddd");
            }
            System.out.println("dddfdafdf");
            //channel.addReturnListener();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }finally {
            conn.close();
        }
        System.out.println("end");
    }
}
备份交换机
package com.rabbitmq.test;
import com.rabbitmq.client.*;
import org.omg.CosNaming.NamingContextExtPackage.StringNameHelper;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;


public class ProducerV5 {
    static String EXCHANGE_NAME = "exchange_name_v2";
    static String BEI_FEN_EXCHANGE_NAME = "bei_fen_exchange_name";
    static String UN_ROUTED_QUEUE_NAME = "un_routed_exchange_name";
    static String QUEUE_NAME = "queue_name";
    static String ROUTING_KEY = "root_key";
    public static void main(String[] args) throws Exception{
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUsername("***");
        factory.setPassword("***");
        factory.setVirtualHost("/");
        factory.setHost("**.**.**.**");
        factory.setPort(5672);
        Connection conn = null;
        final Channel channel;
        try {
            conn = factory.newConnection();
            channel = conn.createChannel();
            Map<String,Object> argsProperties = new HashMap<String, Object>();
            argsProperties.put("alternate-exchange", BEI_FEN_EXCHANGE_NAME);
            channel.exchangeDeclare(EXCHANGE_NAME,"direct", true, false, argsProperties);
            channel.exchangeDeclare(BEI_FEN_EXCHANGE_NAME,"fanout", true, false, null);
            channel.queueDeclare(QUEUE_NAME, true, false, false, null);
            channel.queueDeclare(UN_ROUTED_QUEUE_NAME, true, false, false, null);
            channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
            channel.queueBind(UN_ROUTED_QUEUE_NAME, BEI_FEN_EXCHANGE_NAME, "");
            int i=1;
            while (i<20){
                String string = "hello world"+i;
                //此时路由不到队列时,消息发送到备份交换机。
                channel.basicPublish(EXCHANGE_NAME,  ROUTING_KEY+"uuuufff", true, MessageProperties.PERSISTENT_TEXT_PLAIN,
                        string.getBytes());
                i++;
                System.out.println(i+"ddd");
            }
            //channel.addReturnListener();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }finally {
            conn.close();
        }
        System.out.println("end");
    }
}

注意:备份交换机与普通交换机没有区别,发送到备份交换机的路由key与正常交换机一样。所以备份交换机建议设置为fanout。
在这里插入图片描述

confirm异步模式
package com.rabbitmq.test;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.TreeMap;
import java.util.concurrent.TimeoutException;

public class ProducerV3 {
    static String EXCHANGE_NAME = "exchange_name";
    static String QUEUE_NAME = "queue_name";
    static String ROUTING_KEY = "root_key";
    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUsername("***");
        factory.setPassword("****");
        factory.setVirtualHost("/");
        factory.setHost("10.12.24.104");
        factory.setPort(5672);
        Connection conn = null;
        final Channel channel;
        final TreeMap<Long, String> map = new TreeMap<Long, String>();
        try {
            conn = factory.newConnection();
            channel = conn.createChannel();
            channel.exchangeDeclare(EXCHANGE_NAME,"direct", true, false, null);
            channel.exchangeDeclare(EXCHANGE_NAME+"dddd","direct", true, false, null);

            channel.queueDeclare(QUEUE_NAME, true, false, false, null);
            channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
            //设置mq为认证模式。
            channel.confirmSelect();
            channel.addReturnListener(new ReturnListener() {
                public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    System.out.println("replyCode:"+replyCode);
                    System.out.println("replyText:"+replyText);
                    System.out.println("exchange:"+exchange);
                    System.out.println("routingKey:"+routingKey);
                    String string = new String(body);
                    System.out.println("basic return"+string);

                }
            });
            //rabbitmq发送Basic.Ack.(这里测试与书上说的不一样,并不是正确的投递到队列,才会发送Basic.Ack,自己测试得到的结果消息到达了交换机就会发送Ack,所以发送认证机制最好跟备份交换机或者madatory参数共同保证可靠性。Nack理解可能是rabbitmq因为自身错误,无法接收消息时,才发送Nack。)
            channel.addConfirmListener(new ConfirmListener() {
                public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                   System.out.println("handleAck"+deliveryTag);
                   map.remove(deliveryTag);
                }
                //mutiple为之前的消息是否正确投递
                public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                    System.out.println("handleNack"+deliveryTag);
                    // 重新发送一条相同的消息。注意信道认证模式时,没发送到信道的消息都有唯一的deliveryTag.
                    String string = map.get(deliveryTag);
                    channel.basicPublish(EXCHANGE_NAME,  ROUTING_KEY, true, MessageProperties.PERSISTENT_TEXT_PLAIN,
                            string.getBytes());
                    map.clear(deliveryTag);
                }
            });
            int i=1;
            while (i<20){
                String string = "hello world"+i;
                long nextNo = channel.getNextPublishSeqNo();
                //这里无法路由到正确的队里,仍然会触发ack.
                channel.basicPublish(EXCHANGE_NAME,  ROUTING_KEY+"gggggg", true, MessageProperties.PERSISTENT_TEXT_PLAIN,
                        string.getBytes());
                i++;
                map.put(nextNo, string);
            }
            System.out.println("dddfdafdf");
            //channel.addReturnListener();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
    }
}
confirm批量认证模式
package com.rabbitmq.test;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.ArrayList;
import java.util.concurrent.TimeoutException;

public class ProducerV4 {
    static String EXCHANGE_NAME = "exchange_name";
    static String QUEUE_NAME = "queue_name";
    static String ROUTING_KEY = "root_key";
    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUsername("kreditplus_dev");
        factory.setPassword("kreditplus_dev");
        factory.setVirtualHost("/");
        factory.setHost("10.12.24.104");
        factory.setPort(5672);
        Connection conn = null;
        Channel channel = null;
        try {
            conn = factory.newConnection();
            channel = conn.createChannel();
            channel.exchangeDeclare(EXCHANGE_NAME,"direct", true, false, null);
            channel.queueDeclare(QUEUE_NAME, true, false, false, null);
            channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
            channel.confirmSelect();
            ArrayList<String> arraylist = new ArrayList<String>();
            channel.addReturnListener(new ReturnListener() {
                public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    System.out.println("replyCode:" + replyCode);
                    System.out.println("replyText:" + replyText);
                    System.out.println("exchange:" + exchange);
                    System.out.println("routingKey:" + routingKey);
                    String string = new String(body);
                    System.out.println("basic return" + string);
                }
            });
            int i=1;
            int mod = 0;
            while (i<40) {
                String string = "hello world" + i;
                long nextNo = channel.getNextPublishSeqNo();
                System.out.println("nextNo"+ nextNo);
                channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, true, MessageProperties.PERSISTENT_TEXT_PLAIN,
                        string.getBytes());
                i++;
                arraylist.add(string);
                if (++mod >= 20) {
                    mod = 0;
                    try {
                        //批量认证模式
                        boolean value = channel.waitForConfirms();
                        if (!value) {
                            //重复发送
                            for (String string_v2 : arraylist) {
                                channel.basicPublish(EXCHANGE_NAME, "dadfa", true, MessageProperties.PERSISTENT_TEXT_PLAIN,
                                        string_v2.getBytes());
                            }
                            System.out.println();
                        } else {
                            arraylist.clear();
                            System.out.println("success" + string);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
    }
}
rabbitmq持久化可靠性保证
交换机,队列,消息持久化。

交换机持久化:保证重启时,交换机元数据不丢失。不持久化,则mq重启后,交换机无法继续使用。 不影响消息是否丢失。
队列持久化:队列不持久化时,重启后消息丢失。
消息持久化:发送消息时,deliveryMode设置为2,将消息持久化,如果只有队列持久化,没有消息持久化,重启后,消息会丢失。

镜像队列

消息持久化到磁盘时,可能需要在操作系统缓存保存一段时间,才能够存到物理磁盘中,这段时间如果rabbitmq挂掉,可能会丢失数据,可通过镜像队里进行保证。

消息的保存时间

消息保存时间有两个限制:
1.消息本身的的保存时间TTL和队列的保存时间TTL,消息的过期时间,取两者中小的那个,不设置TTL,则消息会等到消费后,才会从rabbitmq删除。

消费者可靠性保证
  1. 消费者可靠性保证主要利用手动确认机制
  2. 如果多个消费者订阅同一队列,rabbitmq采用轮训方式推送消息到消费者。
  3. channel.basicQos(2),控制了单个消费者,在信道上未确认的个数,channel.basixQos(10, true), global为true整体信道上的消费者都需要遵从这个限定值,(加和限定和单个限定有待实验)。
  4. 手动确认,basicNack 方法的第三个参数代表是否重回队列,如果你填 false 那么消息就直接丢弃了,相当于没有保障消息可靠。如果你填 true ,当发生消费报错之后,这个消息会被重回消息队列顶端,继续推送到消费端,继续消费这条消息。通常需要手动处理这条消息。
package com.rabbitmq.test;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class ConsumerTestV2 {
    static String QUEUE_NAME = "queue_name";

    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("10.12.24.104");
        factory.setPort(5672);
        factory.setUsername("kreditplus_dev");
        factory.setPassword("kreditplus_dev");
        //factory.setVirtualHost("/");
        Connection connection = null;

        try {
            connection = factory.newConnection();
            final Channel channel = connection.createChannel();
            //设置客户端最多接收未被ack的消息的个数, 起到了一个滑动窗口的作用,channel.basicQos还有针对一个消费者和多个消费者的区别。
            channel.basicQos(2);
            System.out.println("aaaaaa");
            boolean autoAck = false;
            channel.basicConsume(QUEUE_NAME, autoAck, "myConsumerTagV2",
                    new DefaultConsumer(channel) {
                        @Override
                        public void handleDelivery(String consumerTag,
                                                   Envelope envelope,
                                                   AMQP.BasicProperties properties,
                                                   byte[] body)
                                throws IOException
                        {
                            String routingKey = envelope.getRoutingKey();
                            String contentType = properties.getContentType();
                            long deliveryTag = envelope.getDeliveryTag();
                            // (process the message components here ...)
                            System.out.print("aaaaaavdadfadsa"+new String(body)+deliveryTag);
                            try {
                                Thread.sleep(1000);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            // 手动确认
                            channel.basicAck(deliveryTag, false);
                        }
                    });
//            channel.close();
//            connection.close();

       } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }finally {
        }
    }
}

4.死信队列

消息在一个队列中变成死信,能够过重新被发送到一个交换机DLX,
绑定DLX的队列就称之为死信队列。
消息变成死信的情况:

  1. 消息被拒绝
  2. 消息过期
  3. 队列达到最大长度。

生产者

package com.rabbitmq.test;
import com.rabbitmq.client.*;
import com.rabbitmq.client.AMQP.BasicProperties;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;

public class Producer {
    static String EXCHANGE_NAME = "exchange_name";
    static String QUEUE_NAME = "queue_name_v2";
    static String ROUTING_KEY = "root_key";
    static String DEAD_DLX_EXCHANGE = "dlx_exchange";
    static String DEAD_QUEUE = "dlx_queue";
    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUsername("kreditplus_dev");
        factory.setPassword("kreditplus_dev");
        factory.setVirtualHost("/");
        factory.setHost("10.12.24.104");
        factory.setPort(5672);
        Connection conn = null;
        Channel channel = null;
        try {
            conn = factory.newConnection();
            channel = conn.createChannel();
            channel.exchangeDeclare(EXCHANGE_NAME,"direct", true, false, null);
            //dead exchange
            channel.exchangeDeclare(DEAD_DLX_EXCHANGE,"direct", true, false, null);
            Map<String,Object> ars= new HashMap<String,Object>();
            ars.put("x-dead-letter-exchange", DEAD_DLX_EXCHANGE);
            //也可以为这个DLX指定路由键,如果没有特殊指定,则使用原队列的路由键
            ars.put("x-dead-letter-routing-key","dlx-routing-key"); 
            //给正常队列绑定死信交换机
            channel.queueDeclare(QUEUE_NAME, true, false, false, ars);
            channel.queueDeclare(DEAD_QUEUE, true, false, false, null);
            channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
            //死信交换机、死信队列、路由key绑定
            channel.queueBind(DEAD_QUEUE, DEAD_DLX_EXCHANGE, "dlx-routing-key");
            int i=10;
            while (i<20){
                String string = "hello world"+i;
                channel.basicPublish(EXCHANGE_NAME,  ROUTING_KEY, new BasicProperties.Builder().deliveryMode(2).build(),  //设置为持久化
                        string.getBytes());
                i++;
            }

            channel.close();
            conn.close();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
        System.out.println("aaaaaa");
    }

}

消费者

package com.rabbitmq.test;
import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeoutException;


public class ConsumerTest {
    static String QUEUE_NAME = "queue_name";

    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("10.12.24.104");
        factory.setPort(5672);
        factory.setUsername("kreditplus_dev");
        factory.setPassword("kreditplus_dev");
        //factory.setVirtualHost("/");
        Connection connection = null;
        try {
            connection = factory.newConnection();
            final Channel channel = connection.createChannel();
            //设置客户端最多接收未被ack的消息的个数
            channel.basicQos(3);
            System.out.println("aaaaaa");
            boolean autoAck = false;
            channel.basicConsume(QUEUE_NAME, autoAck, "myConsumerTag",
                    new DefaultConsumer(channel) {
                        @Override
                        public void handleDelivery(String consumerTag,
                                                   Envelope envelope,
                                                   AMQP.BasicProperties properties,
                                                   byte[] body)
                                throws IOException
                        {
                            String routingKey = envelope.getRoutingKey();
                            String contentType = properties.getContentType();
                            long deliveryTag = envelope.getDeliveryTag();
                            // (process the message components here ...)
                            System.out.println("aaaaaavdadfadsa"+new String(body));
                            System.out.println(deliveryTag);
                            try {
                                Thread.sleep(10000);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            //重点是这里最后一个参数决定了是否让消息重新回到队里,还是转发到死信队列中。
                            channel.basicNack(deliveryTag, false, false);
                            //channel.basicAck(deliveryTag, false);
                        }
                    });
//            channel.close();
//            connection.close();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }finally {

        }
    }
}

延迟队列

延迟队列是在死信队列的基础上实现的,可通过消息的TTL来控制,生产者消息生产时,设置死信队列和消息TTL时间,消息过期进入到死信队列。 消费者消费死信队列保证了延迟功能。

幂等性

生产者给每条消息设置一个唯一uuid。

生产者
package com.rabbitmq.test;

import com.rabbitmq.client.*;
import com.rabbitmq.redis.RedisPool;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.io.IOException;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.TimeoutException;

public class ProducerV6 {
    static String EXCHANGE_NAME = "exchange_name";
    static String QUEUE_NAME = "queue_name";
    static String ROUTING_KEY = "root_key";
    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUsername("admin");
        factory.setPassword("admin");
        factory.setVirtualHost("/");
        factory.setHost("192.168.0.17");
        factory.setPort(5672);
        Connection conn = null;
        final Channel channel;
        final TreeMap<Long, String> map = new TreeMap<Long, String>();
        try {
            conn = factory.newConnection();
            channel = conn.createChannel();
            channel.exchangeDeclare(EXCHANGE_NAME,"direct", true, false, null);
            channel.exchangeDeclare(EXCHANGE_NAME+"dddd","direct", true, false, null);

            channel.queueDeclare(QUEUE_NAME, true, false, false, null);
            channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
            channel.confirmSelect();
            channel.addReturnListener(new ReturnListener() {
                public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    System.out.println("replyCode:"+replyCode);
                    System.out.println("replyText:"+replyText);
                    System.out.println("exchange:"+exchange);
                    System.out.println("routingKey:"+routingKey);
                    String string = new String(body);
                    System.out.println("basic return"+string);

                }
            });
            channel.addConfirmListener(new ConfirmListener() {
                public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                    System.out.println("handleAck"+deliveryTag);
                    map.remove(deliveryTag);
                }
                public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                    System.out.println("handleNAck"+deliveryTag);
                    map.remove(deliveryTag);
                    // string need has primary key,
                    // repeat send
                    String string = map.get(deliveryTag);
                    channel.basicPublish(EXCHANGE_NAME,  ROUTING_KEY, true, MessageProperties.PERSISTENT_TEXT_PLAIN,
                            string.getBytes());
                }
            });
            //这里为了做实验保证幂等性,实际corrId每条消息唯一。
            String corrId = "10389234451254177779900";
            AMQP.BasicProperties props = new AMQP.BasicProperties.Builder()
                    .deliveryMode(2)
                    .correlationId(corrId)
                    .build();
            int i=1;
            while (i<20){
                String string = "hello world"+i;

                long nextNo = channel.getNextPublishSeqNo();
                channel.basicPublish(EXCHANGE_NAME,  ROUTING_KEY, false, props,
                        string.getBytes());
                i++;
                map.put(nextNo, string);
            }
            System.out.println("dddfdafdf");
            //channel.addReturnListener();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
    }
}


消费者避免重复消费。

消息重复发送的原因有,重试,生产者重复发送等,需要自己在消费者端保证消息的唯一性。
方法有

  1. 利用redis的setnx命令判断是否已经消费过,缓存时间的设置,要看内存的大小,及消息重复发送的时间间隔进行评估。
  2. 利用mysql的唯一健,但是高并发的情况下,对是数据库压力较大。
    消费失败的处理
  3. 手动确认,basicNack 方法的第三个参数代表是否重回队列,如果你填 false 那么消息就直接丢弃了,相当于没有保障消息可靠。如果你填 true ,当发生消费报错之后,这个消息会被重回消息队列顶端,继续推送到消费端,继续消费这条消息。通常代码的报错并不会因为重试就能解决,所以这个消息将会出现这种情况:继续被消费,继续报错,重回队列,继续被消费…死循环。
    所以真实的场景一般是:
    当消费失败后将此消息存到 Redis,记录消费次数,如果消费了三次还是失败,就丢弃掉消息,记录日志落库保存
    直接填 false ,不重回队列,记录日志、发送邮件等待开发手动处理。
package com.rabbitmq.test;

import com.rabbitmq.client.*;
import com.rabbitmq.redis.RedisPool;
import redis.clients.jedis.Jedis;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class ConsumerTestV3 {
    static String QUEUE_NAME = "queue_name";
    static Jedis jedis = RedisPool.getJedis();

    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.0.17");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("admin");
        //factory.setVirtualHost("/");
        Connection connection = null;

        try {
            connection = factory.newConnection();
            final Channel channel = connection.createChannel();
            //设置客户端最多接收未被ack的消息的个数
            channel.basicQos(3);
            System.out.println("aaaaaa");
            boolean autoAck = false;
            channel.basicConsume(QUEUE_NAME, autoAck, "myConsumerTag",
                    new DefaultConsumer(channel) {
                        @Override
                        public void handleDelivery(String consumerTag,
                                                   Envelope envelope,
                                                   AMQP.BasicProperties properties,
                                                   byte[] body)
                                throws IOException
                        {
                            String routingKey = envelope.getRoutingKey();
                            String contentType = properties.getContentType();
                            long deliveryTag = envelope.getDeliveryTag();
                            String uuid = properties.getCorrelationId();
                            System.out.println(uuid);
                            //缓存超时时间需要根据业务指定
                            if(jedis.set(uuid,"success","NX","PX",30000)!=null){
                                // (process the message components here ...)
                                System.out.println(new String(body));
                                try {
                                    Thread.sleep(10000);
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                            }
                            channel.basicNack(deliveryTag, false, false);
                            //channel.basicAck(deliveryTag, false);
                        }
                    });
//            channel.close();
//            connection.close();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }finally {

        }
    }
}

Rabbitmq的顺序性

rabbitmq的顺序性实现的话核心原理

  1. 按顺序的进入队列
  2. 按顺序的处理消息
  3. 按顺序进入队列需要保证,生产者唯一,发送消息时保证顺序,比如数据1,数据2,数据3,按顺序发送到同一队列。
  4. 消费者处理消息时,处理数据1,数据2,数据3时,核心是保证一个消费者线程处理数据1,数据2,数据3.,如果多个消费者,或者有重试情况。个人理解需要结合共享内存和消息特点(序号顺序,时间戳等进行处理)。

Rabbitmq消费消息的两种模式

推模式
  1. 推模式接收消息是最有效的一种消息处理方式。channel.basicConsume(queneName,consumer)方法将信道(channel)设置成投递模式,直到取消队列的订阅为止;在投递模式期间,当消息到达RabbitMQ时,RabbitMQ会自动地、不断地投递消息给匹配的消费者,而不需要消费端手动来拉取,当然投递消息的个数还是会受到channel.basicQos的限制。
  2. 推模式将消息提前推送给消费者,消费者必须设置一个缓冲区缓存这些消息。优点是消费者总是有一堆在内存中待处理的消息,所以当真正去消费消息时效率很高。缺点就是缓冲区可能会溢出。
  3. 由于推模式是信息到达RabbitMQ后,就会立即被投递给匹配的消费者,所以实时性非常好,消费者能及时得到最新的消息。
拉模式
  1. 如果只想从队列中获取单条消息而不是持续订阅,则可以使用channel.basicGet方法来进行消费消息。
  2. 拉模式在消费者需要时才去消息中间件拉取消息,这段网络开销会明显增加消息延迟,降低系统吞吐量。
  3. 由于拉模式需要消费者手动去RabbitMQ中拉取消息,所以实时性较差;消费者难以获取实时消息,具体什么时候能拿到新消息完全取决于消费者什么时候去拉取消息。

Rabbitmq的消息堆积

  1. 监控报警
  2. 新增消费者
  3. 调查消费瓶颈

高可用性

rabbitmq支持三种模式搭建:
一是单机模式,生产环境不常用。
二是普通集群模式,这种情况创建队列只会在一个节点上,其他的rabbitmq实例会同步这个queue的元数据(队里所在的位置),如果消费者连接队列所在的mq实例宕机了,则服务不可用,其他消费者也消费不到数据。连接其他实例获取数据,也需要从队列所在的mq实例拉取数据,会导致内部产生大量数据传输,可用性无保障。
三是镜像集群模式,指的是将队列做成镜像队列,队里的原数据和消息都会存在到镜像队列中。采用的是主从复制原理,主从复制指的是读从,写主,这种模式下才能保证mq的高可用,当主实例挂掉时,选取最早创建的从实例变为主实例。 集群模式连接是使用的代理控制。

1.集群同步4中元数据

a. 队列元数据:队列名称和它的属性

b. 交换器元数据:交换器名称、类型和属性

c. 绑定元数据:一张简单的表格展示了如何将消息路由到队列

d. vhost元数据:为vhost内的队列、交换器和绑定提供命名空间和安全属性
在这里插入图片描述

abbitmq集群采用原数据同步的原因

第一,存储空间。如果每个集群节点都拥有所有Queue的完全数据拷贝,那么每个节点的存储空间会非常大,集群的消息积压能力会非常弱(无法通过集群节点的扩容提高消息积压能力);

第二,性能。消息的发布者需要将消息复制到每一个集群节点,对于持久化消息,网络和磁盘同步复制的开销都会明显增加。

客户端连接队列所在节点

a. 如果有一个消息生产者或者消息消费者通过amqp-client的客户端连接至节点1进行消息的发布或者订阅,那么此时的集群中的消息收发只与节点1相关。
b.客户端连接的是非队列数据所在节点
非数据所在节点只会起到一个路由转发的作用,最终负责存储和消费的消息还是会在队列数据真正所在的节点上。

集群节点类型

磁盘节点:持久化存储原数据,如果是单点部署,那么一定是磁盘节点,如果磁盘节点挂掉,则不能创建队列,交换器等。
内存节点:将配置信息和原数据信息保存在内存中,通常负责整个集群与客户端的连接。

负载均衡模式

下图未将磁盘节点和内存节点进行区分,可理解为节点均需持久化原数据。
在这里插入图片描述
keepalived机制利用(虚拟路由冗余协议),已软件的形式实现服务的热备功能,(通常是两台linux服务器组成热备组,master和backup,同一时间只有master对外服务,master会虚拟出一个ip地址,简称VIP,这个VIP只存在于master上对外服务。如果keepalived检测到master故障,备份服务器backup自动接管VIP并成为master. keepalived将原master移除,当源master恢复后,会自动加入到热备组,默认再抢占,成为master。

rabbitmq镜像队列

镜像队列是基于普通的集群模式的,然后再添加一些策略,所以还是得先配置普通集群,然后才能设置镜像队列。镜像队列存在于多个节点。要实现镜像模式,需要先搭建一个普通集群模式,在这个模式的基础上再配置镜像模式以实现高可用。
镜像队列结构如下:
在这里插入图片描述
所有对mirror_queue_master的操作,会通过可靠组播GM的方式同步到各slave节点。
GM负责消息的广播,mirror_queue_slave负责回调处理,而master上的回调处理是由coordinator负责完成。mirror_queue_slave中包含了普通的BackingQueue进行消息的存储,master节点中BackingQueue包含在mirror_queue_master中由AMQQueue进行调用。
整体流程个人理解:mirror_queue_master当收到操作时,触发master对应的GM,GM收到消息并进行回调Coordinator. 同时AMQQueue也会调用BackingQueue进行master消息的持久化,
master的GM通过链表的方式传递消息,当slave的GM收到消息时,回调mirror_queue_slave进行处理,将消息持久化到或镜像队列所在BackingQueue。 此时可保证master节点在持久化数据时,数据还未刷到磁盘中,挂掉了,可通过镜像队列保证可靠性。

GM

GM模块实现的一种可靠的组播通讯协议,该协议能够保证组播消息的原子性,即保证组中活着的节点要么都收到消息要么都收不到。

它的实现大致如下:

将所有的节点形成一个循环链表,每个节点都会监控位于自己左右两边的节点,当有节点新增时,相邻的节点保证当前广播的消息会复制到新的节点上;当有节点失效时,相邻的节点会接管保证本次广播的消息会复制到所有的节点。在master节点和slave节点上的这些gm形成一个group,group(gm_group)的信息会记录在mnesia中。不同的镜像队列形成不同的group。消息从master节点对于的gm发出后,顺着链表依次传送到所有的节点,由于所有节点组成一个循环链表,master节点对应的gm最终会收到自己发送的消息,这个时候master节点就知道消息已经复制到所有的slave节点了。
新增节点:

在这里插入图片描述
每当一个节点加入或者重新加入(例如从网络分区中恢复过来)镜像队列,之前保存的队列内容会被清空。

节点的失效

slave节点失效,系统处理做些记录外几乎啥都不做。
master节点失效:
1.与客户端连接全部断开
2.选取最老的slave节点作为master, 如果此时slave为同步master数据,则数据丢失。
3.新的master重新入队列所有为没有ack的消息,此时客户端可能会有重复消息。

kafka

Kafka基础架构

在这里插入图片描述

生产者
  1. 生产者有主线程和sender线程。
    a. 主线程负责将消息,进行拦截,序列化、分区,分区器查看消息ProducerRecord的partition字段,分配分区,如果没有partition,则将key进行hash计算,然后计算分区,如果key为null,则通过轮训方式分配分区。
    b. 主线程有消息累加器,将ProducerRecord存储为一个ProducerBatch。
    c. Sender线程负责从消息累加器读取数据,然后封装消息为<Node, Request>的形式,发送到各个分区。其中发送方式利用的是selector,(一直轮训)。
  2. 生产者可靠性保证。
    1. ack=0,失败异常不会进行重试。
    2. 同步发送利用的是ProducerRecord.wait进行阻塞,处理完成回释放countDownLaunch,由于ack=0, request关联的response的body为null,不会等待broker反回,(其他需要等待有返回才能进行完成处理),selector处理已经完成的请求时,会countdownLaunch.countDown(同步的话,进行释放)。完成请求处理。
    3. 当ack为1或者-1时,如果发生异常,则进行重试。
    4. 源码整理

a.kafka如何保证消息可靠性
生产者:发送消息服务端返回ack有三种等级。ack=0,生产者发送完消息,就认为发送成功。
ack=1, 分区leader接收到消息并写入分区返回确认ack=1,仍有丢失消息的可能,未同步到follower节点,leader节点挂掉。ack=-1.在所有副本都同步完成后。leader通知生产者ack=-1。注意由leader返回相应消息。 异步:如果调整为异步模式,则有极大的可能丢失数据,因为kafka异步发送消息,会先将消息放到缓存队列里,有个定时将数据推送出去,在缓存队列这段时间,如果生产者挂掉,则消息丢失。
kafak本身通过多副本保证消息可靠。
消费者:关闭自动提交 offset,在处理完之后自己手动提交 offset,就可以保证数据不会丢。但是此时确实还是可能会有重复消费,比如你刚处理完,还没提交 offset,结果自己挂了,此时肯定会重复消费一次,自己保证幂等性。
b.kafka保证消息一致性(leader和follower消息一致):
High Water Mark 机制。leader 的HW值也就是实际已提交消息的范围,每个replica都有HW值,但仅仅leader中的HW才能作为标示信息。什么意思呢,就是说当按照参数标准成功完成消息备份(成功同步给follower replica后)才会更新HW的值,代表消息理论上已经不会丢失,可以认为“已提交”。
一致性定义:若某条消息对client可见,那么即使Leader挂了,在新Leader上数据依然可以被读到HW-HighWaterMark: client可以从Leader读到的最大msg offset,即对外可见的最大offset, HW=max(replica.offset)对于Leader新收到的msg,client不能立刻消费,Leader会等待该消息被所有ISR中的replica同步后,更新HW,此时该消息才能被client消费,这样就保证了如果Leader fail,该消息仍然可以从新选举的Leader中获取。
对于来自内部Broker的读取请求,没有HW的限制。同时,Follower也会维护一份自己的HW,Follower.HW = min(Leader.HW, Follower.offset)

c.kafka的保证leader负载均衡
kafka一个topic具有多个分区能够实现负载均衡,当kafka的一个分区的leader挂掉后,会从ISR(每个分区的 leader 会维护一个 ISR 列表(针对每个分区一个ISR),ISR 列表里面就是 follower 副本的 borker 编号)选择第一个最新的broker作为leader节点。leader指的是broker节点。负载均衡避免了一个作为leader节点的broker管理多个分区。
d.ISR、OSR、AR 是什么
ISR:In-Sync Replicas 副本同步队列
OSR:Out-of-Sync Replicas
AR:Assigned Replicas 所有副本
ISR是由leader维护,follower从leader同步数据有一些延迟(具体可以参见 图文了解 Kafka 的副本复制机制),超过相应的阈值会把 follower 剔除出 ISR, 存入OSR(Out-of-Sync Replicas )列表,新加入的follower也会先存放在OSR中。AR=ISR+OSR。这些信息都放到了zk中。
e.kafka分区的分配策略
同一个 Consumer Group 内新增消费者
消费者离开当前所属的Consumer Group,包括shuts down 或 crashes
订阅的主题新增分区
当发j生上面3中情况时,kafka会对分区重新进行分配。
kafka存在两种分区策略range和RoundRobin。
range策略:
将topic的分区进行排序并编号,除消费者的数量,11/3 一个消费者消费0.1.2,3 第二个消费者消费4,5,6,7 第三个消费8,9,10
RoundRobin strategy策略:
两个前提
1)同一个Consumer Group里面的所有消费者的num.streams必须相等;
2)每个消费者订阅的主题必须相同。
策略:RoundRobin策略的工作原理:将消费组内所有消费者以及消费者所订阅的所有topic的partition按照字典序排序,然后通过轮询方式逐个将分区以此分配给每个消费者。如果不满足上面的策略,则会造成,消费者会消费多个分区。
https://blog.csdn.net/u013256816/article/details/81123625
d.kafka速度为什么快
1.支持顺序读写,零copy,数据不经过用户态,在内核态进行内存映射,批量处理。
2.生产者,写消息到leader节点会有cache缓存,单独启进程将消息刷到磁盘,批量处理。
3.消费者,顺序消费fowller,顺序读写磁盘, 零copy(数据不经过用户态,在内核态进行内存映射, 文件io到socket的io)
e.kakfa消费顺序性
https://www.jianshu.com/p/02fdcb9e8784
消息写到一个分区中,由一个消费者消费,消费者内部要开多线程的话,将消息分发到一个内存队列中,由一个线程进行消费

3.区别

kafka的broker无状态,不保留消费者消费到那个offset。
rabbitmq的broker会保留,消费者的确认到的消息信息。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值