RabbitMQ是一个消息中间件,你可以想象它是一个邮局。当你把信件放到邮箱里时,能够确信邮递员会正确地递送你的信件。RabbitMq就是一个邮箱、一个邮局和一个邮递员。
添加slf4j依赖,和rabbitmq amqp依赖
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.4.3</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.8.0-alpha2</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.8.0-alpha2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
public class Producer {
public static void main(String[] args) throws Exception{
//连接 rabbitmq服务器
ConnectionFactory f = new ConnectionFactory();
f.setHost("192.168.66.129");
f.setPort(5672);//收发消息是5672是通信端口
f.setUsername("admin");
f.setPassword("admin");
f.setVirtualHost("/jyl");//设置虚拟主机
Connection con = f.newConnection();
Channel c = con.createChannel();
//定义队列,会通知服务器想使用一个 "helloworld" 队列
//服务器会找到这个队列,如果不存在,服务器会新建队列
// 5个参数
// 队列名,是否持久队列,是否排他(独占)队列,是否自动删除,其他参数属性
c.queueDeclare("helloworld",false,false,false,null);
//发送消息
// 4个参数
// 空串儿-后面用到时在介绍,队列名,其他参数属性,消息数据
c.basicPublish("","helloworld",null,"Hello world".getBytes());
System.out.println("消息已发送");
c.close();
con.close();
}
}
public class Consumer {
public static void main(String[] args) throws Exception{
//连接
ConnectionFactory f = new ConnectionFactory();
f.setHost("192.168.66.129");
f.setPort(5672);
f.setUsername("admin");
f.setPassword("admin");
f.setVirtualHost("/jyl");//设置虚拟主机
Channel c = f.newConnection().createChannel();//新建(重用)连接,创建通道
// 定义队列
c.queueDeclare("helloworld",false,false,false,null);
//创建处理消息的回调对象
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
byte[] body = message.getBody();
String msg=new String(body);
System.out.println("收到:"+msg);
}
};
//创建取消接受的回调对象
CancelCallback cancelCallback = new CancelCallback() {
@Override
public void handle(String consumerTag) throws IOException {
}
};
//从helloworld 队列接受消息,消费消息
c.basicConsume("helloworld",true,deliverCallback,cancelCallback);
}
}
工作队列(即任务队列)背后的主要思想是避免立即执行资源密集型任务,并且必须等待它完成。相反,我们将任务安排在稍后完成。
我们将任务封装为消息并将其发送到队列。后台运行的工作进程将获取任务并最终执行任务。当运行多个消费者时,任务将在它们之间分发。
使用任务队列的一个优点是能够轻松地并行工作。如果我们正在积压工作任务,我们可以添加更多工作进程,这样就可以轻松扩展。
public class Producer {
public static void main(String[] args) throws Exception{
ConnectionFactory f= new ConnectionFactory();
f.setHost("192.168.66.129");
f.setUsername("admin");
f.setPassword("admin");
f.setVirtualHost("/jyl");
Channel c = f.newConnection().createChannel();
//定义队列
c.queueDeclare("task_queue",false,false,false,null);
//发消息
while (true){
System.out.println("输入消息");
String msg= new Scanner(System.in).nextLine();
c.basicPublish("","task_queue", null,msg.getBytes());
}
}
}
public class Consumer {
public static void main(String[] args) throws Exception{
//连接
ConnectionFactory f= new ConnectionFactory();
f.setHost("192.168.66.129");
f.setUsername("admin");
f.setPassword("admin");
f.setVirtualHost("/jyl");
Channel c = f.newConnection().createChannel();
//定义队列
c.queueDeclare("task_queue",false,false,false,null);
//处理消息
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
String msg=new String(message.getBody());
System.out.println("收到:"+msg);
for (int i = 0; i <msg.length() ; i++) {
if(msg.charAt(i)=='.'){
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
System.out.println("消息处理完");
}
};
CancelCallback cancelCallback = new CancelCallback() {
@Override
public void handle(String consumerTag) throws IOException {
}
};
//消费数据
c.basicConsume("task_queue",true,deliverCallback,cancelCallback);
System.out.println("开始消费数据");
}
}
运行:
- 一个生产者
- 连个消费者
生产者发送多条消息
如: 1,2,3,4,5 两个消费者分别收到:
一个消费者接收消息后,在消息没有完全处理完时就挂掉了,那么这时会发生什么呢?
就现在的代码来说,rabbitmq把消息发送给消费者后,会立即删除消息,那么消费者挂掉后,它没来得及处理的消息就会丢失
如果生产者发送以下消息:
1…2
3
4
5
两个消费者分别收到:
消费者一: 1…, 3, 5 消费者二: 2, 4
当消费者一收到所有消息后,要话费7秒时间来处理第一条消息,这期间如果关闭该消费者,那么1未处理完成,3,5则没有被处理
我们并不想丢失任何消息, 如果一个消费者挂掉,我们想把它的任务消息派发给其他消费者
为了确保消息不会丢失,rabbitmq支持消息确认(回执)。当一个消息被消费者接收到并且执行完成后,消费者会发送一个ack (acknowledgment) 给rabbitmq服务器, 告诉他我已经执行完成了,你可以把这条消息删除了。
如果一个消费者没有返回消息确认就挂掉了(信道关闭,连接关闭或者TCP链接丢失),rabbitmq就会明白,这个消息没有被处理完成,rebbitmq就会把这条消息重新放入队列,如果在这时有其他的消费者在线,那么rabbitmq就会迅速的把这条消息传递给其他的消费者,这样就确保了没有消息会丢失。
这里不存在消息超时, rabbitmq只在消费者挂掉时重新分派消息, 即使消费者花非常久的时间来处理消息也可以
手动消息确认默认是开启的,前面的例子我们通过autoAck=ture把它关闭了。我们现在要把它设置为false,然后工作进程处理完意向任务时,发送一个消息确认(回执)。
public class Consumer {
public static void main(String[] args) throws Exception{
//连接
ConnectionFactory f= new ConnectionFactory();
f.setHost("192.168.66.129");
f.setUsername("admin");
f.setPassword("admin");
f.setVirtualHost("/jyl");
Channel c = f.newConnection().createChannel();
//定义队列
c.queueDeclare("task_queue",false,false,false,null);
//处理消息
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
String msg=new String(message.getBody());
System.out.println("收到:"+msg);
for (int i = 0; i <msg.length() ; i++) {
if(msg.charAt(i)=='.'){
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
c.basicAck(message.getEnvelope().getDeliveryTag(),false);
System.out.println("消息处理完");
}
};
CancelCallback cancelCallback = new CancelCallback() {
@Override
public void handle(String consumerTag) throws IOException {
}
};
//消费数据
c.basicConsume("task_queue",false,deliverCallback,cancelCallback);
System.out.println("开始消费数据");
}
}
使用以上代码,就算杀掉一个正在处理消息的工作进程也不会丢失任何消息,工作进程挂掉之后,没有确认的消息就会被自动重新传递。
rabbitmq会一次把多个消息分发给消费者, 这样可能造成有的消费者非常繁忙, 而其它消费者空闲. 而rabbitmq对此一无所知, 仍然会均匀的分发消息
我们可以使用 basicQos(1)
方法, 这告诉rabbitmq一次只向消费者发送一条消息, 在返回确认回执前, 不要向消费者发送新消息. 而是把消息发给下一个空闲的消费者
//QOS: Quality of Service
//理解: 每次抓取的消息数量
//如果设置成1,每次只抓取一条消息,这条消息处理完之前,不会继续抓取吓一条
//必须在手动Ack模式下才有效
c.basicQos(1);
当rabbitmq关闭时, 我们队列中的消息仍然会丢失, 除非明确要求它不要丢失数据
要求rabbitmq不丢失数据要做如下两点: 把队列和消息都设置为可持久化(durable)
队列设置为可持久化, 可以在定义队列时指定参数durable为true
//第二个参数是持久化参数durable
ch.queueDeclare("某某", true, false, false, null);
由于之前我们已经定义过队列"某某"是不可持久化的, 对已存在的队列, rabbitmq不允许对其定义不同的参数, 否则会出错, 所以这里我们定义一个不同名字的队列"task_queue"
//定义一个新的队列,名为 task_queue
//第二个参数是持久化参数 durable
ch.queueDeclare("task_queue", true, false, false, null);
生产者和消费者代码都要修改
这样即使rabbitmq重新启动, 队列也不会丢失. 现在我们再设置队列中消息的持久化, 使用MessageProperties.PERSISTENT_TEXT_PLAIN
参数
//第三个参数设置消息持久化
ch.basicPublish("", "task_queue",
MessageProperties.PERSISTENT_TEXT_PLAIN,
msg.getBytes());
下面是"工作模式"最终完成的生产者和消费者代码
public class Producer {
public static void main(String[] args) throws Exception{
ConnectionFactory f= new ConnectionFactory();
f.setHost("192.168.66.129");
f.setUsername("admin");
f.setPassword("admin");
f.setVirtualHost("/jyl");
Channel c = f.newConnection().createChannel();
//定义队列
c.queueDeclare("task_queue",true,false,false,null);
//发消息
while (true){
System.out.println("输入消息");
String msg= new Scanner(System.in).nextLine();
c.basicPublish("","task_queue", MessageProperties.PERSISTENT_TEXT_PLAIN,msg.getBytes());
}
}
}
public class Consumer {
public static void main(String[] args) throws Exception{
//连接
ConnectionFactory f= new ConnectionFactory();
f.setHost("192.168.66.129");
f.setUsername("admin");
f.setPassword("admin");
f.setVirtualHost("/jyl");
Channel c = f.newConnection().createChannel();
//定义队列
c.queueDeclare("task_queue",true,false,false,null);
//处理消息
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
String msg=new String(message.getBody());
System.out.println("收到:"+msg);
for (int i = 0; i <msg.length() ; i++) {
if(msg.charAt(i)=='.'){
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
c.basicAck(message.getEnvelope().getDeliveryTag(),false);
System.out.println("消息处理完");
}
};
CancelCallback cancelCallback = new CancelCallback() {
@Override
public void handle(String consumerTag) throws IOException {
}
};
c.basicQos(1);
//QOS: Quality of Service
//理解: 每次抓取的消息数量
//如果设置成1,每次只抓取一条消息,这条消息处理完之前,不会继续抓取吓一条
//必须在手动Ack模式下才有效
//消费数据
c.basicConsume("task_queue",false,deliverCallback,cancelCallback);
System.out.println("开始消费数据");
}
}
在前面的例子中,我们任务消息只交付给一个工作进程。在这部分,我们将做一些完全不同的事情——我们将向多个消费者传递同一条消息。这种模式称为“发布/订阅”。
为了说明该模式,我们将构建一个简单的日志系统。它将由两个程序组成——第一个程序将发出日志消息,第二个程序接收它们。
在我们的日志系统中,接收程序的每个运行副本都将获得消息。这样,我们就可以运行一个消费者并将日志保存到磁盘; 同时我们可以运行另一个消费者在屏幕上打印日志。
最终, 消息会被广播到所有消息接受者
RabbitMQ消息传递模型的核心思想是,生产者永远不会将任何消息直接发送到队列。实际上,通常生产者甚至不知道消息是否会被传递到任何队列。
相反,生产者只能向交换机(Exchange)发送消息。交换机是一个非常简单的东西。一边接收来自生产者的消息,另一边将消息推送到队列。交换器必须确切地知道如何处理它接收到的消息。它应该被添加到一个特定的队列中吗?它应该添加到多个队列中吗?或者它应该被丢弃。这些规则由exchange的类型定义。
有几种可用的交换类型:direct、topic、header和fanout。我们将关注最后一个——fanout
。让我们创建一个这种类型的交换机,并称之为 logs: ch.exchangeDeclare(“logs”, “fanout”);
fanout交换机非常简单。它只是将接收到的所有消息广播给它所知道的所有队列。这正是我们的日志系统所需要的。
我们前面使用的队列具有特定的名称(还记得hello和task_queue吗?)能够为队列命名对我们来说至关重要——我们需要将工作进程指向同一个队列,在生产者和消费者之间共享队列。
但日志记录案例不是这种情况。我们想要接收所有的日志消息,而不仅仅是其中的一部分。我们还只对当前的最新消息感兴趣,而不是旧消息。
要解决这个问题,我们需要两件事。首先,每当我们连接到Rabbitmq时,我们需要一个新的空队列。为此,我们可以创建一个具有随机名称的队列,或者,更好的方法是让服务器为我们选择一个随机队列名称。其次,一旦断开与使用者的连接,队列就会自动删除。在Java客户端中,当我们不向queueDeclare()提供任何参数时,会创建一个具有生成名称的、非持久的、独占的、自动删除队列
//自动生成队列名
//非持久,独占,自动删除
String queueName = ch.queueDeclare().getQueue();
我们已经创建了一个fanout交换机和一个队列。现在我们需要告诉exchange向指定队列发送消息。exchange和队列之间的关系称为绑定
//指定的队列,与指定的交换机关联起来
//成为绑定 -- binding
//第三个参数时 routingKey, 由于是fanout交换机, 这里忽略 routingKey
ch.queueBind(queueName, "logs", "");
现在, logs交换机将会向我们指定的队列添加消息
列出绑定关系:
rabbitmqctl list_bindings
生产者发出日志消息,看起来与前一教程没有太大不同。最重要的更改是,我们现在希望将消息发布到logs交换机,而不是无名的日志交换机。我们需要在发送时提供一个routingKey,但是对于fanout交换机类型,该值会被忽略。
public class Producer {
public static void main(String[] args) throws Exception{
//创建连接
ConnectionFactory f= new ConnectionFactory();
f.setHost("192.168.66.129");
//f.setPort(5672);
f.setUsername("admin");
f.setPassword("admin");
f.setVirtualHost("/jyl");
Channel c = f.newConnection().createChannel();
//定义交换机
c.exchangeDeclare("logs", BuiltinExchangeType.FANOUT);
//向交换机发送数据
while (true){
System.out.println("输入消息");
String msg = new Scanner(System.in).nextLine();
//四个参数
// 交换机名,队列名(对于fanout交换机无效)
c.basicPublish("logs","",null,msg.getBytes());
}
}
}
如果还没有队列绑定到交换器,消息就会丢失,但这对我们来说没有问题;如果还没有消费者在听,我们可以安全地丢弃这些信息。
public class Consumer {
public static void main(String[] args) throws Exception{
//创建连接
ConnectionFactory f= new ConnectionFactory();
f.setHost("192.168.66.129");
//f.setPort(5672);
f.setUsername("admin");
f.setPassword("admin");
f.setVirtualHost("/jyl");
Channel c = f.newConnection().createChannel();
//先做三步: 1.定义交换机 2.定义随机队列 3.绑定到交换机
c.exchangeDeclare("logs", BuiltinExchangeType.FANOUT);
//String queue=UUID.randomUUID().toString();
//队列名称,是否持久化,独占,自动删除,其他参数属性
//c.queueDeclare(queue,false,true,true,null);
//让rabbitmq服务器来随机命名,非持久,独占,自动删除
String queue = c.queueDeclare().getQueue();
//第三个参数对 fanout交换机无效
c.queueBind(queue,"logs","");
DeliverCallback deliverCallback=new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
String msg = new String(message.getBody());
System.out.println("收到:" + msg);
}
};
CancelCallback cancelCallback = new CancelCallback() {
@Override
public void handle(String consumerTag) throws IOException {
}
};
//正常消费数据
c.basicConsume(queue,true, deliverCallback, cancelCallback);
}
}