RabbitMQ

Message Queue

消息队列,又称消息中间件,是典型的生产者和消费者模型,生产者不断向队列中生产消息,消费者不断从队列中获取消息。因为消息的生产和消费都是异步进行的,所以只需要关心消息的发送和接收,没有业务逻辑的侵入,轻松实现系统间的解耦。

AMQP

Advanced Message Queue Protocol(高级消息队列协议),AMQP不是从API层面进行限定的,而是直接定义网络交换的数据格式。

创建虚拟机时,虚拟机的命名必须是以“/”开头,生产者可以将消息发送给交换机,也可以直接发送给队列。

1. 创建虚拟机和用户(同时给该用户赋权限)

2. 将虚拟机和用户设置关联

RabbitMQ 的几种工作模式

生产者发送消息后要关闭连接,避免浪费资源,消费者不能关闭连接,否则接收不到队列中的消息。

1. 点对点

最简单的工作模式,生产者不经过交换机直接将消息发送到队列,消费者从队列中获取消息。

工具类:

public class RabbitMQUtils {
    // 创建mq连接工厂
    public static ConnectionFactory connectionFactory = new ConnectionFactory();

    static {
        // 设置mq服务器地址
        connectionFactory.setHost("110.27.10.63");
        // 设置mq端口号
        connectionFactory.setPort(5672);
        // 设置需要连接的虚拟主机名
        connectionFactory.setVirtualHost("/ems");
        // 设置访问虚拟主机的账号和密码
        connectionFactory.setUsername("emsuser");
        connectionFactory.setPassword("123");
    }
    public static Connection getConnection() {
        try {
            return connectionFactory.newConnection();
        } catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
    public static void close(Connection connection, Channel channel){
        try {
            if(channel!=null)channel.close();
            if(connection!=null)connection.close();
        } catch (Exception e){
            e.printStackTrace();
        }
    }

}

生产者:

public void sendMessage() throws IOException, TimeoutException {
        Connection connection = RabbitMQUtils.getConnection();
        // 获取连接中的通道
        Channel channel = connection.createChannel();
        /*
         * 通道绑定指定队列(可理解为提前对某些队列的一些设置,对指定队列的参数要与消费者一致)
         * arg1:要绑定的队列名称,队列不存在则自动创建
         * arg2:是否持久化队列(是队列本身,不包含队列中的数据),true-是
         * arg3:当前连接是否独享队列,true-是
         * arg4:当前队列在被消费完成后是否自动被删除(消费完成后只有消费者与队列断开连接才会彻底删除该队列),true-是
         * arg5:额外附加参数
         */
        channel.queueDeclare("hello1",true,false,true,null);
        /*
         * 发布消息(具体发布信息到哪个队列)
         * arg1:交换机名
         * arg2:路由key(没有交换机时为队列名)
         * arg3:传递消息额外设置(如消息持久化参数:MessageProperties.PERSISTENT_TEXT_PLAIN)
         * arg4:消息内容
         */
        channel.basicPublish("","hello1", MessageProperties.PERSISTENT_TEXT_PLAIN,"hello rabbit's1收到".getBytes());
        RabbitMQUtils.close(connection,channel);
    }

消费者:

public static void main(String[] args) throws IOException, TimeoutException {

        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare("hello1",true,false,true,null);
        /*
         * 消费消息(对队列的参数要与生产者一致)
         * arg1:要消耗消息的队列名
         * arg2:开启消息自动确认机制(接收到消息就确认,不关心业务逻辑是否完成)
         * arg3:消费时的回调接口
         */
        channel.basicConsume("hello1", true, new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("============"+new String(body));
            }
        });

    }

2. work queue

点对点模式中当生产速度大于消费速度时,队列中会出现大量消息堆积,无法及时处理。多个消费者共同消费同一个队列时,消息消费速度会大大提高,消息一旦被消费就会从队列中消失,因此不会出现重复消费。

默认情况下队列的消息分配策略是平均分配,rabbitmq只关心消费者是否拿到消息,不关系消息的处理业务是否完成,所以会导致处理消息慢的消费者消息积压。(也就是默认的平均分配策略是不可取的。)

根据消费者的处理速度决定消息的分配:将消费者消息自动确认改成手动确认,实现能者多劳

public class Consumer1 {
    public static void main(String[] args) throws IOException {
        Connection connection = RabbitMQUtils.getConnection();
        final Channel channel = connection.createChannel();
        channel.queueDeclare("work",true,false,false,null);
        // 每次只消费一个消息
        channel.basicQos(1);
        // arg2:消息自动确认
        channel.basicConsume("work",false,new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("consumer-1:"+new String(body));
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 手动确认消息 arg1:当前消息,arg2:是否开启多个消息同时确认
                channel.basicAck(envelope.getDeliveryTag(),false);
            }
        });
    }
}

