RabbitMQ

1,RabbitMQ的介绍

1,RabbitMQ的简介

        RabbitMQ是一个在AMQP(高级消息队列协议)基础上完成的,可复用的企业消息系统,是当前最主流的消息中间件之一。erlang语言开发,具有高并发特性,吞吐量到万级,MQ功能比较完备,健壮、稳定、易用、跨平台。

2,RabbitMQ的特点

1,可靠性

RabbitMQ使用一些机制来保证可靠性,如持久化、传输确认、发布确认。(保证消息不丢失)

2,灵活的路由

在消息进入队列之前,通过Exchange来路由消息的。对于典型的路由功能,RabbitMQ已经提供了一些内置的Exchange来实现。针对更复杂的路由功能,可以将多个Exchange绑定在一起,也通过插件机制实现自己的Exchange 。

3,消息集群

多个RabbitMQ服务器可以组成一个集群,形成一个逻辑Broker。

4,高可用

队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。

5,多种协议

RabbitMQ支持多种消息队列协议,比如STOMP、MQTT等等。

6,管理界面

RabbitMQ提供了一个易用的用户界面,使得用户可以监控和管理消息Broker的许多方面。

7,跟踪机制

如果消息异常,RabbitMQ提供了消息跟踪机制,使用者可以找出发生了什么。

8,插件机制

RabbitMQ提供了许多插件,来从多方面进行扩展,也可以编写自己的插件。

3,RabbitMQ工作原理介绍

1,Broker

表示消息队列服务器实体(RabbitMQ服务器)。

2,Exchange(交换机)

交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。

3,Queue(队列)

消息队列,用来保存消息到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。

4,Binding(路由key)

绑定,用于交换器和消息队列之间的关联。一个绑定就是基于路由键(路由key)将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。

5,Virtual Host(包含了交换机 路由key 队列 )

虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个vhost本质上就是一个mini版的RabbitMQ服务器,拥有自己的队列、交换器、绑定和权限机制。vhost是AMQP概念的基础,必须在连接时指定,RabbitMQ默认的vhost是/。当多个不同的用户使用同一个RabbitMQ Server提供的服务时,可以划分出多个vhost,每个用户在自己的 vhost创建交换器和队列等。

6,Connection(连接)

网络连接,比如一个TCP连接。

7,Channel(管道,信道)

信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内地虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁TCP都是非常昂贵的开销,所以引入了信道的概念,以复用一条TCP连接。

8,Message(消息)

消息,它由消息头(放属性)消息体(具体内容)组成。消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出消息可能需要持久性存储)等。

9,Publisher(生产者)

消息的生产者,也是一个向交换器发布消息的客户端应用程序

10>Consumer(消费者)

消息的消费者,表示一个从消息队列中取得消息的客户端应用程序

2,RabbitMQ的安装

1,配置docker

1,配置镜像加速器

阿里云登录 - 欢迎登录阿里云,安全稳定的云计算服务平台

 向docker中添加配置文件daemon.json配置镜像加速器:

tee /etc/docker/daemon.json <<-'EOF'

{

  "registry-mirrors": ["https://fymtl0tv.mirror.aliyuncs.com"]

}

EOF

systemctl daemon-reload

systemctl restart docker

2,配置自启动

设置Docker随Linux一起启动:

systemctl enable docker

2, docker安装RabbitMQ

1,拉取镜像

docker pull rabbitmq

docker pull rabbitmq:management

2, 运行RabbitMQ容器

5672是RabbitMQ服务器端口;15672是RabbitMQ控制台端口,可用于访问web管理界面。

docker run --name rabbitmq -p 15672:15672 -p 5672:5672  -d rabbitmq:3-management

 停止/启动RabbitMQ容器:

docker stop/start rabbitmq

开放5672和15672端口: 

firewall-cmd --zone=public --add-port=5672/tcp --permanent

firewall-cmd --zone=public --add-port=15672/tcp --permanent

firewall-cmd --reload

3,测试访问 

访问RabbitMQ的web管理界面测试RabbitMQ是否安装成功:http://LinuxIp:15672

用户名:guest ------密码:guest

3,Rabbitmq的web管理界面设置

1,创建虚拟主机

2,创建用户

3, 给用户分配虚拟主机

4,Rabbitmq在idea中的原生使用 

1,创建maven项目并引入依赖

<dependencies>
        <!--导入RabbitMQ的依赖-->
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.9.0</version>
        </dependency>
</dependencies>

2,hello模式的使用

 一个生产者对应一个消费者,生产者向队列中生产消息,消费者从队列中消费消息。

1,创建生产者主方法

public class Producer {
    public static void main(String[] args) throws Exception {
        //创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //配置连接工厂
        connectionFactory.setHost("192.168.40.128");//RabbitMQ服务器ip
        connectionFactory.setPort(5672);//RabbitMQ服务器端口
        connectionFactory.setUsername("mmy");//用户名
        connectionFactory.setPassword("123456");//密码
        connectionFactory.setVirtualHost("/myhost");//虚拟主机
        //创建连接
        Connection con = connectionFactory.newConnection();
        //获取通道(多路复用连接,在连接中开出一条通道)
        Channel channel = con.createChannel();
        /*
          声明一个队列:
          参数一:队列名称
          参数二:队列是否持久化
          参数三:通道是否独占这个队列
          参数四:当队列中没有消息的时候是否自动删除队列
          参数五:其它设置,没有就给null
         */
        channel.queueDeclare("hello.queue", true, false, false, null);
        /*
          发送消息:
          参数一:交换机的名称,没有就给""
          参数二:路由key,没有就给队列名称
          参数三:消息属性,没有就给null
          参数四:消息内容 byte[]
         */
        channel.basicPublish("", "hello.queue", null, "我是hello的第一个消息".
getBytes());
        //关闭资源(后开先关)
        channel.close();
        con.close();
        System.out.println("消息已发出...");
    }
}

