消息队列——rabbitMQ

什么是消息队列?

队列我们都知道,是一种数据结构,先进先出。那么消息队列就是指,队列中的内容,是一种消息,这个消息是一个抽象的概念,可以指很多东西,比如对象等。

消息队列的作用:

1.解耦:用户下订单,库存扣减。但是如果库存不能正常使用,就会影响用户下订单,所以就可以把用户下订单这个操作放入消息队列中,这样即使库存出问题了,也不会影响用户下订单。
2.消峰:当高并发来临的时候,把多余的高并发放入消息队列中。
3.日志:当系统产生异常的时候,可以把异常放入消息队列。

rabbitMQ:

1.什么是rabbitMQ?

它是消息队列的一种实现技术,使用它就可以实现消息队列的操作。

2.rabbitMQ的核心组件:

1.链接对象
链接分为长连接和短连接,其中长连接就像人类社会的大桥,不会轻易销毁,短连接就是频繁创建和销毁的,进程想要使用rabbitMQ的消息队列,就需要创建连接去连接rabbitMQ。

2.交换机(exchange)
进程不能直接操作消息队列的,它只能先访问交换机,让交换机去操作消息队列。

3.队列(queue)
队列是rabbitmq里接受存储消息对象的组件。必须绑定一个交换机,当交换机接受到客户端发送的消息时,会根据路由逻辑判断当前消息发送给哪个/哪些队列 消息封装成对象,最终发送到队列。
在这里插入图片描述

3.客户端角色

