RabbitMQ

第一章 RabbitMQ概述

1.1 消息队列

消息(Message)是指在应用间传送的数据

消息队列(Message Queue)是一种应用见的通信方式。消息发布者将消息发布到MQ中,不用管谁来取。消息使用者只管取数据而不用管谁发布的消息,由消息系统来确保消息的可靠传输。

为何使用消息队列

以常见订单为例,用户点积下单按钮后的业务逻辑包括:扣减库存,生成相应订单,发红包,发短信通知。

在业务初期这些逻辑可能放在一起同步执行,随着业务的发展订单量增长,需要提升系统服务的性能,这时可以将一些不需要立即生效的操作拆分出来异步操作,比如发红包,发短信通知等。这种场景下就可以用MQ,在下单的主流程(比如扣减库存,生成相应订单)完成之后发送一条消息到MQ让主流程快速完结,而由另外的单独线程拉取MQ的消息(或者由MQ推送消息),当发现MQ中有发红包或发短信之类的消息时,执行相应的业务逻辑。(解耦合)

其他常见场景包括最终一致性,广播,错峰流控等。

1.2 RabbitMQ特点

RabbitMQ是Erlang语言开发的AMQP的开源实现。

AMQP:Advanced Message Queue Protocol 高级消息队列协议。它是应用层协议的一个开放标准,为面向消息的中间件设计,基于此协议的客户端与消息中间件可传递消息,并不受产品,开发语言等条件的限制。

RabbitMQ特点:

  1. 可靠性

    持久化,传输确认,发布确认

  2. 灵活路由

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

  3. 消息集群

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

  4. 高可用

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

  5. 多种协议

    支持多种消息队列协议,如STOMP,MQTT等

  6. 多语言客户端

  7. 管理界面

  8. 跟踪机制:如果消息异常,RabbitMQ提供了消息跟踪机制

kafka和RabbitMQ一样都是消息队列,但前者效率高不安全,后者安全但没kafka效率高

1.3 Rabbit MQ安装

  1. 安装Rabbit MQ前需要安装Erlang,可以去官网下载。版本为otp_src_19.3.tar.gz

  2. 之后下载RabbitMQ,版本为3.7.2-1.el7.noarch.rpm

  3. 安装RabbitMQ前必须安装需要的依赖包,可以使用以下命令安装

yum install gcc glibc-devel make ncurses-devel openssl-devel xmlto
  1. 安装erlang:解压缩安装包后,有一个configure,创建安装位置的erlang文件夹后,配置erlang的安装信息 :./configure --prefix=/usr/local/erlang <这是用户自己指定的> --without-javac
    大概是openssl的版本问题,安装后在运行rabbitmq时会报错,直接用后面提到的解决问题的安装方法安装,一步到位

  2. 编译并安装 make&&make install

  3. 配置环境变量

    vim /etc/profile

    ERL_HOME=/usr/local/erlang

    PATH=$ERL_HOME/bin:$PATH

    export ERL_HOME PATH

  4. 启动环境变量配置文件

    source /etc/profile

  5. 安装RabbitMQ

    一种是下载源代码,make&&make install

    另一种是下载rpm安装包 rpm -ivh --nodeps ***********.rpm

第二章 RabbitMQ使用

2.1 Rabbit MQ常用命令

2.1.1 rabbitmq启动与关闭

rabbitmq-server start & 后台启动服务

注意:这里可能会出现错误,错误原因是/var/lib/rabbitmq/.erlang.cookie文件权限 不够

解决方法:对这个文件授权

chown rabbitmq:rabbitmq /var/lib/rabbitmq/.erlang.cookie

chmod 400 /var/lib/rabbitmq/.erlang.cookie

如果root@localhost 不是host中指定的计算机名称,也运行不了

https://blog.csdn.net/veloi/article/details/103165784

在此处运行时报错,缺少依赖,重装openssl时发现显示:
openssl is configured for kerberos but no krb5.h found 
已为kerberos配置OpenSSL,但未找到krb5.h

说明安装openssl出了问题,解决方法如上述链接所述
1.重装openssl
wget http://www.openssl.org/source/openssl-1.0.1s.tar.gz
 
tar -zvxf openssl-1.0.1s.tar.gz
 
cd openssl-1.0.1s
 
./config --prefix=/usr/local/openssl

2.修改Makefile
	vi Makefile
将原来的:CFLAG=     -DOPENSSL_THREADS
修改为: CFLAG= -fPIC -DOPENSSL_THREADS
也就是添加-fPIC
执行 make && make install

3.重装erlang
tar xf otp_src_20.1.tar.gz
cd otp_src_20.1
./configure --prefix=/usr/local/erlang --with-ssl=/usr/local/openssl
make&& make install


rabbitmqctl stop 停止服务