2,创建消费者主方法

public class Customer {
    public static void main(String[] args) throws Exception {
        //创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //配置连接工厂
        connectionFactory.setHost("192.168.40.128");//RabbitMQ服务器ip
        connectionFactory.setPort(5672);//RabbitMQ服务器端口
        connectionFactory.setUsername("mmy");//用户名
        connectionFactory.setPassword("123456");//密码
        connectionFactory.setVirtualHost("/myhost");//虚拟主机
        //创建连接
        Connection con = connectionFactory.newConnection();
        //获取通道(多路复用连接,在连接中开出一条通道)
        Channel channel = con.createChannel();
        /*
          消费消息:
          参数一:队列名称
          参数二:消息消费成功的确认模式,true自动模式,false手动模式
          参数三:消费者,当获取到消息的时候会自动执行其回调函数
         */
        channel.basicConsume("hello.queue", 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));
            }
        });
        //回调是异步的,让线程休眠,回调执行结束再关闭资源
        Thread.sleep(1000);
        //关闭资源(后开先关)
        channel.close();
        con.close();
    }
}

3,封装重复代码创建RabbitmqUtil 

public class RabbitmqUtil {
    private static ConnectionFactory connectionFactory;
    //实例化并配置连接工厂
    static{
        connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("192.168.9.128");
        connectionFactory.setPort(5672);
        connectionFactory.setUsername("mmy");
        connectionFactory.setPassword("123456");
        connectionFactory.setVirtualHost("/myhost");
    }
    //获取连接的静态封装方法
    public static Connection getConnection(){
        try {
            return connectionFactory.newConnection();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    //关闭资源的静态封装方法
    public static void close(Channel channel, Connection con){
        if(channel!=null){
            try {
                channel.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        if(con!=null){
            try {
                con.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

3,work模式的使用

一个生产者对应多个消费者,但是一条消息只能由一个消费者获得;

轮询分发:将消息队列中的消息,依次发送给所有消费者,一个消息只能被一个消费者获取。

1,创建消费者A主方法

public class CustomerA {
    public static void main(String[] args) throws Exception{
        //获取连接
        Connection con = RabbitmqUtil.getConnection();
        //获取通道
        Channel channel = con.createChannel();
        //声明队列 -- 生产者和消费者,哪方先运行在哪方声明队列
        channel.queueDeclare("work.queue", true, false, false, null);
        //消费消息
        channel.basicConsume("work.queue", 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));
            }
        });
        //让线程休眠,回调执行结束再关闭资源
        Thread.sleep(1000*60);
        //关闭资源
        RabbitmqUtil.close(channel, con);
    }
}

2,创建消费者B主方法

public class CustomerB {
    public static void main(String[] args) throws Exception{
        //获取连接
        Connection con = RabbitmqUtil.getConnection();
        //获取通道
        Channel channel = con.createChannel();
        //声明队列
        channel.queueDeclare("work.queue", true, false, false, null);
        //消费消息
        channel.basicConsume("work.queue", 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));
            }
        });
        //让线程休眠,回调执行结束再关闭资源
        Thread.sleep(1000*60);
        //关闭资源
        RabbitmqUtil.close(channel, con);
    }
}

3,创建生产者主方法

public class Producer {
    public static void main(String[] args) throws Exception{
        //获取连接
        Connection con = RabbitmqUtil.getConnection();
        //获取通道
        Channel channel = con.createChannel();
        //生产10条消息
        for (int i = 1; i <=10 ; i++) {
            channel.basicPublish("", "work.queue", null, ("这是第"+i+"条消息").getBytes());
        }
        //关闭资源
        RabbitmqUtil.close(channel, con);
    }
}

4,消息的手动确认机制

public class CustomerA {
    public static void main(String[] args) throws Exception{
        //获取连接
        Connection con = RabbitmqUtil.getConnection();
        //获取通道
        Channel channel = con.createChannel();
        //声明队列 -- 生产者和消费者,哪方先运行在哪方声明队列
        channel.queueDeclare("work.queue", true, false, false, null);
        //消费消息
        channel.basicConsume("work.queue", true, new DefaultConsumer(channel){
            int i = 1;
            //回调函数
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body)
                    throws IOException {
                //每消费一条消息i就自增一次,当i大于2时抛出异常,停止消费
                if(i>2){
                    throw new RuntimeException();
                }
                System.out.println("第一个消费者:"+new String(body));
                i++;
            }
        });
        //让线程休眠,回调执行结束再关闭资源
        Thread.sleep(1000*60);
        //关闭资源
        RabbitmqUtil.close(channel, con);
    }
}

