一行命令搞定:
docker pull rabbitmq:management
开启宿主机与容器的两个重要的端口映射即可:
docker run -id -p 5672:5672 15672:15672 rabbitmq:management
RabbitMQ在安装好后,可以访问宿主机IP地址:15672
使用其默认用户名和密码均为guest进行后台管理界面登录。
如果本地没有编译相关环境也需要通过包管理器安装:
yum install build-essential openssl openssl-devel unixODBC unixODBC-devel make gcc gcc-c++ kernel-devel m4 ncurses-devel tk tc xz
由于该中间件RabbitMQ是由Erlang语言编写的,因此需要安装对应环境:
#下载安装包后安装 或者 可以通过Shell前端软件包管理器安装(例如yum和apt)
rpm -ivh erlang-18.3-1.el7.centos.x86_64.rpm
如果安装Erlang时报错,请自行百度解决相关依赖问题,例如glibc
版本过低等问题。
Socat 是 Linux 下的一个多功能的网络工具,名字来由是 「Socket CAT」。其功能与有瑞士军刀之称的 Netcat 类似,可以看做是 Netcat 的加强版。RabbitMQ依赖该工具,需要先安装:
rpm -ivh socat-1.7.3.2-1.1.el7.x86_64.rpm --force --nodeps
安装Rabbit包:
rpm -ivh rabbitmq-server-3.6.5-1.noarch.rpm
设置默认配置文件:
cd /usr/share/doc/rabbitmq-server-3.6.5/
cp rabbitmq.config.example /etc/rabbitmq/rabbitmq.config
开启管理界面并且使用默认提供的账号和密码:
# 开启管理界⾯
rabbitmq-plugins enable rabbitmq_management
# 修改默认配置信息
vim /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.5/ebin/rabbit.app
# ⽐如修改密码、配置等等,例如:
#启用guest账号(未启动前有尖括号):loopback_users: guest
重启这个RabbitMQ服务:
service rabbitmq-server start # 启动服务
service rabbitmq-server stop # 停⽌服务
service rabbitmq-server restart # 重启服务
用户管理:不同用户对应可以操作的权限不同。最高:(administrator)
虚拟主机:类似于MySQL中的不同数据库,是隔离开的不同空间。Virtual Hosts。
类似于计算机系统里的告诉缓存Cache的引入,是为了解决CPU和IO操作的效率不对等的问题,引入Cache后,能够提高整体的操作效率。
假设A系统负责向B系统发送消息,如果A发送的速度远远高于B接收的速度,这会导致A系统的效率严重降低,A系统响应会变慢,而且无法处理其他问题。
这时候可以引入中间件C,他有两个特点,一方面他的速度可以匹配A系统,另一方面,他可以存储多条消息,并且以B系统可以接受的速度向B转发消息。这就解决了A系统面临的上述问题。
引入C中间件后,尽管A系统在某个时间段内会有一个尖峰的消息发送速度,但经过C处理后,在B这边以B能接受的最大速度成一个平缓的速度趋势消费消息。
由于中间件有一定的标准,使得应用扩展变得更容易,新增的应用只需要对接中间件即可完成需求。
使用MQ使得应用间解耦,提升容错性和可维护性。为引入中间件之前一个系统失败将导致生产者失败,进而导致其他消费者也失败,引入之后,只需等待或者修复出问题的消费者即可恢复整个系统,且其他系统并不会受影响。
系统可用性降低
系统复杂度提高
channel.txSelect()
),然后发送消息,如果发送过程中出现什么异常,事务就会回滚(channel.txRollback()
),如果发送成功则提交事务(channel.txCommit()
)。一致性问题
RabbitMQ、RocketMQ、ActiveMQ、Kafka、ZeroMQ、MetaMq等,也有直接使用 Redis 充当消息队列的案例,
说几个特殊点:Kafka主要是为大数据准备。Rabbit是Erlang语言(Erlang 语言专门为开发高并发和分布式系统的一种语言,在电信领域使用广泛)编写的产品,因此速度遥遥领先,为微妙级别(其他的为毫秒)。
其他对比见下表:
完整结构图如下:
Connection使用的是TCP连接,其中的Channel是基于此做的逻辑连接且相互隔离,本质上是共用同一个TCP连接的,以此节省TCP资源和开销。
Exchange是交换机,这个模块并不是MQ必须有的,在RabbitMQ里该模块负责决定哪些消息分配给哪些队列,因此,不同的分配策略导致有了6种不同的模式:简单模式,work模式,Publish/Subscribe发布与订阅模式,Routing路由模式,Topics主题模式,RPC远程调用模式(不常用)
模式总结:
Exchange交换机常用类型有:
该模式下,一个生产者对应一个消费者,消息队列就类似一个邮箱,用于缓存消息:
生产者只需要指定队列名称即可:
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
消费者也一样,只需指定队列名称,并且在回调consumer里接收消息:
channel.basicConsume(Producer.QUEUE_NAME, true, consumer);
Work Queues 与入门程序的 简单模式 相比,多了一些消费者,多个消费端共同消费同一个队列中的消息,属于竞争关系。应用场景:对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。
代码同简单模式,只不过,消费者从一个变成了多个。为了测试便于观察,可以将消费者设置为每次只消费一个消息:
//一次只能接收并处理一个消息
channel.basicQos(1);
该模式下,引入了exchange的角色,且队列也增加到多个,生产者不再直接发送给队列,而是由exchange来决定转发到哪个队列中。Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!
每个消费者监听自己的队列。 生产者将消息发给broker,由交换机将消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将接收到消息。
生产者代码里多了exchange的声明:
channel.exchangeDeclare(FANOUT_EXCHAGE, BuiltinExchangeType.FANOUT);
channel.queueDeclare(FANOUT_QUEUE_1, true, false, false, null);
channel.queueDeclare(FANOUT_QUEUE_2, true, false, false, null);
//队列绑定FANOUT交换机 无需指定routing key 指定了也无效
channel.queueBind(FANOUT_QUEUE_1, FANOUT_EXCHAGE, "");
channel.queueBind(FANOUT_QUEUE_2, FANOUT_EXCHAGE, "");
//指定了FANOUT交换机 无需指定队列名称了 队列已经和交换机绑定了
channel.basicPublish(FANOUT_EXCHAGE, "", null, message.getBytes());
消费者代码里也多了exchange与队列的绑定操作,但是还没有routing key:
//声明交换机
channel.exchangeDeclare(FANOUT_EXCHAGE,BuiltinExchangeType.FANOUT);
channel.queueDeclare(FANOUT_QUEUE_1, true, false, false, null);
//队列绑定交换机
channel.queueBind(FANOUT_QUEUE_1, FANOUT_EXCHAGE, "");
channel.basicConsume(FANOUT_QUEUE_1, true, consumer);
交换机需要与队列进行绑定,绑定之后;一个消息可以被多个消费者都收到。不存在竞争关系。
该模式下,除了引入Exchange外,还增加了队列与交换机的绑定的时候指定一个RoutingKey (路由key),也就是说,即使你将队列绑定在了交换机上,也不一定会收到消息,还需要匹配路由。
生产者代码中在队列绑定和发送消息时都多了指定route Key的部分:
channel.exchangeDeclare(DIRECT_EXCHAGE, BuiltinExchangeType.DIRECT);
channel.queueDeclare(DIRECT_QUEUE_INSERT, true, false, false, null);
channel.queueDeclare(DIRECT_QUEUE_UPDATE, true, false, false, null);
//队列绑定交换机
channel.queueBind(DIRECT_QUEUE_INSERT, DIRECT_EXCHAGE, "insert");
channel.queueBind(DIRECT_QUEUE_UPDATE, DIRECT_EXCHAGE, "update");
channel.basicPublish(DIRECT_EXCHAGE, "insert", null,
message.getBytes());
channel.basicPublish(DIRECT_EXCHAGE, "update", null,
message.getBytes());
消费者代码中同样在绑定队列时指定了route Key:
channel.exchangeDeclare(DIRECT_EXCHAGE,BuiltinExchangeType.DIRECT);
channel.queueDeclare(DIRECT_QUEUE_INSERT, true, false, false,null);
//队列绑定交换机
channel.queueBind(DIRECT_QUEUE_INSERT,DIRECT_EXCHAGE,"insert");
channel.basicConsume(DIRECT_QUEUE_INSERT, true, consumer);
该模式下,除了引入了Exchange(类型由direct变成了topic),另外还引入了通配符(#代表多个任意词,*代码一个任意词),用于更加灵活的绑定队列:
生产者代码和路由模式差不多,只不过exchange类型不同,发送消息时指定route Key:
channel.exchangeDeclare(TOPIC_EXCHAGE, BuiltinExchangeType.TOPIC);
//绑定队列放在消费者 只需要有一方绑定即可
channel.basicPublish(TOPIC_EXCHAGE, "item.insert", null,
message.getBytes());
channel.basicPublish(TOPIC_EXCHAGE, "item.update", null,
message.getBytes());
消费者代码中除了exchange类型改为topic外,在绑定接收队列时也可以使用通配符了:
channel.queueDeclare(TOPIC_QUEUE_1, true, false, false, null);
channel.queueDeclare(TOPIC_QUEUE_2, true, false, false, null);
//队列绑定交换机 可以使用通配符了
channel.queueBind(TOPIC_QUEUE_2, Producer.TOPIC_EXCHAGE,"item.*");
channel.basicConsume(TOPIC_QUEUE_2, true, consumer);
引入依赖:
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>5.1.7.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.amqpgroupId>
<artifactId>spring-rabbitartifactId>
<version>2.1.8.RELEASEversion>
dependency>
使用配置文件声明网络连接的重要参数(和JDBC参数类似),最后被spring配置文件读取和引用:
rabbitmq.host=192.168.220.12
rabbitmq.port=5672
rabbitmq.username=xiangwei
rabbitmq.password=xiangwei
rabbitmq.virtual-host=/xiangwei
生产者在spring配置文件里配置交换机、队列、以及通配符配置等:
、
测试代码里如果需要加载配置文件,需要使用@ContextConfiguration注解:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring/spring-rabbitmq.xml")
public class ProducerTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void topicTest(){
/**
* 参数1:交换机名称
* 参数2:路由键名
* 参数3:发送的消息内容
*/
rabbitTemplate.convertAndSend("exchange", "key", "消息");
}
}
消费者spring配置,定义队列后,设置监听器绑定这些队列即可:
<bean id="springQueueListener"
class="cn.forwardxiang.rabbitmq.listener.SpringQueueListener"/>
<bean id="fanoutListener1"
class="cn.forwardxiang.rabbitmq.listener.FanoutListener1"/>
<rabbit:listener-container connection-factory="connectionFactory" autodeclare="true">
<rabbit:listener ref="springQueueListener" queue-names="spring_queue"/>
<rabbit:listener ref="fanoutListener1" queuenames="spring_fanout_queue_1"/>
rabbit:listener-container>
代码中实现监听器:
public class TopicListenerStar implements MessageListener {
public void onMessage(Message message) {
try {
String msg = new String(message.getBody(), "utf-8");
System.out.printf("通配符*监听器:接收路由名称为:%s,路由键为:%s,队列名为:%s的消息:%s \n", message.getMessageProperties().getReceivedExchange(), message.getMessageProperties().getReceivedRoutingKey(), message.getMessageProperties().getConsumerQueue(),
msg);
} catch (Exception e) {
e.printStackTrace();
}
}
}
引入依赖,加入了启动器,无需引入context依赖了:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
<dependency>
application.yml配置文件:
spring:
rabbitmq:
host: 192.168.220.12
port: 5672
virtual-host: /xiangwei
username: xiangwei
password: xiangwei
使用全注解的方式用来配置队列和交换机:
@Configuration
public class RabbitMQConfig {
//交换机名称
public static final String ITEM_TOPIC_EXCHANGE =
"springboot_item_topic_exchange";
//队列名称
public static final String ITEM_QUEUE = "springboot_item_queue";
//声明交换机
@Bean("itemTopicExchange")
public Exchange topicExchange(){
return ExchangeBuilder.topicExchange(ITEM_TOPIC_EXCHANGE).durable(true).build();
}
//声明队列
@Bean("itemQueue")
public Queue itemQueue(){
return QueueBuilder.durable(ITEM_QUEUE).build();
}
//绑定队列和交换机
@Bean
public Binding itemQueueExchange(@Qualifier("itemQueue") Queue queue,@Qualifier("itemTopicExchange") Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("item.#").noargs();
}
}
消费者一段使用全注解配置监听器:
@Component
public class MyListener {
/**
* 监听某个队列的消息
* @param message 接收到的消息
*/
@RabbitListener(queues = "springboot_item_queue")
public void myListener1(String message){
System.out.println("消费者接收到的消息为:" + message);
}
}
ack指Acknowledge,确认。 表示消费端收到消息后的确认方式
有三种确认方式:
自动确认,会在消费者收到消息后自动回复ack,即使后续的业务处理异常,该条消息也会丢失。而手动确认,则需要手动回复ack,这样MQ才会投递下一条消息,程序员可以灵活的决定是否回复ack。
这个功能应用于削峰填谷很有用,需要开启ack手动确认,并在消费者监听器里配置perfetch
属性:
<rabbit:listener-container connection-factory="connectionFactory" concurrency="2" prefetch="3">
<rabbit:listener ref="listener" queue-names="remoting.queue" />
rabbit:listener-container>
或者使用Java代码:
channel.basicQos(10);//每次消费10条,相当于设置最大承受速度
TTL(Time To Live,消息过期时间设置),可以针对某个队列设置所有消息的过期时间,也可以针对每个消息设置,RabbitMQ只对处于队头的消息判断是否过期(即不会扫描队列),所以,很可能队列中已存在死消息,但是队列并不知情。这会影响队列统计数据的正确性,妨碍队列及时释放资源。
配置文件中对某个队列配置TTL参数:注意配置时间需要指定类型为Integer
<rabbit:queue name="test_queue_ttl" id="test_queue_ttl">
<rabbit:queue-arguments>
<entry key="x-message-ttl" value="100000" valuetype="java.lang.Integer">entry>
rabbit:queue-arguments>
rabbit:queue>
利用MessagePostProcessor
消息后处理对象, 配置消息单独过期代码:
MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws
AmqpException {
//1.设置message的信息 消息的过期时间5s
message.getMessageProperties().setExpiration("5000");
//2.返回该消息
return message;
}
};
//消息单独过期
rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.hehe",
"message ttl....",messagePostProcessor);
针对于RabbitMQ准确的说是,DLX(Dead Letter Exchange,死信交换机),因有的MQ没有交换机概念而统称为死信队列。当消息成为Dead message后,会从所在队列直接转给死信队列。
成为死信的三种情况:
正常消息成为死信的参数设置是绑定在正常队列设置里的,且需要指定死后的DLX以及route Key:
<rabbit:queue name="test_queue_dlx" id="test_queue_dlx">
<rabbit:queue-arguments>
<entry key="x-dead-letter-exchange" value="exchange_dlx" />
<entry key="x-dead-letter-routing-key" value="dlx.hehe" />
<entry key="x-message-ttl" value="10000" value-type="java.lang.Integer"/>
<entry key="x-max-length" value="10" value-type="java.lang.Integer" />
rabbit:queue-arguments>
rabbit:queue>
<rabbit:queue name="queue_dlx" id="queue_dlx">rabbit:queue>
<rabbit:topic-exchange name="exchange_dlx">
<rabbit:bindings>
<rabbit:binding pattern="dlx.#" queue="queue_dlx">rabbit:binding>
rabbit:bindings>
rabbit:topic-exchange>
找到配置集群的关键文件,magic cookie文件:
**同步cookie:**其他节点如果要成为集群,就必须使得这些节点的该文件保持一致,请在各节点手动覆盖或新建该文件保持内容一致。
**设置hosts:**将每个节点的/etc/hosts添加映射:
#(只是示例,请填写自己的ip和主机名称)
172.18.8.157 live-mq-01
172.18.8.158 live-mq-02
172.18.8.161 live-mq-03
重启服务并使用detached参数启动各节点:
systemctl restart rabbitmq-server
rabbitmqctl stop
rabbitmq-server -detached
**组建集群:**让新的节点加入到最先启动的节点中去:
rabbitmqctl stop_app
rabbitmqctl join_cluster rabbit@live-mq-01#这里写第一个节点主机名称
rabbitmqctl start_app
每个集群至少要有一个disk节点,上面方式组建后的每个节点都是disk节点,更改为ram内存节点:
rabbitmqctl stop_app
rabbitmqctl change_cluster_node_type ram
rabbitmqctl start_app
查看集群状态:
rabbitmqctl cluster_status
设置镜像队列高可用:默认的普通集群,exchange,binding等数据可以复制到集群各节点,但是各节点只是拥有相同的队列元数据,即队列结构,队列实体只存在于创建该队列的节点,即队列内容不会复制。设置为镜像队列后队列内容会被复制到各个节点:
#查看镜像队列
rabbitmqctl list_policies
#对队列名称以hello开头的所有队列进行镜像,并在集群的两个节点上完成镜像
rabbitmqctl set_policy hello-ha "^hello" '{"ha-mode":"exactly","ha-params":2,"ha-sync-mode":"automatic"}'
参数规则:rabbitmqctl set_policy [-p Vhost] Name Pattern Definition [Priority]