概念:所有可以链接rabbitmq的软件,代码,插件都可以是rabbitmq的客户端(根据客户端不同的功能区分角色

1.生产者(productor):
A把信息发送到交换机,交换机把信息存入消息队列,那么A就是生产者。

2.消费者(consumer):
消费者监听着消息队列,从消息队列中获取存储的消息进行操作。

3.无角色:
如果客户端连接上rabbitmq,没有发送消息,不需要接收消费消息,就是无角色的客户端。

4.rabbitMQ的五种工作模式(java实现):

要使用java客户端,就需要注入依赖

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

1.简单模式:
强调的是消费端结构,每个队列,只被一个消费端监听.
在这里插入图片描述
channel是连接rabbitMQ和客户端的桥梁,所以需要优先实例化channel,消息队列也需要通过channel来创建,每一个消息队列必须要绑定一个交换机。生产者在发送消息的时候需要通过channel去找到交换机,根据要求的路由key,去寻找队列,在简单模式中,消息queue的路由key就是queue的名字。

应用场景:手机短信,邮件发送

/**

 * 准备连接逻辑 java代码先连接rabbitmq

 * 才能发送消息,监听消费消息,声明创建组件

 */

public class SimpleMode {

    //创建连接对象

    private Channel channel;

    @Before

    public void initChannel() throws IOException, TimeoutException {

        //提供一些连接参数 host port username password

        //创建一个connection连接工厂

        ConnectionFactory factory=new ConnectionFactory();

        //提供属性

        factory.setUsername("guest");

        factory.setPassword("guest");

        factory.setHost("10.9.151.60");

        factory.setPort(5672);

        //拿到一个连接对象

        Connection connection = factory.newConnection();

        //从连接对象获取channel赋值给私有属性

        channel=connection.createChannel();

    }

    //只有通过channel能操作rabbitmq,这段代码就是客户端代码

    //实现消息的发送

    @Test

    public void productor() throws Exception {

        //准备一个发送的消息

        String msg="hello world,rabbitmq";

        //声明一个队列 queue01

        channel.queueDeclare("queue01",false,false,false,null);

        /*

           String queue:声明的创建的这个队列的名字 如果已经存在,声明无效

           boolean durable:队列是否持久化 持久化队列会在重启rm,宕机回复rm之后一并个回复,没有持久化的就没了

           bolean exclusive: 是否专属于当前连接.true表示专属,除了创建声明这个队列的连接可以操作他以外,别人不能用

           boolean autoDelete:最后一个channel连接完queue 是否自动删除

           Map args:表示队列声明时的各种属性

            例如:消息存活时间

                最大存储的消息个数

                ...

         */

        //默认绑定,(AMQP default) 特性:任意当前rabbitmq声明的队列

        //都会使用队列名称作为路由key绑定这个交换机

        //将消息发送给交换机

        channel.basicPublish("","queue01",null,msg.getBytes());

        /*

        String exchange:交换机名称 ""表示发送给(AMQP default)

        String routingKey:当前消息发送时携带的路由key

        BasicProperties props: 表示封装消息对象message时的各种属性

            属性的添加,可以丰富消息的信息,使得处理消费逻辑变得灵活

        byte[] body:消息体的二进制数据,java所有对象都可以成为消息体.

            由于这种消息数据占用网络带宽传输,存储在queue一段时间,

            消息的封装 遵循精简准确

         */

    }

    //消费端逻辑

    @Test

    public void consumer01() throws Exception {

        //可以通过channel信道连接rabbitmq,监听任何一个

        //或者多个队列,连接不断,可以通过方法获取队列中生成的消息

        //生成一个客户端消费对象

        QueueingConsumer consumer=new QueueingConsumer(channel);

        //将consumer绑定监听队列

        channel.basicConsume("queue01",consumer);

 

        //可以通过delivery获取消费者监听的队列里的信息

        QueueingConsumer.Delivery delivery = consumer.nextDelivery();

        //delivery看成是一个包含消息的对象 body properties header

        byte[] body = delivery.getBody();//content_type content_encoding

        AMQP.BasicProperties properties = delivery.getProperties();

        /*properties.getAppId();

        properties.getUserId();*/

        Integer priority = properties.getPriority();

        String contentEncoding = properties.getContentEncoding();

        System.out.println("属性encoding:"+contentEncoding);

        /*properties.getContentType();*/

        String msg=new String(body,"utf-8");

        System.out.println(delivery.getEnvelope().getRoutingKey());

        System.out.println(msg);

    }

}

2.争抢模式:
强调的消费端结构,队列可以由多个消费端同时监听形成争抢
在这里插入图片描述
原理和上面的简单模式差不多,唯一的区别就是代码最后加了一个死循环,表示启动了多个消费者去监听同一个端口。
应用场景:抢红包

/**

 * 准备连接逻辑 java代码能先连接rabbitmq

 * 才能发送消息,监听消费消息,声明创建组件

 */

public class SimpleMode {

    //创建连接对象

    private Channel channel;

    @Before

    public void initChannel() throws IOException, TimeoutException {

        //提供一些连接参数 host port username password

        //创建一个connection连接工厂

        ConnectionFactory factory=new ConnectionFactory();

        //提供属性

        factory.setUsername("guest");

        factory.setPassword("guest");

        factory.setHost("10.9.151.60");

        factory.setPort(5672);

        //拿到一个连接对象

        Connection connection = factory.newConnection();

        //从连接对象获取channel赋值给私有属性

        channel=connection.createChannel();

    }

    //只要通过channel能操作rabbitmq,这段代码就是客户端代码

    //实现消息的发送

    @Test

    public void productor() throws Exception {

        //准备一个发送的消息

        String msg="hello world,rabbitmq";

        //声明一个队列 queue01

        channel.queueDeclare("queue01",false,false,false,null);

        /*

           String queue:声明的创建的这个队列的名字 如果已经存在,声明无效

           boolean durable:队列是否持久化 持久化队列会在重启rm,宕机回复rm之后一并个回复,没有持久化的就没了

           bolean exclusive: 是否专属于当前连接.true表示专属,除了创建声明这个队列的连接可以操作他以外,别人不能用

           boolean autoDelete:最后一个channel连接完queue 是否自动删除

           Map args:表示队列声明时的各种属性

            例如:消息存活时间

                最大存储的消息个数

                ...

         */

        //默认绑定,(AMQP default) 特性:任意当前rabbitmq声明的队列

        //都会使用队列名称作为路由key绑定这个交换机

        //将消息发送给交换机

        channel.basicPublish("","queue01",null,msg.getBytes());

        /*

        String exchange:交换机名称 ""表示发送给(AMQP default)

        String routingKey:当前消息发送时携带的路由key

        BasicProperties props: 表示封装消息对象message时的各种属性

            属性的添加,可以丰富消息的信息,使得处理消费逻辑变得灵活

        byte[] body:消息体的二进制数据,java所有对象都可以成为消息体.

            由于这种消息数据占用网络带宽传输,存储在queue一段时间,

            消息的封装 遵循精简准确

         */

    }

    //消费端逻辑

    @Test

    public void consumer01() throws Exception {

        //可以通过channel信道连接rabbitmq,监听任何一个

        //或者多个队列,连接不断,可以通过方法获取队列中生成的消息

        //生成一个客户端消费对象

        QueueingConsumer consumer=new QueueingConsumer(channel);

        //将consumer绑定监听队列

        //channel.basicConsume("queue01",consumer);

        channel.basicConsume("queue01",false,consumer);

 

        //可以通过链接抵用消息消费

        QueueingConsumer.Delivery delivery = consumer.nextDelivery();

        channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);

        //delivery看成是一个包含消息的对象 body properties header

        byte[] body = delivery.getBody();//content_type content_encoding

        AMQP.BasicProperties properties = delivery.getProperties();

        /*properties.getAppId();

        properties.getUserId();*/

        Integer priority = properties.getPriority();

        String contentEncoding = properties.getContentEncoding();

        System.out.println("属性encoding:"+contentEncoding);

        /*properties.getContentType();*/

        String msg=new String(body,"utf-8");

        System.out.println(delivery.getEnvelope().getRoutingKey());

        System.out.println(msg);

        //如果不自动确认,需要手动返回,告诉rabbitmq确认哪条消息

        channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);

        while(true);

    }

}