4,fanout(广播)模式的使用

        一个生产者将消息首先发送到交换器,交换器绑定到多个队列,然后被监听该队列的消费者所接收并消费。两个消费者获得了同一条消息,如果没有队列绑定交换机,则消息将丢失,因为交换机没有存储能力,消息只能存储在队列中。

1,创建消费者A的主方法 

public class CustomerA {
    public static void main(String[] args) throws Exception{
        //获取连接
        Connection con = RabbitmqUtil.getConnection();
        //获取通道
        Channel channel = con.createChannel();
        /*
          声明一个交换机:
          生产者和消费者,哪方先运行在哪方声明交换机
          参数一:交换机的名称
          参数二:交换机的类型,BuiltinExchangeType.FANOUT广播类型
         */
        channel.exchangeDeclare("fanoutEx", BuiltinExchangeType.FANOUT);
        //声明一个临时队列--消费者连接关闭,队列就会被自动删除--返回值临时队列名称
        String queueName = channel.queueDeclare().getQueue();
        /*
          将队列和交换机绑定:
          参数一:队列名称
          参数二:交换机名称
          参数三:路由Key,没有给""
         */
        channel.queueBind(queueName, "fanoutEx", "");
        //消费消息
        channel.basicConsume(queueName, true, new DefaultConsumer(channel){
            //回调函数
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body)
                    throws IOException {
                System.out.println("消费者A:"+new String(body));
            }
        });
        //让线程休眠,回调执行结束再关闭资源
        Thread.sleep(1000*60);
        //关闭资源
        RabbitmqUtil.close(channel, con);
    }
}

2,创建消费者B的主方法

public class CustomerB {
    public static void main(String[] args) throws Exception{
        //获取连接
        Connection con = RabbitmqUtil.getConnection();
        //获取通道
        Channel channel = con.createChannel();
        /*
          声明一个交换机:
          生产者和消费者,哪方先运行在哪方声明交换机
          参数一:交换机的名称
          参数二:交换机的类型,BuiltinExchangeType.FANOUT广播类型
         */
        channel.exchangeDeclare("fanoutEx", BuiltinExchangeType.FANOUT);
        //声明一个临时队列--消费者连接关闭,队列就会被自动删除--返回值临时队列名称
        String queueName = channel.queueDeclare().getQueue();
        /*
          将队列和交换机绑定:
          参数一:队列名称
          参数二:交换机名称
          参数三:路由Key,没有给""
         */
        channel.queueBind(queueName, "fanoutEx", "");
        //消费消息
        channel.basicConsume(queueName, true, new DefaultConsumer(channel){
            //回调函数
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body)
                    throws IOException {
                System.out.println("消费者B:"+new String(body));
            }
        });

        //让线程休眠,回调执行结束再关闭资源
        Thread.sleep(1000*60);
        //关闭资源
        RabbitmqUtil.close(channel, con);
    }
}

3,创建生产者的主方法

public class Producer {
    public static void main(String[] args) throws Exception{
        //获取连接
        Connection con = RabbitmqUtil.getConnection();
        //获取通道
        Channel channel = con.createChannel();
        /*
           发送消息到交换机:
           参数一:交换机的名称
           参数二:路由key,没有给""
           参数三:消息属性,没有给null
           参数四:消息内容 byte[]
         */
        channel.basicPublish("fanoutEx", "", null, "这是fanout的一个消息".getBytes());
        //关闭资源
        RabbitmqUtil.close(channel, con);
    }
}

5,routing(路由)模式

生产者将消息发送到direct(直连)交换器,在绑定队列和交换器的时候有一个路由key,生产者发送的消息会指定一个路由key,那么消息只会发送到相应相同key的队列,接着监听该队列的消费者消费消息。

1,创建消费者A的主方法 

public class CustomerA {
    public static void main(String[] args) throws Exception{
        //获取连接
        Connection con = RabbitmqUtil.getConnection();
        //获取通道
        Channel channel = con.createChannel();
        /*
           声明交换机:
           参数一:交换机名称
           参数二:交换机类型,BuiltinExchangeType.DIRECT直连类型
         */
        channel.exchangeDeclare("directEx", BuiltinExchangeType.DIRECT);
        //声明一个临时队列
        String queueName = channel.queueDeclare().getQueue();
        /*
          将队列和交换绑定,同时给队列定义路由key,交换机会匹配路由key将消息
          存放到对应的队列中:
          参数一:队列名称
          参数二:交换机名称
          参数三:路由Key
         */
        channel.queueBind(queueName, "directEx", "k1");
        //消费消息
        channel.basicConsume(queueName, true, new DefaultConsumer(channel){
            //回调用函数
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body)
                    throws IOException {
                System.out.println("消费者A:"+new String(body));
            }
        });
        //让线程休眠,回调执行结束再关闭资源
        Thread.sleep(1000*60);
        //关闭资源
        RabbitmqUtil.close(channel, con);
    }
}

2,创建消费者B的主方法