3. fanout(广播或发布订阅)

生产者将消息发给交换机,由交换机将消息发送给与此交换机绑定过的所有队列。

public class Provider {
    public static void main(String[] args) throws IOException {
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();
        /*
         * 通道指定交换机
         * arg1:交换机名称
         * arg2:交换机类型
         */
        channel.exchangeDeclare("logs","fanout");
        channel.basicPublish("logs","",null,"fanout广播模式11sdsd22".getBytes());
        RabbitMQUtils.close(connection,channel);
    }
}
public class Consumer1 {
    public static void main(String[] args) throws IOException {
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();
        // 通道声明交换机
        channel.exchangeDeclare("logs","fanout");
        // 创建临时队列
        String queue = channel.queueDeclare().getQueue();
        /*
         * 队列与交换机绑定
         * 参数分别为:队列名、交换机名、rountkey
         */
        channel.queueBind(queue,"logs","");
        // 消费信息
        channel.basicConsume(queue,true,new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("consumer1:"+new String(body));
            }
        });
    }
}

4. direct(路由)

生产者发送消息时指定路由键,队列只从交换机中获取指定路由键的消息,而不是全部获取交换机中的消息。与fanout模式多指定了rountingKey而已

public class Provider {
    public static void main(String[] args) throws IOException {
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();
        // 声明交换机,并指定为订阅类型
        channel.exchangeDeclare("logs_direct","direct");
        String routingName = "debug";
        channel.basicPublish("logs_direct",routingName,null,routingName.getBytes());
        RabbitMQUtils.close(connection,channel);
    }
}
public class Comsumer1 {
    public static void main(String[] args) throws IOException {
        Connection connection = RabbitMQUtils.getConnection();
        final Channel channel = connection.createChannel();
        channel.exchangeDeclare("logs_direct","direct");
        channel.basicQos(1);
        String queue = channel.queueDeclare().getQueue();
        channel.queueBind(queue,"logs_direct","info");
        channel.basicConsume(queue,false,new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("Consmer-1:"+new String(body));
                channel.basicAck(envelope.getDeliveryTag(),false);
            }
        });
    }
}
public class Consumer2 {
    public static void main(String[] args) throws IOException {
        Connection connection = RabbitMQUtils.getConnection();
        final Channel channel = connection.createChannel();
        channel.exchangeDeclare("logs_direct","direct");
        String queue = channel.queueDeclare().getQueue();
        channel.queueBind(queue,"logs_direct","debug");
        channel.queueBind(queue,"logs_direct","info");
        channel.queueBind(queue,"logs_direct","error");
        channel.basicQos(1);
        channel.basicConsume(queue,false,new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("Consumer-2:"+new String(body));
                channel.basicAck(envelope.getDeliveryTag(),false);
            }
        });
    }
}

5. topic(主题)

与direct模式不同的是不再是固定的rountingkey,而是模糊的rountingkey。

*:替代一个单词

#:替代0到多个单词

public class Provider {
    public static void main(String[] args) throws IOException {
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();
        // 声明交换机名称和类型
        channel.exchangeDeclare("topics","topic");
        String routingKey = "sd.name";
        // 发送信息
        channel.basicPublish("topics",routingKey,null,("路由key为:"+routingKey).getBytes());
        RabbitMQUtils.close(connection,channel);
    }
}
public class Consumer1 {
    public static void main(String[] args) throws IOException {
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();
        channel.exchangeDeclare("topics","topic");
        String queue = channel.queueDeclare().getQueue();
        String routingKey = "name.*";
        channel.queueBind(queue,"topics",routingKey);
        channel.basicConsume(queue,true,new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("Consumer-1 消费的路由为:"+new String(body));
            }
        });
    }
}
public class Consumer2 {
    public static void main(String[] args) throws IOException {
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();
        channel.exchangeDeclare("topics","topic");
        String queue = channel.queueDeclare().getQueue();
        channel.queueBind(queue,"topics","name.#");
        channel.queueBind(queue,"topics","*.name.*");
        channel.basicConsume(queue,true,new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("Consumer-2 消费路由为:"+new String(body));
            }
        });
    }
}

通过channel.queueDeclare().getQueue()获得的队列为临时队列,该队列不会持久化,要想获取持久化数据要做以下修改:

生产者发送消息时将消息持久化:

channel.basicPublish("topics",routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN,("路由key为:"+routingKey).getBytes());

消费者获取非临时队列并绑定到指定交换机exchange:

String queue = channel.queueDeclare("holo",true,false,false,null).getQueue();
channel.queueBind(queue,"topics","name.#");

生产者消息确认

事务

有一条消息发送失败,则全部消息回滚。事务的机制是阻塞性的,在发送一条消息后要等待rabbitmq回应后才能发送下一条; 会增加生产者与broker的交互次数,造成资源的浪费; 整体效率不高;因此实际应用中比较少用。

public static void main(String[] args) throws IOException {
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();
        channel.exchangeDeclare("tx.exchange.direct","direct");
        channel.queueDeclare("tx.direct.queue",true,false,true,null);
        channel.queueBind("tx.direct.queue","tx.exchange.direct","info");
        /**
         * 事务的机制是阻塞性的,在发送一条消息后要等待rabbitmq回应后才能发送下一条;
         * 会增加生产者与broker的交互次数,造成资源的浪费;
         * 整体效率不高;因此实际应用中比较少用。
         */
        // 开启事务
        channel.txSelect();
        try {
            channel.basicPublish("tx.exchange.direct","info",null,"事务模式1".getBytes());
            int a = 10/0;
            channel.basicPublish("tx.exchange.direct","info",null,"事务模式2".getBytes());
            // 事务提交
            channel.txCommit();
            System.out.println("消息发送成功提交");
        } catch (Exception e){
            // 事务回滚
            channel.txRollback();
            System.out.println("消息发送失败回滚");
            // 处理发送失败的消
        } finally {
            RabbitMQUtils.close(connection,channel);
        }
    }

消息确认

1. 单条确认

缺点:每条消息都要确认,效率不是很高;

优点:能明确知道是哪条消息发送失败,可以避免补发重复。