3.路由模式:
强调交换机,将会按照消息的路由key转发消息。前面两个都是注重消费者端的,所以路由key都是queue的名字,这里就可以自定义。创建好channel,声明好queue,把queue绑定到交换机上并且声明好queue的路由key,生产者发送消息,指定交换机指定路由key,就可以发送到对应的queue上了。
需要注意的是这里的交换机类型是direct。

/**

 * 完成路由模式测试

 * 上海,北京作为2个队列路由key

 */

public class RouteMode {

    private Channel channel;

    @Before

    public void initChannel() throws IOException, TimeoutException {

        ConnectionFactory factory=new ConnectionFactory();

        factory.setUsername("guest");

        factory.setPassword("guest");

        factory.setHost("10.9.151.60");

        factory.setPort(5672);

        Connection connection = factory.newConnection();

        channel=connection.createChannel();

    }

    //准备一批静态常量

    private static final String type="direct";//自定义交换机类型

    private static final String exName=type+"EX";//自定义交换机名称

    //准备2个队列

    private static final String q1=type+"q01";

    private static final String q2=type+"q02";

    //声明所有组件 一个交换机,一个队列,和绑定关系

    @Test

    public void bind() throws Exception {

        //声明队列

        channel.queueDeclare(q1,false,false,false,null);

        channel.queueDeclare(q2,false,false,false,null);

        //声明交换机

        channel.exchangeDeclare(exName,type);

        //声明绑定关系

        //q1绑定exName 使用路由key 上海

        channel.queueBind(q1,exName,"上海");

        channel.queueBind(q1,exName,"天津");

        //q2绑定exName 使用路由key 北京

        channel.queueBind(q2,exName,"北京");

    }

    //发送消息

    @Test

    public void producter() throws IOException {

        //将消息携带一个路由key发送到exName

        channel.basicPublish(exName,"上海",null,"上海发送".getBytes());

    }

}

4.发布订阅模式:
强调的是一个交换机,会将消息发送给后端所有的队列queue群发
应用场景:广告推送,邮寄群发
在这里插入图片描述
使用方法和上面基本一样,唯一的不同就是这里的交换机类型是fanout。

public class FanoutMode {

    private Channel channel;

    @Before

    public void initChannel() throws IOException, TimeoutException {

        ConnectionFactory factory=new ConnectionFactory();

        factory.setUsername("guest");

        factory.setPassword("guest");

        factory.setHost("10.9.151.60");

        factory.setPort(5672);

        Connection connection = factory.newConnection();

        channel=connection.createChannel();

    }

    //准备一批静态常量

    private static final String type="fanout";//自定义交换机类型