public class CustomerB {
    public static void main(String[] args) throws Exception{
        //获取连接
        Connection con = RabbitmqUtil.getConnection();
        //获取通道
        Channel channel = con.createChannel();
        /*
           声明交换机:
           参数一:交换机名称
           参数二:交换机类型,BuiltinExchangeType.DIRECT直连类型
         */
        channel.exchangeDeclare("directEx", BuiltinExchangeType.DIRECT);
        //声明一个临时队列
        String queueName = channel.queueDeclare().getQueue();
        /*
          将队列和交换绑定,同时给队列定义路由key,交换机会匹配路由key将消息
          存放到对应的队列中:
          参数一:队列名称
          参数二:交换机名称
          参数三:路由Key
          给该队列定义了两个路由key k1和k2,那么交换机会将匹配到路由key k1
          和k2的消息都存放到该队列中;
         */
        channel.queueBind(queueName, "directEx", "k1");
        channel.queueBind(queueName, "directEx", "k2");
        //消费消息
        channel.basicConsume(queueName, true, new DefaultConsumer(channel){
            //回调用函数
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body)
                    throws IOException {
                System.out.println("消费者B:"+new String(body));
            }
        });
        //让线程休眠,回调执行结束再关闭资源
        Thread.sleep(1000*60);
        //关闭资源
        RabbitmqUtil.close(channel, con);
    }
}

3,创建生产者的主方法

public class Producer {
    public static void main(String[] args) throws Exception{
        //获取连接
        Connection con = RabbitmqUtil.getConnection();
        //获取通道
        Channel channel = con.createChannel();
        //发送消息
        //消费者A和消费者B绑定的队列指定的路由Key都有k1,所有此消息往消费者A和消费者B绑定
//的队列都会发送
        channel.basicPublish("directEx", "k1", null, "这是k1能看到的消息".getBytes());
        //消费者B绑定的队列指定的路由Key有k2,所有此消息只会往消费者B绑定的队列发送
        channel.basicPublish("directEx", "k2", null, "这是k2能看到的消息".getBytes());
        //关闭资源
        RabbitmqUtil.close(channel, con);
    }
}

5,Rabbitmq在springboot框架中的使用

1,环境配置

创建消费者和生产者两个模块

配置文件application.yml

spring:
  rabbitmq:
    host: 192.168.40.128
    port: 5672
    username: mmy
    password: 123456
    virtual-host: /myhost

2,hello模式的使用

1,创建生产者配置类

@Configuration
public class ProducerConfig {
    //配置一个Queue的对象,一个Queue对象即为一个队列,构造器参数为队列名称
    @Bean
    public Queue helloQueue(){
        return new Queue("hello.boot.queue");
    }
}

2,创建生产者测试类

@SpringBootTest
class RabbitmqProducerApplicationTests {
    //注入RabbitMQ模板
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Test
    void contextLoads() {
        //向队列hello.boot.queue中发送消息,参数一队列名称,参数二消息内容
        rabbitTemplate.convertAndSend("hello.boot.queue", "这是boot的hello模式的消息");
    }
}

3,创建消费者监听类

@Component
public class HelloListener {
    /*
一个监听方法即为一个消费者:
      1)监听方法必须public且无返回值;
      2)@RabbitListener(queues = "hello.boot.queue")
        监听名称为hello.boot.queue的队列,即从名称为hello.boot.queue队列中消费消息;
      3)参数Message message消息对象,包含消息头和消息体;
      4)参数Channel channel通道
     */
    @RabbitListener(queues = "hello.boot.queue")
    public void handlerHelloMsg(Message message, Channel channel){
        System.out.println("消费者:"+new String(message.getBody()));
    }
}

3,work模式的使用

1,创建消费者配置类

@Configuration
public class CustomerConfig {
    //创建个队列,队列名称为work.boot.queue
    @Bean
    public Queue workQueue(){
        return new Queue("work.boot.queue");
    }
}

2,创建消费者监听类

@Component
public class WorkListener {
    //一个监听方法即为一个消费者
    //从名称为work.boot.queue的队列中消费消息
    @RabbitListener(queues = "work.boot.queue")
    public void handlerWorkMsg1(Message message, Channel channel){
        System.out.println("消费者A:"+new String(message.getBody()));
    }
    //从名称为work.boot.queue的队列中消费消息
    @RabbitListener(queues = "work.boot.queue")
    public void handlerWorkMsg2(Message message, Channel channel){
        System.out.println("消费者B:"+new String(message.getBody()));
    }
}

3,创建生产者测试类

@SpringBootTest
class WorkTest {
    //注入RabbitMQ模板
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Test
    void contextLoads() {
        //向队列work.boot.queue中发送10条消息
        for(int i=1;i<=10;i++){
            rabbitTemplate.convertAndSend("work.boot.queue", "work模式第"+i+"个消息");
        }
    }
}

4,fanout广播模式的使用

1,创建消费者配置类

@Configuration
public class CustomerConfig {
    //配置一个FanoutExchange的对象,即为一个广播类型的交换机,构造器参数为交换机名称
    @Bean
    public FanoutExchange fanoutEx(){
        return new FanoutExchange("boot.fanoutEx");
    }
    //创建个队列,队列名称为fanout.boot.queue1
    @Bean
    public Queue fanoutQueue1(){
        return new Queue("fanout.boot.queue1");
    }
    //将队列fanout.boot.queue1和交换机boot.fanoutEx绑定
    @Bean
    public Binding fanoutQueue1Bind(){
        return BindingBuilder.bind(fanoutQueue1()).to(fanoutEx());
    }
    //再创建个队列,队列名称为fanout.boot.queue2
    @Bean
    public Queue fanoutQueue2(){
        return new Queue("fanout.boot.queue2");
    }
    //将队列fanout.boot.queue2和交换机boot.fanoutEx也绑定
    @Bean
    public Binding fanoutQueue2Bind(){
        return BindingBuilder.bind(fanoutQueue2()).to(fanoutEx());
    }
}

