一、安装ActiveMQ
docker 玩法
https://hub.docker.com/_/rabbitmq/
快速启动
docker run --name='activemq' -d \
-e RABBITMQ_DEFAULT_USER=admin \
-e RABBITMQ_DEFAULT_PASS=admin \
-p 15672:15672 -p 5672:5672 \
--name rabbitmq rabbitmq:3-management
- 15672 :表示 RabbitMQ 控制台端口号,可以在浏览器中通过控制台来执行 RabbitMQ 的相关操作。
- 5672 : 表示 RabbitMQ 所监听的 TCP 端口号,应用程序可通过该端口与 RabbitMQ 建立 TCP 连接,并完成后续的异步消息通信
- RABBITMQ_DEFAULT_USER:用于设置登陆控制台的用户名
- RABBITMQ_DEFAULT_PASS:用于设置登陆控制台的密码
如果不设置 默认为 guest / guest
登录 http://www.creprice.cn/market/hz/forsale/ALL/11.html
二、RabbitMQ消息队列基本概念
- Broker: 简单来说就是消息队列服务器实体。
- vhost :一个broker可以开设多个vhost,用于不同用户的权限分离(暂不清楚怎么用 todo)
- Producer: 消息生产者,如图A、B、数据的发送方。消息生产者连接RabbitMQ服务器然后将消息投递到Exchange。
- Consumer:消息消费者,如图1、2、3,数据的接收方。消息消费者订阅队列,RabbitMQ将Queue中的消息发送到消息消费者。
- Exchange:生产者将消息发送到Exchange(交换器),由Exchange将消息路由到一个或多个Queue中(或者丢弃)。Exchange并不存储消息。RabbitMQ中的Exchange 的 ExchangeTypes有fanout、direct、topic、headers四种类型,每种类型对应不同的路由规则。
- Queue:(队列)是RabbitMQ的内部对象,用于存储消息。消息消费者就是通过订阅队列来获取消息的,RabbitMQ中的消息都只能存储在Queue中,生产者生产消息并最终投递到Queue中,消费者可以从Queue中获取消息并消费。多个消费者可以订阅同一个Queue,这时Queue中的消息会被平均分摊给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理。
- RoutingKey:生产者在将消息发送给Exchange的时候
,一般会指定一个routingkey,
来指定这个消息的路由规则,
而这个routingkey需要与ExchangeType及bindingkey联合使用才能最终生效。在Exchange Type与bindingkey固定的情况下(在正常使用时一般这些内容都是固定配置好的),我们的生产者就可以在发送消息给Exchange时,
通过指定routingkey来决定消息流向哪里。
RabbitMQ为routing key设定的长度限制为255 bytes。 - ExchangeTypes
- fanout(广播模式)
1).fanout类型的Exchange路由规则非常简单,它会把所有发送到 该Exchange的消息路由到所有与它绑定的Queue中。 这种是publisher/subcribe模式。用来做广播最好。 生产者发送到Exchange的所有消息都会路由到绑定的Queue。 2).这种模式不需要RouteKey 3).这种模式需要提前将Exchange与Queue进行绑定,一个Exchange可以绑定多个Queue,一个Queue可以同多个Exchange进行绑定。 4).如果接受到消息的Exchange没有与任何Queue绑定,则消息会被抛弃。 5).广播实时性:消费者没有开启的时候,发消息的时候,就没有收到,这个时候就没有了。如果消费者开启了,生产者发消息时,消费者是收的到的
- direct(路由模式)
1)如果 routing key 匹配, 那么Message就会被传递到相应的queue中。
- topic(key模糊匹配模式)
1)对 routing key 进行模式匹配, 比如 绑定 china.# 的queue 发送 china.news 或 china.weather 都能接受到。 绑定 #.weather 可以接受到 china.weather 、usa.weather 类似
三、RabbitMq特性
1. 消息确认
Client 处理消息可能需要几秒钟的时间。
你可能想知道如果其中一个消费者开始一项长期任务并且只是部分完成而死亡会发生什么。
用我们目前的代码,一旦RabbitMQ将消息传递给客户,它立即将其标记为删除。
在这种情况下,如果你杀了一个工人,我们将失去刚刚处理的信息。我们也会失去所有派发给这个特定工作人员但尚未处理的消息。
但我们不想失去任何任务。
如果一名工人死亡,我们希望将任务交付给另一名工人。
为了确保消息永不丢失,RabbitMQ支持消息确认。消费者发回ack(请求)告诉RabbitMQ已经收到,处理了特定的消息,
并且RabbitMQ可以自由删除它。
如果消费者死亡(其通道关闭,连接关闭或TCP连接丢失),RabbitMQ将理解消息未被完全处理,并将重新排队。
如果有其他消费者同时在线,它会迅速将其重新发送给另一位消费者。这样,即使工作人员偶尔死亡,也可以确保没有任何信息丢失。
没有任何消息超时;
当消费者死亡时,RabbitMQ将重新传递消息。即使处理消息需要非常很长的时间也没关系。
2.持久化和非持久化队列
当RabbitMQ退出或崩溃时,它会忘记队列和消息,除非您告诉它不要。
需要做两件事来确保消息不会丢失:我们需要将队列和消息标记为持久。
设置 durable = True
关于消息持久性的说明
将队列标记为永久并不能完全保证消息不会丢失。
尽管它告诉RabbitMQ将消息保存到磁盘,但RabbitMQ接收到消息并且尚未保存消息时仍有一段时间窗口。
此外,RabbitMQ不会为每条消息执行fsync(2) - 它可能只是保存到缓存中,并没有真正写入磁盘。持久性保证不强,但对我们简单的任务队列来说已经足够了。
3.公平派遣
调度仍然无法完全按照我们的想法工作。
例如,在有两名工人的情况下,
当所有奇怪的信息都很多,甚至信息很少时,一名工作人员会一直很忙,另一名工作人员几乎不会做任何工作。
那么,RabbitMQ不知道任何有关这一点,并仍将均匀地发送消息。
发生这种情况是因为RabbitMQ只在消息进入队列时调度消息。它没有考虑消费者未确认消息的数量。它只是盲目地将第n条消息分发给第n个消费者。
为了解决这个问题,我们可以使用
basic.qos方法和 prefetch_count=1设置。
这告诉RabbitMQ一次不要向工作人员发送多个消息。
或者换句话说,不要向工作人员发送新消息,直到它处理并确认了前一个消息。
相反,它会将其分派给不是仍然忙碌的下一个工作人员。
int prefetchCount = 1;
channel.basicQos(prefetchCount);
四、使用Java 连接RabbitMQ
- rabbitmq-java-client
Starting with 4.0, this client releases
are independent from RabbitMQ server releases.
These versions can still be used with RabbitMQ server 3.x.
4.0从此开始,此客户端版本独立于RabbitMQ服务器版本
- 目前最新的为(18.5.16)
com.rabbitmq
amqp-client
5.2.0
五、RabbitMq 工作模式
严格来讲,RabbitMQ只有三种模式:“简单模式”、“work模式”以及“交换机模式”。
对于交换机模式来说,又分三种:“订阅模式”、“路由模式”、“通配符模式”,而他们之间的不同就是交换机类型的不同。
-
1.简单队列(Hello World!----The simplest thing that does something )
public class Producer {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
//步骤一:建立连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//步骤二:定义消息队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
String message = "[Message Producer] 66666";
//步骤三:发布消息
// channel.basicPublish("", QUEUE_NAME, MessageProperties.TEXT_PLAIN, message.getBytes("UTF-8"));
channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
System.out.println("[MessageQueue] Message sent => message=" + message);
//关闭连接
channel.close();
connection.close();
}
public class Customer {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
//步骤一:建立连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//步骤二:创建一个消费者对象,并利用handleDelivery回调函数接受消息
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("[Message Recv] => " + msg);
}
};
//步骤三:将消费者端与消息队列绑定
channel.basicConsume(QUEUE_NAME, false, consumer);
}
}
设置的队列是阅后即焚临时队列没有设置持久,
消费者关闭,队列即消失以上就是RabbitMQ中最简单的消息队列模式,即是生产者生产消息并发送至消息队列,消费者监听队列并从队列中获取消息。
-
2.work-queue模式
如图 C1、C2一起接收 这个Queue 。通过MQ的平均分配策略 轮流消费
代码如下:
public class Worker {
private static final String TASK_QUEUE_NAME = "task_queue";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
final Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
channel.basicQos(1);
final Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println(" [x] Received '" + message + "'");
try {
doWork(message);
} finally {
System.out.println(" [x] Done");
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
channel.basicConsume(TASK_QUEUE_NAME, false, consumer);
}
private static void doWork(String task) {
try {
Thread.sleep(3000);
} catch (InterruptedException _ignored) {
Thread.currentThread().interrupt();
}
}
public class NewTask {
private static final String TASK_QUEUE_NAME = "task_queue";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
String message = getMessage(argv);
channel.basicPublish("", TASK_QUEUE_NAME,
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
channel.close();
connection.close();
}
private static String getMessage(String[] strings) {
if (strings.length < 1)
return "Hello World!";
return joinStrings(strings, " ");
}
private static String joinStrings(String[] strings, String delimiter) {
int length = strings.length;
if (length == 0) return "";
StringBuilder words = new StringBuilder(strings[0]);
for (int i = 1; i < length; i++) {
words.append(delimiter).append(strings[i]);
}
return words.toString();
}
}
-
3.交换器模式
-
(1).广播模式(订阅模式)
ExchangeType 设置成为 fanout
-
“P”为消息的生产者,而“X”是交换机(Exchange),即消息生产者将消息发送到“交换机”而不再是“队列”,然后需要承载成产者消息的队列,与该交换机进行绑定。余下就是消费者监听相关队列来获取服务端推送的消息,满足多个消费者共同获取同一个生产者的所有消息的情况。
当消费者不需要从生产者获取消息了,将相关消息队列与交换机的绑定关系解除即可。
如果队列绑定到交换机,那么此时消息到了哪里了呢?可以这么说,消息“丢失”了。因为当我们尝试将信息发送到一个没有绑定队列的交换机时,由于交换机无法及时将信息推送至消息队列,而交换机本身没有存储信息的能力,则会导致信息丢失。所以这里我们要注意。
public class EmitLog {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
String message = getMessage(argv);
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
channel.close();
connection.close();
}
private static String getMessage(String[] strings){
if (strings.length < 1)
return "info: Hello World!";
return joinStrings(strings, " ");
}
private static String joinStrings(String[] strings, String delimiter) {
int length = strings.length;
if (length == 0) return "";
StringBuilder words = new StringBuilder(strings[0]);
for (int i = 1; i < length; i++) {
words.append(delimiter).append(strings[i]);
}
return words.toString();
}
}
public class ReceiveLogs {
private static final String EXCHANGE_NAME = "logs";
private final static String QUEUE_NAME = "logs_queue_1";//队列名称
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
//每次连接创建新的队列直接bind EXCHANGE 创建随机队列
// String QUEUE_NAME = channel.queueDeclare().getQueue();
// 声明队列 显示 申明
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println(" [x] Received '" + message + "'");
try {
doWork(message);
} finally {
System.out.println(" [x] Done");
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
channel.basicConsume(QUEUE_NAME, false, consumer);
}
private static void doWork(String task) {
try {
Thread.sleep(3000);
} catch (InterruptedException _ignored) {
Thread.currentThread().interrupt();
}
}
}
-
-
(2).路由模式(routing)
ExchangeType 设置成为 direct
-
在这个设置中,我们可以看到有两个队列绑定的RoutingKey 第一个队列用绑定键橙色绑定,第二个队列有两个绑定,一个绑定键为黑色,另一个为绿色。
在这种设置中,使用路由键橙色发布到交换机的消息将被路由到队列Q1。带有黑色或绿色路由键的消息将进入Q2。所有其他消息将被丢弃。
也可以支持多个绑定:
public class EmitLogDirect {
private static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] argv)
throws java.io.IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//消息内容1 info类型
String message = "Hello World!";
channel.basicPublish(EXCHANGE_NAME, "info",null, message.getBytes());
System.out.println("[product] Send '"+ message +"'");
//消息内容2 error类型
message = "Has Error!";
channel.basicPublish(EXCHANGE_NAME, "error",null, message.getBytes());
System.out.println("[product] Send '"+ message +"'");
//消息内容3 warning类型
message = "Has Warning!";
channel.basicPublish(EXCHANGE_NAME, "warning",null, message.getBytes());
System.out.println("[product] Send '"+ message +"'");
channel.close();
connection.close();
}
private static String getSeverity(String[] argv) {
return "";
}
//..
private static String getMessage(String[] strings){
if (strings.length < 1)
return "info: Hello World!";
return joinStrings(strings, " ");
}
private static String joinStrings(String[] strings, String delimiter) {
int length = strings.length;
if (length == 0) return "";
StringBuilder words = new StringBuilder(strings[0]);
for (int i = 1; i < length; i++) {
words.append(delimiter).append(strings[i]);
}
return words.toString();
}
}
public class Recv1 {
private final static String QUEUE_NAME = "test_queue_direct_2";//队列名称
private final static String EXCHANGE_NAME="direct_logs";//交换机名称
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//绑定队列到交换机,并指定了两个路由key
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "info");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "error");
// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1);
// 定义队列的消费者
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println(" [x] Received '" + message + "'");
try {
doWork(message);
} finally {
System.out.println(" [x] Done");
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
// 监听队列,手动返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
}
private static void doWork(String task) {
for (char ch : task.toCharArray()) {
if (ch == '.') {
try {
Thread.sleep(1000);
} catch (InterruptedException _ignored) {
Thread.currentThread().interrupt();
}
}
}
}
}
public class Recv2 {
private final static String QUEUE_NAME = "test_queue_direct_1";//队列名称
private final static String EXCHANGE_NAME="direct_logs";//交换机名称
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//绑定队列到交换机,并指定了两个路由key
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "warning");
// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1);
// 定义队列的消费者
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println(" [x] Received '" + message + "'");
try {
doWork(message);
} finally {
System.out.println(" [x] Done");
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
// 监听队列,手动返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
}
private static void doWork(String task) {
for (char ch : task.toCharArray()) {
if (ch == '.') {
try {
Thread.sleep(1000);
} catch (InterruptedException _ignored) {
Thread.currentThread().interrupt();
}
}
}
}
}
-
-
(3).topic 通配符模式(主题 模糊匹配RutingKey)
ExchangeType 设置成为 topic
-
我们创建了三个绑定:Q1绑定了绑定键“ * .orange。* ”,Q2 绑定了“ 。。rabbit ”和“ lazy。# ”。
将路由键设置为“quick.orange.rabbit”的消息将传递到两个队列。消息“ lazy.orange.elephant ”也会去他们两个对垒。
另一方面,“ quick.orange.fox ”只会进入第一个队列,而“ lazy.brown.fox ”只会进入第二个队列。
“lazy.pink.rabbit”只会传递到第二个队列一次,即使它匹配了两个绑定。“quick.brown.fox ”不匹配任何绑定,因此将被丢弃。
另一方面,“ lazy.orange.male.rabbit”即使有四个单词,也会匹配最后一个绑定,并将传递到第二个队列。
public class Send {
private final static String EXCHANGE_NAME="test_exchange_topic";
public static void main(String[] args) throws Exception {
//获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
//从连接中创建通道
Channel channel = connection.createChannel();
//声明Exchange,指定交换机的类型为topic
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
//消息内容 美国新闻
String message = "美国新闻:川普执行新的医疗政策";
channel.basicPublish(EXCHANGE_NAME, "usa.news",null, message.getBytes());
System.out.println("[product] Send '"+ message +"'");
//消息内容 美国天气
message = "美国天气:芝加哥今日天气阴转多云";
channel.basicPublish(EXCHANGE_NAME, "usa.weather",null, message.getBytes());
System.out.println("[product] Send '"+ message +"'");
//消息内容 中国新闻
message = "中国新闻:苹果发布最新产品iPhoneXXX";
channel.basicPublish(EXCHANGE_NAME, "china.news",null, message.getBytes());
System.out.println("[product] Send '"+ message +"'");
//消息内容 中国天气
message = "中国天气:上海今日天气晴转阵雨";
channel.basicPublish(EXCHANGE_NAME, "china.weather",null, message.getBytes());
System.out.println("[product] Send '"+ message +"'");
//关闭通道和连接
channel.close();
connection.close();
}
}
public class Recv1 {
private final static String QUEUE_NAME = "test_queue_topic_1";//队列名称
private final static String EXCHANGE_NAME="test_exchange_topic";//交换机名称
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//绑定队列到交换机,并指定了一个通配符模式的key,来获取中国大类别下面的所有小类别消息。
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "china.#");
// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1);
// 定义队列的消费者
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println(" [consumer1] Received '" + message + "'");
}
};
// 监听队列,手动返回完成
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
public class Recv2 {
private final static String QUEUE_NAME = "test_queue_topic_2";//队列名称
private final static String EXCHANGE_NAME="test_exchange_topic";//交换机名称
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//绑定队列到交换机,并指定了一个通配符模式的key,来获取美国大类别下面的所有小类别消息。
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "usa.#");
// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1);
// 定义队列的消费者
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println(" [consumer1] Received '" + message + "'");
}
};
// 监听队列,手动返回完成
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}