    private static final String exName=type+"EX";//自定义交换机名称

    //准备2个队列

    private static final String q1=type+"q01";

    private static final String q2=type+"q02";

    //声明所有组件 一个交换机,一个队列,和绑定关系

    @Test

    public void bind() throws Exception {

        //声明队列

        channel.queueDeclare(q1,false,false,false,null);

        channel.queueDeclare(q2,false,false,false,null);

        //声明交换机

        channel.exchangeDeclare(exName,type);

        //声明绑定关系

        //q1绑定exName 使用路由key 上海

        channel.queueBind(q1,exName,"上海");

        channel.queueBind(q1,exName,"天津");

        //q2绑定exName 使用路由key 北京

        channel.queueBind(q2,exName,"北京");

    }

    //发送消息

    @Test

    public void producter() throws IOException {

        //将消息携带一个路由key发送到exName

        channel.basicPublish(exName,"上海",null,"上海发送".getBytes());

    }

}

5.主题模式:
其实我更喜欢叫他范围模式,这里queue绑定交换机使用的不再是详细的路由key,而是一个范围。范围内容:

#:任意多级的任意长度字符串
*:表示一级的任意字符串

在这里插入图片描述
交换机类型是topic,queue绑定交换机的时候,要使用范围服号表明,其他的和上面一样。

public class TopicMode {

    private Channel channel;

    @Before

    public void initChannel() throws IOException, TimeoutException {

        ConnectionFactory factory=new ConnectionFactory();

        factory.setUsername("guest");

        factory.setPassword("guest");

        factory.setHost("10.9.151.60");

        factory.setPort(5672);

        Connection connection = factory.newConnection();

        channel=connection.createChannel();

    }

    //准备一批静态常量

    private static final String type="topic";//自定义交换机类型

    private static final String exName=type+"EX";//自定义交换机名称

    //准备2个队列

    private static final String q1=type+"q01";

    private static final String q2=type+"q02";

    //声明所有组件 一个交换机,一个队列,和绑定关系

    @Test

    public void bind() throws Exception {

        //声明队列

        channel.queueDeclare(q1,false,false,false,null);

        channel.queueDeclare(q2,false,false,false,null);

        //声明交换机

        channel.exchangeDeclare(exName,type);

        //声明绑定关系

        //q1绑定exName 使用路由key 

        channel.queueBind(q1,exName,"中国.#");

        channel.queueBind(q1,exName,"中国.天津.*");

        //q2绑定exName 使用路由key 北京

        channel.queueBind(q2,exName,"*.北京.*");

    }
    //发送消息
    @Test
    public void producter() throws IOException {

        //将消息携带一个路由key发送到exName

        channel.basicPublish(exName,"中国.新疆.乌鲁木齐",null,"hahsadlfjasdlfj".getBytes());

    }

}

5.rabbit MQ的消息确认机制:

convertAndSend: 输出时没有顺序,不需要等待,直接运行。rabbitMQ不会帮你保存消息。

convertSendAndReceive: 使用此方法,只有确定消费者接收到消息,才会发送下一条信息,每条消息之间会有间隔时间。只有收到了确认信息,才会删除queue中的信息

5.1如何防止消息丢失

一、生产者没有成功把消息发送到RabbitMQ

因为网络传输的不稳定性,当生产者在向MQ发送消息的过程中,MQ没有接收到消息

解决办法:

1.使用事务(性能差)

使用AMQP协议提供的一个事务机制,在生产者发送消息之前,通过channel.txSelect开启一个事务,接着发送消息, 如果消息投递server失败,进行事务回滚channel.txRollback,然后重新发送, 如果server收到消息,就提交事务channel.txCommit

但是,很少有人这么干,因为这是同步操作,一条消息发送之后会使发送端阻塞,以等待RabbitMQ-Server的回应,之后才能继续发送下一条消息,生产者生产消息的吞吐量和性能都会大大降低

2.发送方确认机制(publisher confirm)

生产者开启confirm模式之后,你每次写的消息都会分配一个唯一的id,然后如果写入了rabbitmq中,rabbitmq会给你回传一个ack消息,告诉这个消息成功还是失败了。如果rabbitmq没能处理这个消息,会回调你一个nack接口,告诉你这个消息接收失败,可以重试。