2,创建消费者监听类

@Component
public class FanoutListener {
    //一个监听方法即为一个消费者
    /*
      @RabbitListener(queues = "fanout.boot.queue1")
      从名称为fanout.boot.queue1的队列中消费消息;
     */
    @RabbitListener(queues = "fanout.boot.queue1")
    public void handlerFanoutMsg1(Message message, Channel channel){
        System.out.println("消费者A:"+new String(message.getBody()));
    }
    /*
      @RabbitListener(queues = "fanout.boot.queue2")
      从名称为fanout.boot.queue2的队列中消费消息;
     */
    @RabbitListener(queues = "fanout.boot.queue2")
    public void handlerFanoutMsg2(Message message, Channel channel){
        System.out.println("消费者B:"+new String(message.getBody()));
    }
}

3,创建生产者测试类

@SpringBootTest
class FanoutTest {
    //注入RabbitMQ模板
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Test
    void contextLoads() {
        //向交换机boot.fanoutEx中发送消息
        rabbitTemplate.convertAndSend("boot.fanoutEx", "", "这是boot的一条广播消息");
    }
}

5,routing路由模式的使用 

1,创建消费者配置类

@Configuration
public class CustomerConfig {
    //配置一个DirectExchange的对象,即为一个直连类型的交换机,构造器参数为交换机名称
    @Bean
    public DirectExchange directEx(){
        return new DirectExchange("boot.directEx");
    }
    //创建个队列,队列名称为direct.boot.queue1
    @Bean
    public Queue directQueue1(){
        return new Queue("direct.boot.queue1");
    }
    //将队列direct.boot.queue1和交换机boot.directEx绑定,同时给队列direct.boot.queue1
//定义个路由key为k1
    @Bean
    public Binding directQueue1BindK1(){
        return BindingBuilder.bind(directQueue1()).to(directEx()).with("k1");
    }
    //再创建个队列,队列名称为direct.boot.queue2
    @Bean
    public Queue directQueue2(){
        return new Queue("direct.boot.queue2");
    }
    //将队列direct.boot.queue2和交换机boot.directEx绑定,同时给队列direct.boot.queue2
//定义个路由key为k1
    @Bean
    public Binding directQueue2BindK1(){
        return BindingBuilder.bind(directQueue2()).to(directEx()).with("k1");
    }
    //将队列direct.boot.queue2和交换机boot.directEx绑定,同时给队列direct.boot.queue2
//再定义个路由key为k2
    @Bean
    public Binding directQueue2BindK2(){
        return BindingBuilder.bind(directQueue2()).to(directEx()).with("k2");
    }
}

2,创建消费者监听类

@Component
public class RoutingListener {
    //一个监听方法即为一个消费者
    /*
      @RabbitListener(queues = "direct.boot.queue1")
      从名称为direct.boot.queue1的队列中消费消息;
     */
    @RabbitListener(queues = "direct.boot.queue1")
    public void handlerDirectMsg1(Message message, Channel channel){
        System.out.println("消费者A:"+new String(message.getBody()));
    }
    /*
      @RabbitListener(queues = "direct.boot.queue2")
      从名称为direct.boot.queue2队列中消费消息;
     */
    @RabbitListener(queues = "direct.boot.queue2")
    public void handlerDirectMsg2(Message message, Channel channel){
        System.out.println("消费者B:"+new String(message.getBody()));
    }
}

3,创建生产者测试类

@SpringBootTest
class RoutingTest {
    //注入RabbitMQ模板
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Test
    void contextLoads() {
        //发送消息
        //消费者A和消费者B绑定的队列指定的路由Key都有k1,所有此消息往消费者A和消费者B
//绑定的队列都会发送
        rabbitTemplate.convertAndSend("boot.directEx", "k1", "这是k1能看到的消息");
        //消费者B绑定的队列指定的路由Key有k2,所有此消息只会往消费者B绑定的队列发送
        rabbitTemplate.convertAndSend("boot.directEx", "k2", "这是k2能看到的消息");
    }
}

6,消息的监视

        RabbitMQ提供了一个消费监视功能。(目的是确保消息不丢失,如果丢失了,也能知道在哪丢失的,然后进行人工处理)

1,以routing模式进行演示

1,修改生产者配置文件

#开启消息发送到交换机的监视
spring.rabbitmq.publisher-confirm-type=correlated
#开启消息未发送到队列的监视
spring.rabbitmq.publisher-returns=true

2,修改生产者测试类

@SpringBootTest
class RoutingWatchTest {