Connection connection = RabbitMQUtils.getConnection();
        Channel channel = null;
        try {
            channel = connection.createChannel();
            channel.exchangeDeclare("confirm.exchange","direct");
            channel.queueDeclare("confirm.queue",true,false,true,null);
            channel.queueBind("confirm.queue","confirm.exchange","info");
            /*
             * 开启消息确认机制
             *  confirm只能保证消息到达exchange,无法保证消息被exchange分发到queue。
             */
            channel.confirmSelect();
            channel.basicPublish("confirm.exchange","info",null,new Book("出埃及记",10).toString().getBytes());
            /*
             * 确认消息是否发送成功,
             * 缺点:每条消息都要确认,效率不是很高
             * 优点:能明确知道是哪条消息发送失败,可以避免补发重复
             */
            boolean b = channel.waitForConfirms(1000);
            if(b){
                System.out.println("消息发送成功");
            } else {
                System.out.println("消息发送失败");
                // 递归补发,递归一定次数仍然失败后,将数据存入数据库或redis
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            System.out.println("消息发送失败:阻塞");
            // 递归补发,递归一定次数仍然失败后,将数据存入数据库或redis
        } catch (TimeoutException e) {
            System.out.println("消息发送失败:超时");
            // 递归补发,递归一定次数仍然失败后,将数据存入数据库或redis
        } finally {
            RabbitMQUtils.close(connection,channel);
        }

 2. 批量确认

优点:消息批量确认,效率很高;

缺点:批量消息中有一条发送失败则认定失败,抛出异常。消息补发时会全部重新补发,会出现消息重复发送,发送效率变低

        try {
            channel = connection.createChannel();
            channel.exchangeDeclare("confirm.exchange","direct");
            channel.queueDeclare("confirm.queue",true,false,true,null);
            channel.queueBind("confirm.queue","confirm.exchange","info");
            // 开启消息确认
            /*
             * 批量确认消息是否发送成功,
             * 优点:消息批量确认,效率很高
             * 缺点:批量消息中有一条发送失败则认定失败,抛出异常。消息补发时会全部重新补发,会出现消息重复发送,发送效率变低
             */
            channel.confirmSelect();
            channel.basicPublish("confirm.exchange","info",null,new Book("出埃及记",10).toString().getBytes());
            channel.basicPublish("confirm.exchange","info",null,new Book("出埃及记",10).toString().getBytes());
            channel.waitForConfirmsOrDie(1000);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("消息发送失败");
            // 递归补发,递归一定次数仍然失败后,将数据存入数据库或redis
        } catch (TimeoutException e) {
            e.printStackTrace();
            System.out.println("消息发送失败超时");
            // 递归补发,递归一定次数仍然失败后,将数据存入数据库或redis
        } finally {
            RabbitMQUtils.close(connection,channel);
        }

3. 异步确认

        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = null;
        try {
            channel = connection.createChannel();
            channel.exchangeDeclare("confirm.exchange","direct");
            channel.queueDeclare("confirm.queue",true,false,true,null);
            channel.queueBind("confirm.queue","confirm.exchange","info");
            /*
             * 开启confirm 机制
             * exchange不能持久化消息,queue能持久化,由于confirm机制只能确认消息是否发送到exchange,而不能确认消息是否从exchange发送到queue,
             * 所以当消息到达exchange而为到queue时发生异常,同样会导致消息丢失,
             *  所以还需开启return 机制
             */
            channel.confirmSelect();
            /*
             * 异步confirm监听
             */
            channel.addConfirmListener(new ConfirmListener() {
                /*
                 * 消息被确认的回调
                 * arg1: 当前消息编号
                 * arg2: 当前消息是否同时确认了多条
                 */
                public void handleAck(long l, boolean b) throws IOException {
                    System.out.println("当前确认消息编号为:"+l+"。同时确认为:"+b);
                }
                /*
                 * 消息没被确认的回调
                 * arg1: 当前消息编号
                 * arg2: 当前消息是否同时确认了多条
                 */
                public void handleNack(long l, boolean b) throws IOException {
                    System.out.println("当前未被确认消息编号为:"+l+"。同时确认为:"+b);
                }
            });
            /*
             * return 监听
             */
            channel.addReturnListener(new ReturnListener() {
                // 当消息没有到达queue时才会执行
                public void handleReturn(int i, String s, String s1, String s2, AMQP.BasicProperties basicProperties, byte[] bytes) throws IOException {
                    System.out.println(new String(bytes,"utf-8")+"没有被送达queue中.");
                }
            });
            // 第三个参数为true时才会触发return机制(默认为false)
            channel.basicPublish("","info",true,null,new Book("西游记",110).toString().getBytes());
            // deliveryMode 交付模式为1表示持久化消息,反之为0;为每一个Message设置一个id,为消费者消息防重复消费做铺垫
            AMQP.BasicProperties basicProperties = new AMQP.BasicProperties().builder().deliveryMode(1).messageId(UUID.randomUUID().toString()).build();
            channel.basicPublish("confirm.exchange","info",true,basicProperties,new Book("出埃及记",10).toString().getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 异步监听要求连接不能断
            // RabbitMQUtils.close(connection,channel);
        }

消费者消息防重复消费

问题:有多个消费者消费同一个队列时,一个消费者拿到消息消费过程中又被另一个消费者拿到消费,造成消息的重复消费。

思路:当消息被分给某一个消费者时,在redis中记录该消息的唯一标识,当其他消费者也拿到了此消息时先去redis中查看是否有此消息的标识,如果没有则进行消费;如果有且没消费完毕,则不做任何操作,如果有且消费完成,则确认此消息。为避免消费消息过程中出现死锁等异常情况 ,在redis中记录消息标识时需设置超时时间。

    public static void main(String[] args) {
        Connection connection = RabbitMQUtils.getConnection();
        try {
            final Channel channel = connection.createChannel();
            channel.exchangeDeclare("confirm.exchange","direct");
            channel.queueDeclare("confirm.queue",true,false,true,null);
            channel.queueBind("confirm.queue","confirm.exchange","info");
            final Jedis jedis = new Jedis("110.37.20.5",8888);
            channel.basicConsume("confirm.queue",false,new DefaultConsumer(channel){
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    System.out.println("sdsds:"+properties.getMessageId());
                    // 1. 在redis中记录当前要处理的消息
                    String set = jedis.set(properties.getMessageId(), "0", new SetParams().nx().ex(7));
                    // 2. 记录成功:说明redis 中之前没有该消息,该消息未被消费处理,此时进行消息的消费
                    if(set != null && set.equalsIgnoreCase("OK")){
                        System.out.println("接收到消息,处理!");
                        channel.basicAck(envelope.getDeliveryTag(),false);
                        jedis.set(properties.getMessageId(),"1",new SetParams().xx().ex(10));
                    } else {
                        System.out.println("jilu");
                        // 3. 记录失败:说明该消息已被记录,正在被处理;获取key的值,0-正在处理,不做任何操作,1-处理完成没确认,进行确认操作。
                        String status = jedis.get(properties.getMessageId());
                        if("1".equals(status)){
                            channel.basicAck(envelope.getDeliveryTag(),false);
                        } else {

                        }
                    }
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
            // 消息处理异常:将消息重新发送到队列尾部或者其他操作。
        }
    }

rabbitmq依赖:

    <dependency>
      <groupId>com.rabbitmq</groupId>
      <artifactId>amqp-client</artifactId>
      <version>5.9.0</version>
    </dependency>

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值