3.做好容错方法(try-catch)

发送消息可能会网络失败,失败后要有重试机制,可记录到数据库,采用定期扫描重发的方式。每一条消息都做好日志记录,给数据库保存每一个消息的记录,定期扫描数据库将失败的消息重新发送。

二、RabbitMQ弄丢了消息

消息抵达消息队列,要将消息进行持久化,写入磁盘才算成功。此时mq没有持久化成功,mq宕机了造成消息丢失

解决办法:

1、发送方确认机制(publisher confirm)

生产者开启confirm模式之后,rabbitmq会给你回传一个ack消息,告诉这个消息成功还是失败了。如果rabbitmq没能处理这个消息,会回调你一个nack接口,告诉你这个消息接收失败,此时修改数据库中的mq状态,进行消息定期重试。配合上面的容错方法

三、消费端弄丢了数据

消费者刚拿到消息准备消费的时候,此时消费端没有消费消息,mq使用的是自动ack模式,mq就会认为你已经消费了,把消息丢掉

解决办法

一定开启手动ACK,消费成功才移除,失败或者没来得及处理就noAck并重新入队

6.springBoot中使用rabbitMQ

1.注入依赖:

<dependency>

           <groupId>org.springframework.boot</groupId>

           <artifactId>spring-boot-starter-amqp</artifactId>

</dependency>

2.在application配置文件中配置rabbitMQ相关属性:

spring.rabbitmq.host=10.9.151.60
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

3.自定义声明组件(queue,exchange,绑定关系)
启动类中去做这个声明,这样就可以在系统启动的时候完成初始化声明。

@SpringBootApplication

@EnableEurekaClient//开启客户端的功能注解

public class StarterEurekaClient {

    public static void main(String[] args) {

        SpringApplication.run(StarterEurekaClient.class,args);

    }

    //启动类当成声明组件配置类

    @Bean//声明一个队列

    public Queue queue01(){

        //对象包装的属性,会在第一次程序链接rabbitmq时

        //调用queueDeclear

        return new Queue("testqueue",false,false,false,null);

    }

    @Bean//声明交换机

    public DirectExchange ex01(){

        return new DirectExchange("testEX");

    }

    @Bean//绑定关系

    public Binding bind01(){

        return BindingBuilder.bind(queue01()).to(ex01()).with("test");

    }

}



4.声明生产者:
这里使用的对象是RabbitTemplate的对象,通过该对象就可以把消息发送给指定的exchange,在根据指定的路由key找到对应的queue。其中RabbitTemplate的对象还调用了convertAndSend,保证了只有在消费者接收到了消息,才会进行下一条消息的发送。

@RestController

public class HelloController {

    //访问项目,返回工程启动端口

    @Value("${server.port}")

    private String port;

 

    @RequestMapping("client/hello")

    public String sayHi(String name){

        return "Hello "+name+". I am from "+port;

        //name=王翠花,hello 王翠花. I am from port;

    }

 

    @Autowired

    private RabbitTemplate template;

    //发送一个请求 send String msg发送到已有的队列中

    @RequestMapping("send")

    public String send(String msg){

        //发送msg到队列

        MessageProperties properties=new MessageProperties();

        properties.setPriority(100);

        Message message=new Message(msg.getBytes(),properties);
//只是发送消息
        template.send("testEX","test",message);

        //channel.basicPublish(exName,routingkey,props,body);

        //send方法,客户端关心自定义封装消息过程
        //rabbitMQ的消息确认机制
        template.convertAndSend("testEX",

                "test",msg);

        return "succes";

    }

}

5.声明消费者:
消费者通过 @RabbitListener(queues=“testqueue”)来监听名为testqueue的queue,这个queues是可以指定多个消息队列,对他们一起监听。

@Component
public class ConsumerConp {
    @RabbitListener(queues="testqueue")
    public void consume(String msg){
        //当前方法就是消费逻辑调用的消费逻辑,底层链接
        //拿到消息之后会调用这个吧消息传递给方法参数
        System.out.println("消费端接收消息:"+msg);
    }
}

你可能感兴趣的:(java-rabbitmq,rabbitmq,java)