    //注入RabbitMQ模板
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    void contextLoads() {

        //在消息发送前进行监视

        /*
          给消息发送到交换机设置回调 -- 监视消息发送到了交换机:

          调用的是RabbitTemplate的setConfirmCallback()方法,其参数类型是函数式接口
          ConfirmCallback,通过Lambda表达式传递一个ConfirmCallback接口的实现类对象,
          重写的是其confirm()方法;

          confirm()方法有三个参数:
          参数一correlationData:表示相关数据,一般都没有;
          参数二ack:消息是否到达交换机,true到达,false未到达;
          参数三cause:消息未到达交换机的原因
         */
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            System.out.println(correlationData);
            System.out.println(ack);
            System.out.println(cause);
        });

        /*
          给消息未发送到队列设置回调 -- 监视消息未发送到队列:

          调用的是RabbitTemplate的setReturnCallback()方法,其参数类型是函数式接口
          ReturnCallback,通过Lambda表达式传递一个ReturnCallback接口的实现类对象,
          重写的是其returnedMessage()方法;

          returnedMessage()方法有5个参数:
          参数一message:消息
          参数二replyCode:失败的状态码
          参数三replyText:失败的说明
          参数四exchange:交换机
          参数五routingKey:路由key
         */
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, 
routingKey) -> {
            System.out.println(message);
            System.out.println(replyCode);
            System.out.println(replyText);
            System.out.println(exchange);
            System.out.println(routingKey);
        });

        //向交换机boot.directEx绑定的路由key为k2的队列direct.boot.queue2发送消息
        rabbitTemplate.convertAndSend("boot.directEx", "k2", "这是k2能看到的消息");

    }
}

3,rabbitmq客户端删除交换机

运行结果如下:

 启动消费者端重写建立交换机则可运行成功

4,rabbitmq客户端断开交换机与路由key(k2)绑定的队列的关系

2,代码优化

        定义实现类,同时实现ConfirmCallback和ReturnCallback接口,并分别重写confirm()和returnedMessage()方法,完成交换机消息到达的监视和队列消息未到达的监视。

1,创建配置类

@Component
public class MsgWatchHandler 
implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {

        System.out.println(correlationData);
        System.out.println(ack);
        System.out.println(cause);

    }

    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, 
                     String exchange, String routingKey) {

        System.out.println(message);
        System.out.println(replyCode);
        System.out.println(replyText);
        System.out.println(exchange);
        System.out.println(routingKey);

    }

    //注入RabbitTemplate
    @Autowired
    private RabbitTemplate rabbitTemplate;

    /*
      @PostConstruct对Bean对象做后置处理。目的是IOC容器启动,该实现类的Bean对象被创建,
      RabbitTemplate的Bean对象注入完成,就执行init()方法;

      然后在init()方法中调用RabbitTemplate的setConfirmCallback()和setReturnCallback()
      方法,分别对交换机消息到达和队列消息未到达进行监视;
     */
    @PostConstruct
    public void init(){
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnCallback(this);
    }
}

7,消息的签收

自动签收:不是很安全,消息消费失败后无法弥补。吞吐量大,1.5M/s - 4M/s。

手动签收:安全,能够保证消息不会丢失。吞吐量较低,1M/s左右。

消息id:生产者在发送消息的时候,可以为消息设置一个消息id,可用于唯一的标识消息。

1,搭建routing模式,消费者配置类

@Configuration
public class CustomerConfig {

    //配置一个直连交换机,交换机名称为boot.ackEx
    @Bean
    public DirectExchange ackEx(){
        return new DirectExchange("boot.ackEx");
    }

    //创建个队列,队列名称为ack.boot.queue
@Bean
    public Queue ackQueue(){
        return new Queue("ack.boot.queue");
    }

    //将队列ack.boot.queue和交换机boot.ackEx绑定,同时给队列ack.boot.queue定义个
//路由key为k
    @Bean
    public Binding ackQueueBindK(){
        return BindingBuilder.bind(ackQueue()).to(ackEx()).with("k");
    }
}

2,创建消费者监听队列

@Component
public class AckListener {

    //注入StringRedisTemplate
    @Autowired
    private StringRedisTemplate redisTemplate;