2.1.2rabbitmq插件添加

  1. 添加插件

    rabbitmq-plugins enable {插件名}

  2. 删除插件

    rabbitmq-plugins disable {插件名}

注意:rabbitmq启动后可以使用浏览器进入管控台但是默认情况rabbitmq不允许直接使用浏览器进行访问因此必须添加插件

3. 使用浏览器访问管控台 http://RabbitMQ服务器ip :15672
rabbitmq-plugins enable rabbitmq_management

​ 访问控制台后,需要进行登陆操作,在服务器本机上可以用guest作为用户名和密码登录,非本机则无法登录。此外需要注意虚拟机防火墙是否关闭

2.1.3 用户管理

  1. 添加用户

    rabbitmqctl add_user {username} {password}

  2. 删除用户

    rabbitmqctl delete_user {username}

  3. 修改密码

    rabbitmqctl change_password {username} {newpassword}

  4. 设置用户角色

    rabbitmqctl set_user_tags {username} {tag}

    tag:management,monitoring,policymaker administrator

    management:用户可以通过AMQP做的任何事外加:
    	列出自己可以通过AMQP登入的virtual hosts
    	查看自己的virtual hosts中的queues,exchanges和bindings
    	查看和关闭自己的channels和connections
    	查看有关自己的virtual hosts的“全局”统计信息,包含其他用户在这些virtual host中的活动
    
    policymaker:
    	management可以做的任何事外加:
    	查看,创建和删除自己的virtual hosts所属的policies和parameters
    	
    monitoring:
    	management可以做的任何事外加:
    		列出所有virtual hosts,包括他们不能登录的virtual hosts
    		查看其他用户的connections和channels
    		查看节点级别的数据如clustering和memory使用情况
    		查看真正的关于所有virtual hosts的全局统计信息
    		
    administrator:
    	policymaker和management可以做的任何事外加:
    		创建和删除virtual hosts
    		查看、创建和删除users
    		查看创建和删除permissions
    		关闭其他用户的connections
    
    

2.1.4 权限管理

  1. 授权命令:rabbitmqctl set_permissions [-p vhostpath] {user} {conf} {write} {read}

    -p vhostpath:用于指定一个资源的命名空间,例如 -p/表示根路径命名空间
    user:用于指定要为哪个用户授权填写用户名
    conf:一个正则表达式match哪些配置资源能够被该用户配置
    write:一个正则表达式match哪些配置资源额能够被该用户读
    read:一个正则表达式match哪些配置资源能够被该用户访问
    
    例如:
    rabbitmqctl set_permissions -p / root '.*' '.*' '.*'
    设置root用户拥有对所有资源的读写配置权限
    
  2. 查看用户权限 rabbitmqctl list_permissions [vhostpath]

    例如:查看根路径下的所有用户权限

    rabbitmqctl list_permissions /

    查看指定命名空间下的所有用户权限

    rabbitmqctl list_permissions /abc

  3. 查看指定用户下的权限

    rabbitmqctl list_user_permissions {username}

2.1.5 vhost管理

vhost是RabbitMQ的一个命名空间,可以限制消息的存放位置。利用这个命名空间可以进行权限的控制。有点类似于windows中的文件夹,在不同的文件夹下存放不同的文件,

  1. 添加vhost:rabbitmqctl add vhost {name}
  2. 删除vhost:rabbitmqctl delete vhost {name}

第三章 RabbitMQ消息发送和接收

3.1 RabbitMQ的消息发送和接收机制

MQ产品的模型抽象上来说都是一样的过程:

消费者订阅某个队列,生产者创建消息,发布到队列中,最后将消息发送到监听的消费者。

AMQP协议机制:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QE7fdi7F-1666596207104)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20221019185045225.png)]

  1. broker:消息队列实体(就是启动的那个RabbitMQ,消息服务器)

    一个broker里可以有多个vhost

  2. vhost:一个broker里可以有多个vhost,一个vhost里可以有多个exchange和队列

  3. queue:exchange和queue有一个绑定规则,消息到达交换机后,根据绑定规则(由路由键routing-key决定)存入队列

  4. connection:网络连接,比如一个tcp连接。一个connection中可以有多个双向channel,可读可写.

  5. channel:消费者连接channel,channel进入队列取消息返回给消费者

  6. message:消息由消息头和消息体组成,消息头由一系列的可选属性组成,这些属性包括routing-key(路由键)priority(相对于其他消息的优先权),**delivery-mode(指出该消息可能需要持久性存储)**等

3.2 AMQP中的消息路由

Exchange分发消息时根据类型的不同,分发策略有区别,目前共4种类型:

direct ,fanout,topic,headers。headers交换机和direct交换机完全一致,但性能差很多,目前几乎用不到了。

  1. direct:

    消息中的路由键如果和binding中的bindingkey完全一致,则交换机就将消息发到对应的队列中。路由键与队列名完全匹配,如果一个队列帮i的那个到交换机要求路由键为“dog”,则只会转发routing key标记为“dog”的消息,不会转发“dog.puppy”,也不会转发“dog.guard”。是完全匹配,单播的模式

  2. fanout:

    每个发到fanout类型交换机的消息都会分到所有绑定的队列上去。fanout交换机不处理路由键,只是简单的将队列绑定到交换器上,每个发送到交换机的消息都会被转发到与该交换机绑定的所有队列上。广播模式,不用匹配routing key。fanout类型转发消息是最快的,但是有丢失消息的可能性。

  3. topic:

    topic交换机通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。他将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。它同样也会识别两个通配符:符号“#”和符号“*”。#匹配0个或多个单词,“*”匹配一个单词。例如,usa.news和usa.weather都匹配usa.*,就会分配到一个queue中,usa.weather和europe.weather也会分配到一个queue中。topic也是一对多的模式。topic

    也会丢失消息。需要先启动消费者来监听。

3.3 RabbitMQ 消息发送

  1. 首先准备好依赖

    <dependencies>
    	<dependency>
        	<groupId>com.rabbitmqgroupId>
            <artifactId>amqp-clientartifactId>
            <version>5.1.1version>
        dependency>
    dependencies>
    
  2. 编写消息发送类

    public class Send{
        public static void main(String[] args){
            //创建链接工厂对象
            ConnectionFactory factory = new ConnectionFactory();
            factory.setHost("192.168.213.137");//设置rabbitmq的主机ip
            factory.setPort(5672);//设置rabbitmq的端口号
            factory.setUsername("root");//设置访问用户名
            factory.setPassword("root");//设置访问密码
            Connection connection = null;//定义链接对象
            Channel channel = null;//定义通道对象
            try{
                connection = factory.newConnection();//实例化链接对象
                channel = connection.createChannel();//实例化通道对象
                String message = "hello world ! 3";
    
                //创建队列,名字为myQueue
                /**队列名,
                *	是否持久化
                *	是否排外(只允许一个消费者监听)
                *	是否自动删除,没有消费者监听,也没有消息存储时,删除队列
                *	基本属性设置
                */
                channel.queueDeclare("myQueue",true,false,false,null);
                //发送消息到指定队列
                /**
                *	交换机,为空表示不使用交换机
                *	routing key或队列名,当指定了交换机,则为routing key,否则为队列名
                *	属性,
                *	具体消息数据的字节数组
                */   
                channel.basicPublish("","myQueue",null,message.getBytes("UTF-8"));
                System.out.println("消息发送成功:"+message);
            }
            catch(IOException e){e.printStackTrace();}
            catch(TimeoutException e){e.printStackTrace();}
            finally{
                if (channel != null){
                    try {
                        channel.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    } catch (TimeoutException e) {
                        e.printStackTrace();
                    }
                }
                if (connection != null){
                    try {
                        connection.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
    

    注意事项:

    1. 声明队列时,如果这个队列名称已存在,则会自动放弃声明,如果队列不存在则会声明一个新的队列
    2. 队列名可以取任意值,但是要与消息接收时完全一致
    3. 声明队列的那行代码可有可无,但一定要在发送消息前确认队列名已经存在在rabbitmq中,否则会出现问题。

3.4 RabbitMQ 消息的接收

1.准备依赖

<dependencies>
	<dependency>
		<groupId>com.rabbitmqgroupId>
		<artifactId>amqp-clientartifactId>
		<version>5.1.1version>
	dependency>
dependencies>
  1. 编写消息接受类
public class Receive{
	public static void main(String[] args){
		//创建链接工厂对象
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.213.137");
        factory.setPort(5672);
        factory.setUsername("root");
        factory.setPassword("root");
        Connection connection = null;
        Channel channel = null;
        try{
            connection = factory.newConnection();
            channel = connection.createChannel();
            String queueName = "myQueue";
            channel.queueDeclare(queueName,true,false,false,null);
            //消费者tag,用来标识是哪一个消费者,总不可能是个消费者就能取我的消息吧
            String consumerTag = "";
            //自动确认,只有自动确认需要消费者tag
            boolean autoAck = true;
            //接受消息
            //参数1 队列名
            //参数2 是否自动确认消息
            //参数3 消息标签,用来区分不同的消费者,这里暂定为”“
            //参数4 消费者回调方法用于编写处理消息的具体代码,例如将收到的消息打印或者写入数据库
            //basicConsume启动了一个线程,异步监听,发一个接受一个,会一直开启的,除非你关了。
            channel.basicConsume(queueName,autoAck,consumerTag,new DefaultConsumer(channel){
            	@Override
				public void handleDelivery(String consumerTag,
											Envelope envelope,
											AMQP.BasicProperties properties,
											byte[] body){
					String bodyStr = new String(body,"utf-8");
					System.out.println(bodyStr);
				}
			});
		}catch(TimeoutException e){e.printStackTrace();
		}catch(IOException e){e.printStackTrace();
		}finally{
		//下面两玩意儿别关,关了就没法儿监听消息了儿,如果关闭了可能会造成接受时抛出异常或无法接受消息
//            channel.close();
//            connection.close();
		}
}

3.5 RabbitMQ交换机

3.5.1 Exchange-消息发送

生产者将消息发送给exchange交换机,并不关心交换机会将消息放入到哪个队列中(exchange会按照特定的策略转发到queue进行存储,实际应用中,只需要声明exchange并定义好exchange的路由策略即可),生产者和消费者解耦合。

3.5.2 direct-消息发送与接收

//direct消息发送
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("xxxx");
factory.setPort("xxxx");
factory.setUsername("xxxx");
factory.setPassword("xxxx");
Connection conn = null;
Channel channel = null;
try{
	conn = factory.newConnection();
	channel = conn.createChannel();
	String message = "Hello world";
	String exchangeName = "myExchange";
	//声明队列
	channel.queueDeclare("myQueue",true,false,false,null);
	//声明exchange
	//参数1 交换机名称
	//参数2 转发方式direct,fanout,topic,headers
	//参数3  是否持久化消息
	//如果声明exchange时,对应名字的exchange已经存在,则会放弃声明
	channel.exchangeDeclare(exchangeName,"direct",true)
	//绑定exchange
	//参数1 队列名
	//参数2 exchange名称
	//参数3 消息routingkey,也就是bindingkey
	//不小心写成了exchangeBind,这个方法是交换机绑定交换机,queueBind是队列绑定交换机
	channel.queueBind("myQueue",exchangeName,"myRoutingKey")
	//发送消息
	channel.basicPublish(exchangeName,"myRoutingKey",null,message.getBytes("utf-8"));
}catch(TimeoutException e){
	e.printStackTrace();
}catch(IOException e){
	e.printStackTrace();
}finally{
	if (channel != null){
		try{
			channel.close();
		}catch(TimeoutException e){
			e.printStackTrace();
		}catch(IOException e){
			e.printStackTrace();
		}
	}
	if (conn != null){
		try{
			conn.close();
		}catch(TimeoutException e){
			e.printStackTrace();
		}catch(IOException e){
			e.printStackTrace();
		}
	}
}
//direct消息接收
//创建链接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.31.136");
        factory.setPort(5672);
        factory.setUsername("root");
        factory.setPassword("root");

        Connection connection = null;
        Channel channel = null;
        try {
            connection = factory.newConnection();
            channel = connection.createChannel();
            String queueName = "myQueue";

            String message = "虎虎虎";
            String exchangeName = "myExchange";
            String consumerTag = "";
			//队列声明,交换机声明,队列绑定交换机
            channel.queueDeclare(queueName, true, false, false, null);
            channel.exchangeDeclare(exchangeName,"direct",true);	//第三个参数,持久化
            channel.queueBind(queueName,exchangeName,"myRoutingKey");
            
            channel.basicConsume(queueName,true,consumerTag,new DefaultConsumer(channel){
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    String bodyStr = new String(body,"utf-8");
                    System.out.println(bodyStr);
                }

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

3.5.3 fanout消息发送与接收

//消息接收,fanout存在消息丢失问题,建议要先开启消费者监听
 //创建链接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.31.136");
        factory.setPort(5672);
        factory.setUsername("root");
        factory.setPassword("root");

        Connection connection = null;
        Channel channel = null;
        try {
            connection = factory.newConnection();
            channel = connection.createChannel();
            //调用无参queueDeclare()会创建一个名字随机的队列,该队列非持久(当无消费者监听,会删除队列,造成信息的丢失)且排外(只允许一个消费者监听)
            //getQueue()获取随机队列名,这里创建了两个队列,绑定到了一个exchange上
            for(int i=0;i<2;i++) {
                String queueName = channel.queueDeclare().getQueue();
                String exchangeName = "fanoutExchange";
                String consumerTag = "";

                channel.exchangeDeclare(exchangeName, "fanout", true);
                //fanout不需要routingkey,因为他是广播机制
                channel.queueBind(queueName, exchangeName, "");
                channel.basicConsume(queueName, true, consumerTag, new DefaultConsumer(channel) {
                    @Override
                    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                        String bodyStr = new String(body, "utf-8");
                        System.out.println(bodyStr);
                    }
                });
            }
        }catch (TimeoutException e){
            e.printStackTrace();
        }catch (IOException e){
            e.printStackTrace();
        }
//消息发送
//创建链接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.31.136");
        factory.setPort(5672);
        factory.setUsername("root");
        factory.setPassword("root");

        Connection connection = null;
        Channel channel = null;
        try {
            connection = factory.newConnection();
            channel = connection.createChannel();
            String message = "虎虎虎";
            String exchangeName = "fanoutExchange";
            //由于使用fanout类型的交换机,因此消息的接收方可能存在多个,不建议在消息发送时创建队列
            //以及绑定交换机,建议在消费者中进行,但发送时至少要保证交换机存在
            channel.exchangeDeclare(exchangeName,"fanout",true);
            channel.basicPublish(exchangeName, "",null,message.getBytes("UTF-8"));
        }catch (TimeoutException e){
            e.printStackTrace();
        }catch (IOException e){
            e.printStackTrace();
        }finally {
            if(channel != null){
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            }
            if (connection != null){
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

3.5.4 topic消息发送与接收

//建议先写接收
//创建链接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.31.136");
factory.setPort(5672);
factory.setUsername("root");
factory.setPassword("root");

Connection connection = null;
Channel channel = null;
try {
    connection = factory.newConnection();
    channel = connection.createChannel();
    for(int i=0;i<2;i++) {
        String queueName = channel.queueDeclare().getQueue();
        String exchangeName = "topicExchange";
        String consumerTag = "";

        channel.exchangeDeclare(exchangeName, "topic", true);
        //主要注意绑定队列的routingkey的写法,现假设有三个队列
        channel.queueBind(queueName, exchangeName, "aa");
        channel.queueBind(queueName, exchangeName, "aa.*");
        channel.queueBind(queueName, exchangeName, "aa.#");
        
        channel.basicConsume(queueName, true, consumerTag, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String bodyStr = new String(body, "utf-8");
                System.out.println(bodyStr);
            }
        });
    }
}catch (TimeoutException e){
    e.printStackTrace();
}catch (IOException e){
    e.printStackTrace();
}

//发送消息,try部分的代码
connection = factory.newConnection();
channel = connection.createChannel();
String message = "虎虎虎";
String exchangeName = "topicExchange";
channel.exchangeDeclare(exchangeName,"topic",true);
// #匹配0个或多个单词,*匹配一个单词
//aa.bb满足aa.#,所以aa.#绑定的队列能收到,aa.*也能收到,但aa收不到
//aa.bb.cc 满足aa.#,aa.#能收到,其他收不到
//aa 满足aa.#,aa,其他收不到
channel.basicPublish(exchangeName, "aa.bb",null,message.getBytes("UTF-8"));

topic和fanout的比较:topic和fanout都是一个消息发送给多个队列,

fanout适合广播,比如说消息推送,你手机上安装了一个app,app监听,服务器发送了一条广告,然后广播给所有安装了这个app的用户推送同一条消息。

topic则适合不同的功能模块来分别处理消息,比如下订单有可能成功或者失败,发送消息,order.success说明下订单成功,那么这条消息就应该给成功下单的后续处理功能模块处理,反之,则没成功,那么这条消息就应该发送给下单失败的后续处理功能模块。

3.6 事务消息

事务消息和数据库中的事务类似,MQ中的消息要保证全部发送成功,防止消息丢失。(意思就是队列中的属于一个事务的消息要么一起被取走,要么一个也别走)
RabbitMQ有两种方式解决这个问题:

  1. 通过AMQP提供的事务机制实现
  2. 使用发送者确认模式实现

3.6.1 事务使用

事务的实现主要是对信道的设置,主要方法有三个:

  1. channel.txSelect() 声明启动事务模式
  2. channel.txCommit() 提交事务
  3. channel.txRollback() 回滚事务
//消息发送-生产者
//如下代码,因为除0错误,直接走异常处理程序,事务回滚,上面两条消息都没有进入队列。
//使用了事务机制后,只要不调用txCommit(),就不会提交消息
try{
	channel.txSelect();
	channel.basicPublish(exchangeName, "txRoutingKey",null,message1.getBytes("UTF-8"));
	System.out.println(3 / 0);
	channel.basicPublish(exchangeName, "txRoutingKey",null,message2.getBytes("UTF-8"));
	channel.txCommit();
}catch (ArithmeticException e){
	e.printStackTrace();
}}finally {
    if(channel != null){
        try {
        //放弃当前事务中所有没有提交的消息,释放内存。如果程序正常跑到这,消息肯定已经提交了,但要是异常了,走异常处理流程,消息就肯定没提交
            channel.txRollback();
            channel.close();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
    }

对于消费者,当开启事务时,即使不作为事务的提交,那么依然可以获取队列中的消息并且将消息从队列中移除掉。
暂时的,事务队列对接收者没有影响,之后学到了有影响的再说吧

3.7 消息确认模式

3.7.1 发送者的确认模式Confirm

Confirm和事务一样,都是为了确保消息发送成功。但事务若是除了问题,事务会拒绝提交;确认模式出了问题,会补发消息,直到发送成功。
Confirm的三种实现方式

  1. channel.waitForConfirms()普通发送方确认模式
    该方法发送消息后会等待服务端返回发送成功的消息,如果一直没返回,说明没发送成功。
//开启发送者确认模式
channel.confirmSelect();
channel.basicPublish("","myQueue"null,message.getBytes("utf-8"));
//该方法会返回布尔值,用于确认消息是否发送成功
//可以为这个方法指定一个毫秒当作最大确认时间
//如果超过指定时间会抛出异常InterruptedException,表示需要补发消息
//或将消息缓存到redis中稍后利用定时任务补发
//也有可能消息写入了,但是服务器没发送确认消息,这时也会返回false。
//所谓补发,可以使用递归或利用redis+定时任务来完成补发
channel.waitForConfirms();
  1. channel.waitForConfirmsOrDie();批量消息确认
    会同时向服务中确认之前当前通道中发送的所有的消息是否已经全部成功写入
    ,这个方法没有任何返回值,只要批量发送的消息有一条没成功,就等着全部补发吧你
//开启发送者确认模式
channel.confirmSelect();
channel.basicPublish("","myQueue"null,message.getBytes("utf-8"));
//可以为这个方法指定一个毫秒当作最大确认时间
//无返回值
channel.waitForConfirmsOrDie();
  1. channel.addConfirmsListerner()异步监听发送方确认模式
channel.confirmSelect();
//生成一个监听器
channel.addConfirmsListener(new ConfirmsListener(){
	//消息确认后的回调方法
	//参数1 为被确认的消息的编号,从1开始自动递增用于标记当前是第几个消息
	//参数2 为当前消息是否同时确认了多个,意思就是比如100号消息返回的布尔值为true,就表示从100开始往前的所有消息都确认过了,如果是false则表示只确认了当前编号的消息
	public void handleAck(long deliveryTag,boolean multiple) throws IOException{
		System.out.println("未确认消息,标识:"+deliveryTag+"-----"+multiple);
	}
	//消息没有确认的回调方法
	//如果这个方法被执行,表示当前消息没有被确认,需要消息补发
	//参数1 没有被确认的消息的编号 从1开始自动递增用于标记当前是第几个消息
	//参数2 如果参数2为true,表示小于等于当前编号的消息可能没有发送成功,需要按进行补发;如果为false,则表明当前的消息没有发送成功,需要进行补发。
	public void handleNack(long deliveryTag,boolean multiple) throws IOException{
		System.out.println("已确认消息,标识:"+deliveryTag+"-----多个消息"+multiple);
	}
});
channel.basicPublish("","myQueue"null,message.getBytes("utf-8"));

3.7.2 消费者确认模式

  1. 手动确认
channel.confirmSelect();
//假设启动了事务
channel.txChannel();
//参数2 自动确认消息,如果为true,不管消息是否被处理,只要你取走了,我就删除消息。
//假如我取走消息但还没处理,消息丢失了,那么就再也取不到了;
//如果为false,则不管消息是否被处理,我一直保留消息。
//这样就算你处理好了消息,下次取消息,还是上一批同样的消息。
channel.basicConsumer("myQueue",true,"",new DefaultConsumer(channel){
	public void handleDelivery(String consumerTag,Envelope envelope,AMQP.BasicProperties properties,bytes[] body){
		String message = new String(body);
		//获取消息编号,根据消息编号来确认消息
		long tag = envelope.getDeliveryTag();
		//获取当前消息是否被接收过,如果返回值为false表示该消息之前没被接受过,如果返回值为true
		//则表示这个消息之前被接受过,可能处理完成了。因此需要进行消息的防重复处理。
		if(!envelope.isRedelivery()){
		//获取当前内部类中的通道
		Channel c = this.getChannel();
		//手动确认,确认后表示当前消息已经成功处理了,需要从队列中移除掉
		//这个方法肯定是在消息成功处理后再执行咯
		//参数1 消息序号
		//参数2 是否确认多个。如果为true,则需要确认之前的所有消息,如果为false,
		//则只确认当前一条。
		c.basicAck(tag,true)
		//如果没启动事务,下面代码就当作没有;如果启动了事务,而消息确认模式为手动确认,
		//则必须要提交事务,否则即使调用了确认方法,消息也不会从队列中删除。
		c.txCommit();
		}else{
			//程序能走到这,说明消息已被接受过,需要进行防重复处理
			//例如查询数据库中是否已经添加了记录或者已经修改了记录
			//经过判断如果该消息没被处理,则处理后确认消息
			//否则直接确认掉消息即可
		}		
	}
});

除了c.basicAck()确认方法外,还有:
basicRecover():路由不成功的消息,可以使用recovery重新发送到队列中
basicReject():接收端告诉服务器,拒绝接收,可以设置是放回队列还是扔掉,一次只能拒绝一个消息,但同一个消息只能拒绝一次。
basicNack():一次拒绝多个消息,批量拒绝

第四章 Springboot集成Rabbit MQ

4.1 direct- 发送消息

  1. 新建一个springboot工程,在添加依赖时,选择messaging中的spring for rabbitmq
    RabbitMQ_第1张图片
  2. 在application.properties配置文件中,写入host port之类的信息
    RabbitMQ_第2张图片
  3. 业务方法接口与实现
//接口定义
public interface SendService{
	void sendMessage(String message);
}
//接口实现
@Service("sendService")
public class SendServiceImpl implements SendService {
	//注入Amqp模板
	@Resources
	private AmqpTemplate amqpTemple;

	@Override
	public void sendMessage(String message){
		//参数1 交换机
		//参数2 队列,如果有交换机就是routingkey
		//参数3 消息
		 amqpTemple.convertAndSend("directExchange","routingkey",message);
	}
}
  1. 声明交换机与队列并绑定
    由于springboot中不用写配置文件,所以使用@Configuration来代替
@Configuration
public class RabbitMQConfig{
	//配置交换机	
	@Bean
	public DirectExchange directExchange(){
		return new DirectExchange("directExchange");//directExchange是实现了Exchange的,Exchange还有其他实现方法,具体看源码去
	}
	//配置队列
	//参数与之前的队列声明意思一样
	@Bean
	public Queue queue(){
		return new Queue("myQueue",true,false,false,null);
	}
	//绑定队列与交换机
	@Bean
	public Binding directBinding(Queue queue,DirectExchange exchange){
		//参数1 需要绑定的队列
        //参数2 需要绑定的交换机
        //参数3 routingkey
		return BindingBuilder.bind(queue).to(exchange).with("routingkey");
	}
}

5.springboot,启动!

@SpringBootApplication
public class Application {
	public static void main(String[] args) {
		ApplicationContext ac = SpringApplication.run(Application.class, args);
		SendService service  = (SendService)ac.getBean("sendService");
		service.sendMessage("springboot 测试数据");
	}
}

6.其他发送方式,大同小异。

4.2direct-异步监听接收消息

  1. 首先和4.1一样先创建消息接收的springboot工程
  2. 写接收业务代码
//接口就不写了,直接写实现
@Bean
public void receiveMessage(){
	System.out.println(amqpTemplate.receiveAndConvert("myQueue"))
}
//springboot主方法
public class Application{
	public static void main(String[] args) {
		ApplicationContext ac = springApplication.run(Application.class,args);
		SendService service = ac.getBean("receiveService");
		service.receiveMessage("sakana");
	}
}

//上述方法一个缺点就是,每次接收信息,都需要重新启动一次程序,不能一直监听。所以需要启动一个监听器
//通过监听器持续监听队列
@RabbitListener(queue= "myQueue")
public void receiveMessage(String message){
	System.out.println(message);
}
//springboot主方法
ApplicationContext ac = SpringApplication.run(Application.class,args);
//此时启动了监听器,就不需要调用读取消息的方法了,他会自动把消息赋值给形参
ac.getBean("receiveService");

注意:springboot中的消息确认机制是只要程序正常结束,spring就会帮你手动确认;如果出现异常则不会确认;
	在消息处理时,需要做好消息的防重复工作。

4.3 fanout消息发送与接收

1.不多说,先搞一个springboot工程再说
2.fanout比较常用的场景是随机生成队列,无监听就会销毁,那么最好先生成消费者进行监听

//由于队列是随机生成的,用配置文件就不太方便了,因为都写死了,于是用注解来解决这个问题
@RabbitListener(bindings = {@QueueBinding(//看英文就知道咯,这注解的功能就是将队列绑定到交换机上,那参数肯定要有队列和交换机
					value=@Queue(),//@Queue不给参数就是随机生成队列
					exchange = @Exchange(name="fanoutExchange",type="fanout")//生成一个指定名字和类型的交换机,很难理解吗
					)})
//关于上述的注解内容,为啥有value = 注解这种赋值等式?如果点进入@QueueBinding就会知道,value的类型就是Queue,在点进Queue就会发现,Queue就是个注解,所以我给一个类型为@Queue的参数value赋值一个注解@Queue的值不是合情合理么
public void receiveMessageFanout01(String message){
	System,out.println(message);
}

//需要几个队列就重复上述方法几次,不过这是不是也挺蠢的?
@RabbitListener(bindings = {@QueueBinding(value=@Queue(),exchange = @Exchange(name="fanoutExchange",type="fanout"))})
public void receiveMessageFanout02(String message){
	System,out.println(message);
}
  1. fanout发送者
//跟direct的没啥区别,注意一点,发送前要保证exchange已经生成了,发送端的交换机声明要在配置文件里写
public void sendMessageFanout(String message){
	return amqpTemplate.convertAndSend(message);
}
//配置交换机
@Bean
public Exchange fanoutExchange(){
	return new FanoutExchange("fanoutExchange");
}

4.4 topic消息发送与接收

  1. 懒得说了
  2. 和fanout类似,但要指定queue和key罢了,还是先写接收
//注意注解中routingkey的不同
@RabbitListener(bindings = {@QueueBinding(value=@Queue("topic01"),key={"aa"},exchange=@Exchange(name="topicExchange",type="topic"))})
public void receiveMessageTopic01(String message){
	System.out.println(message);
}
@RabbitListener(bindings = {@QueueBinding(value=@Queue("topic02"),key={"aa.*"},exchange=@Exchange(name="topicExchange",type="topic"))})
public void receiveMessageTopic02(String message){
	System.out.println(message);
}
@RabbitListener(bindings = {@QueueBinding(value=@Queue("topic03"),key={"aa.#"},exchange=@Exchange(name="topicExchange",type="topic"))})
public void receiveMessageTopic03(String message){
	System.out.println(message);
}

3.发送端

public void sendMessageTopic(String message){
	return amqpTemplate.convertAndSend("topicExchange","aa.bb.cc",message);//谁能接收到?
}

第五章 Rabbit MQ集群

  1. 普通模式(默认):对于A/B两个节点的Rabbitmq来说,如果生产者向A中写数据,消费者从B中取数据,那么A中的数据会被转移到B中,再由B交给消费者,AB间会进行临时的消息传输.如果A故障了,那么B自然也就取不到数据了.这种方式适合非持久化队列,因为当队列为非持久化队列时,客户端才能重新连接到集群中的其他节点,并重新创建队列,如果该节点是持久化的,那么唯一的办法就是将故障节点回复起来.
  2. 镜像模式(高可用):把需要的队列做成镜像模式,存在于多个节点,数据Rabbitmq的HA(高可用)方案.消息实体会主动在镜像节点间同步.但吃资源,如果镜像队列过多,加之有大量消息进入,集群内部网络带宽会被大量消耗,适用于对可靠性要求较高的场合中适用.

5.1 镜像模式集群实现

1.准备两台linux虚拟机,改hostname为A和B,方便识别

vim /etc/hosts
#在A号机内
127.0.0.1 A xxxxxxxxxxx
xxxx      A xxxxxxxxxxx
ip1 A	#这是A号机的ip
ip2 B	#这是B号机的ip
#在B号机内
127.0.0.1 B xxxxxxxxxxx
xxxx      B xxxxxxxxxxx
ip1 A	#这是A号机的ip
ip2 B	#这是B号机的ip
#然后重启,像这样能ping通就说明改成功了
[root@A ~]ping B
[root@B ~]ping A
  1. 在两台机器里分别安装rabbitmq,安装方法和以前一样
  2. 配置cookie文件
    Erlang cookie 是保证不同节点可以互相通信的密钥,要保证集群中的不同节点互相通信必须共享相同的erlang cookie ,具体存放在/var/lib/rabbitmq/.erlang.cookie
    例如:
cat /var/lib/rabbitmq/.erlang.cookie

必须保证两台linux的cookie文件完全一样,可以使用vim进行编辑,也可以使用scp命令完成文件跨机器拷贝,
例如:

#改之前先修改文件的权限为777,改完后再改回400
#将当前服务器中文件拷贝到ip的指定目录下
scp /var/lib/rabbitmq/.erlang.cookie 192.168.31.55:var/lib/rabbitmq
  1. 组建集群
    分别启动2台linux机器中的rabbitmq服务器
rabbitmqctl stop
rabbitmq-server -detached	#表示在后台运行
# 将某个rabbitmq加入到某个服务器节点
rabbitmqctl stop_app	#停止应用
rabbitmqctl join_cluster rabbit@A	#加入节点,这里的A为某个机器的hostname,这些命令在B中运行,只要执行一次就行了
rabbitmqctl start_app	#启动应用

rabbitmqctl cluster_status	#查看集群状态

5.2 springboot连接rabbitmq集群

  1. 普通模式
#设置application.properties内容
#配置rabbitmq的相关信息(集群)
spring.rabbitmq.addresses=192.168.31.136:15672,192.168.31.55:15672
spring.rabbitmq.username=root
spring.rabbitmq.password=root

启动rabbitmq,发送消息,会发现消息随机发送到一个节点上了,例如发送到了A节点
此时如果关闭A节点,然后试图从B节点获取消息,会发现报错,因为现在只是实现了普通模式的集群
  1. 镜像模式
    RabbitMQ_第3张图片
    点击admin中的右侧policy,点击添加policy
    Name是规则名,自定义
    pattern是匹配模式,^表示匹配所有, ^aa表示匹配aa
    Apply to:exchange and queue表示对所有的交换机和队列进行操作
    definition:前面表示模式.ha-mode是高可用镜像模式,all表示对所有的交换机和队列进行镜像

你可能感兴趣的:(Java学习笔记,java-rabbitmq,rabbitmq,java)