    /*
      @RabbitListener(queues = "ack.boot.queue")
      从名称为ack.boot.queue的队列中消费消息;
     */
    @RabbitListener(queues = "ack.boot.queue")
    public void handlerMsg(Message message, Channel channel){

        //拿到消息投递id
        long deliveryTag = message.getMessageProperties().getDeliveryTag();

        try {
            //拿到消息,将消息保存到数据库
            //System.out.println(10/0);//制造异常,表示消息保存失败

            /*
              保存成功了,则签收消息:
              参数一:消息的投递id
              参数二:false不批量签收
             */
            channel.basicAck(deliveryTag, false);
            System.out.println("消费者:"+new String(message.getBody()));

        } catch (Exception exception){
            /*
             保存失败或签收失败,则让队列继续投递消息,最多再投递3次,
             如果这3次还是保存或签收失败,则让队列不再继续投递:
             */
            System.out.println("消费者:失败");

            //以消息id为键向redis保存键值对,并让值自增一次,返回值为自增的值即消息投递次数
            String messageId = message.getMessageProperties().getMessageId();
            Long count = redisTemplate.opsForValue().increment(messageId);

            if(count<=3){
                try {
                    /*
                      如果消息投递次数<=3次,则拒收,让消息重新回到队列,被重新投递:
                      参数一:消息的投递id
                      参数二:false不批量签收
                      参数三:true拒收
                     */
                    channel.basicNack(deliveryTag, false, true);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }else{
                //如果消息投递次数>3次,则签收,目的是不让队列再继续投递消息了
                try {
                    channel.basicAck(deliveryTag, false);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

3,创建生产者测试类

@SpringBootTest
class RoutingAckTest {

    //注入RabbitMQ模板
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    void contextLoads() {
        //向交换机boot.ackEx绑定的路由key为k的队列ack.boot.queue发送消息
        rabbitTemplate.convertAndSend("boot.ackEx", "k", "这是k能看到的消息", message -> {
            //给消息设置个id
            String id = UUID.randomUUID().toString();
            message.getMessageProperties().setMessageId(id);
            return message;
        });
    }
}

4,修改配置文件

#开启手动签收模式
spring.rabbitmq.listener.simple.acknowledge-mode=manual

8,延时队列

1,搭建routing模式消费者配置类

@Configuration
public class CustomerConfig {
//配置一个直连交换机,交换机名称为boot.deadEx
    @Bean
    public DirectExchange deadEx(){
        return new DirectExchange("boot.deadEx");
    }
    //配置一个延时队列,队列名称为boot.msQueue
    @Bean
    public Queue msQueue(){
        Map<String, Object> map = new HashMap<>();
        //延时时间(1分钟)
        map.put("x-message-ttl", 1000*60);
        //时间到后走哪个交换机
        map.put("x-dead-letter-exchange", "boot.deadEx");
        //时间到后走哪个路由key绑定的队列
        map.put("x-dead-letter-routing-key", "dk");
        return new Queue("boot.msQueue", true, false, false, map);
    }
}

2,创建监听队列

@Component
public class DeadQueueListener {

    //一个监听方法即为一个消费者

    /*
     @RabbitListener(queues = "boot.msQueue")
     从名称为boot.msQueue的队列中消费消息,即从死信队列中消费消息;
    */
    @RabbitListener(queues = "boot.msQueue")
    public void handlerMsg(Message message, Channel channel){

        System.out.println("消费者:"+new String(message.getBody()));
        System.out.println("收到消息的时间:"+new Date());

        //签收消息
        try {
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

3,创建生产者测试类

@SpringBootTest
class MsQueueTest {

    //注入RabbitMQ模板
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    void contextLoads() {

        //向延时队列boot.msQueue发送消息
        rabbitTemplate.convertAndSend("boot.msQueue","这是延时消息");

        System.out.println("发送消息时间:"+new Date());
    }
}

4,发送消息也可指定延时时间(生产者测试类)

@SpringBootTest
class MsQueueTest {

    //注入RabbitMQ模板
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    void contextLoads() {

        //向延时队列boot.msQueue发送消息
        rabbitTemplate.convertAndSend("boot.msQueue","这是延时消息", message -> {

            //指定消息发送的演示时间为20秒
            message.getMessageProperties().setExpiration("20000");
            
            return message;
        });

        System.out.println("发送消息时间:"+new Date());
    }
}

9,MQ的应用场景

1,异步处理(增加用户体验)

用户注册后,需要发注册邮件和注册短信。传统的做法有两种1.串行的方式;2.并行方式

1,串行方式

2, 并行方式

2,应用解耦(高内聚,低耦合的设计思想)

3, 流量削锋

流量削锋也是消息队列中的常用场景,一般在秒杀或团抢活动中使用广泛。

应用场景:秒杀活动,一般会因为流量过大,导致流量暴增,应用挂掉。为解决这个问题,一般需要在应用前端加入消息队列。

a、可以控制活动的人数

b、可以缓解短时间内高流量压垮应用

 

4, 日志处理

日志处理是指将消息队列用在日志处理中,比如Kafka的应用,解决大量日志传输的问题。架构简化如下:

5, 消息通讯

1,点对点通讯:

2,聊天室通讯:

 10,秒杀实战

1,创建生产者boot项目

1,引入依赖

 <!--rabbitmq的依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

        <!--redis的依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!--手动引入的hutool工具包的依赖(需要使用bloom过滤器)-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.3.9</version>
        </dependency>
         
        <!--手动引入fastjson的依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.78</version>
        </dependency>

2,配置文件

#应用名称
spring.application.name=spike-producer
#服务器端口
server.port=8080

#--------------RabbitMQ的配置----------------

#RabbitMQ的服务器ip
spring.rabbitmq.host=192.168.9.128
#RabbitMQ的服务器端口
spring.rabbitmq.port=5672
#用户名
spring.rabbitmq.username=mmy
#密码
spring.rabbitmq.password=123456
#虚拟主机
spring.rabbitmq.virtual-host=/myhost

#------------redis的配置---------------------

#redis服务器的ip(安装redis的linux的ip)
spring.redis.host=192.168.9.128
#redis的端口
spring.redis.port=6379
#如果redis设置了密码则指定密码
spring.redis.password=mmy123
#操作redis的数据库的下标
spring.redis.database=1

3,主运行类

@SpringBootApplication
public class SpikeProducerApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpikeProducerApplication.class, args);
    }
    //配置Bloom过滤器
    @Bean
    public BitMapBloomFilter bitMapBloomFilter(){
        //构造器参数为位图大小(可以理解为过滤面的大小)
        return new BitMapBloomFilter(100);
    }
}

4,控制器类

@RestController
public class SpikeController {

    //注入bloom过滤器
    @Autowired
    private BitMapBloomFilter bitMapBloomFilter;

    //注入rabbitmq模板
    @Autowired
    private RabbitTemplate rabbitTemplate;

    //注入redis模板
    @Autowired
    private StringRedisTemplate redisTemplate;

    /*
      处理秒杀请求的方法,请求url为/doSpike,并传参用户id userId、商品id goodsId,
      向客户端响应字符串文本;
     */
    @RequestMapping("/doSpike")
    public String doSpike(Integer userId, Integer goodsId){

        /*
          1.通过bloom过滤器判断用户是否参与过该商品的秒杀:
            一个用户对一个商品只能秒杀一次
         */
        //通过用户id和商品id生成唯一标识
        String spikeId = userId+"-"+goodsId;
        //判断bloom过滤器中是否存在该标识,存在则已参与秒杀,不存在则未参与
        if(bitMapBloomFilter.contains(spikeId)){
            return "您已参加过该商品的抢购...";
        }

        /*
          2.操作redis判断是否还有库存,如果还有则对库存做扣减
         */
        //商品库存在redis中以键goods_stock:商品id,值库存数存储;
        //对键goods_stock:商品id的值即库存数做递减,并返回递减后的库存数;
        Long stock = redisTemplate.opsForValue().decrement("goods_stock:" + goodsId);
        if(stock<0){
            return "商品已被抢完了,下次早点来哦...";
        }

        //到此说明秒杀成功了

        //3.将用户id和商品id生产的唯一标识添加到bloom过滤器
        bitMapBloomFilter.add(spikeId);

        /*
          4.将订单信息发送到rabbitmq,即将用户id和商品id发送到rabbitmq
         */
        //将用户id和商品id存到map再转成json字符串发送给rabbitmq
        Map<String, Integer> map = new HashMap<>();
        map.put("userId", userId);
        map.put("goodsId", goodsId);
        String jsonStr = JSON.toJSONString(map);
        //发送消息到名称为spike.queue的队列
        rabbitTemplate.convertAndSend("spike.queue", jsonStr);

        //5.向客户端返回成功结果
        return "正在拼命抢购中,请稍后去订单查看...";
    }
}

2,创建消费者boot项目

1,引入依赖

 <!--rabbitmq的依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

        <!--redis的依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!--mybatis的依赖-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.4</version>
        </dependency>

        <!--mysql驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!--lombok的依赖-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!--手动引入fastjson的依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.78</version>
        </dependency>

2,主程序运行类

//指定Mapper接口所在的包,自动扫描Mapper接口
@MapperScan(basePackages = "com.mmy.dao")
@SpringBootApplication
public class SpikeServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpikeServiceApplication.class, args);
    }
}

3,配置文件

#应用名称
spring.application.name=spike-service
#服务器端口
server.port=8081

#-------------------mybatis的配置-----------------------------------------------

#配置数据源
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/db_rabbitmq?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=1234

#指定sql映射文件的位置
mybatis.mapper-locations=classpath:mapper/*.xml
#输出日志
mybatis.configuration.log-impl=org.apache.ibatis.logging.slf4j.Slf4jImpl
#开启驼峰命名映射规则
mybatis.configuration.map-underscore-to-camel-case=true
#--------------RabbitMQ的配置----------------

#RabbitMQ的服务器ip
spring.rabbitmq.host=192.168.9.128
#RabbitMQ的服务器端口
spring.rabbitmq.port=5672
#用户名
spring.rabbitmq.username=mmy
#密码
spring.rabbitmq.password=123456
#虚拟主机
spring.rabbitmq.virtual-host=/myhost
#开启手动签收模式
spring.rabbitmq.listener.simple.acknowledge-mode=manual

#------------redis的配置---------------------

#redis服务器的ip(安装redis的linux的ip)
spring.redis.host=192.168.9.128
#redis的端口
spring.redis.port=6379
#如果redis设置了密码则指定密码
spring.redis.password=mmy123
#操作redis的数据库的下标
spring.redis.database=1

4,实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Goods {
    private Integer goodsid;
    private String goodsname;
    private Double price;
    private Integer status;
    private Integer stock;
    private Date createtime;
    private Date updatetime;
    private Integer spike;
}

5,mapper接口类

public interface GoodsMapper {
    //查询所有参与秒杀的商品
    public List<Goods> getSpikeGoods();
}

6,mapper映射文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mmy.dao.GoodsMapper">
    <!--public List<Goods> getSpikeGoods()-->
    <select id="getSpikeGoods" resultType="com.mmy.domain.Goods">
        select * from goods where spike = 1
    </select>
</mapper>

7,配置类

//实现CommandLineRunner接口并重写run(),run()方法会在boot应用启动时执行;
@Component
public class MysqlToRedis implements CommandLineRunner {
    //注入redis模板
    @Autowired
    private StringRedisTemplate redisTemplate;
    //注入GoodsMapper
    @Autowired
    private GoodsMapper goodsMapper;
    //boot应用启动将mysql数据同步到redis
    @Override
    public void run(String... args) throws Exception {
        //查询所有参数秒杀的商品
        List<Goods> spikeGoods = goodsMapper.getSpikeGoods();

        //将每个参与秒杀的商品,以goods_stock:商品id为键,库存量为值,存储到redis
        if(!CollectionUtils.isEmpty(spikeGoods)){
            for (Goods goods : spikeGoods) {
                Integer id = goods.getGoodsid();
                Integer stock = goods.getStock();
                redisTemplate.opsForValue().set("goods_stock:"+id, stock.toString());
            }
        }
    }
}

8,监听队列

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hbb123654

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值