1.安装
docker拉取镜像:
docker pull rabbitmq
运行镜像:
docker run -dit --name Myrabbitmq -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin -p 15672:15672 -p 5672:5672 rabbitmq
其中5672为应用访问端口,15672为web端口,如果是阿里云需要在阿里云开放安全组。
之后发现ip:15672仍然不能访问,执行如下两条命令即可。
docker exec -i -t rabbitmq容器id /bin/bash
rabbitmq-plugins enable rabbitmq_management
2.使用
2.1 逻辑结构
2.2 工作模式
消息通信由消息生产者(Producer) 和消息消费者(Consumer) 共同完成。
2.2.1 简单模式
一个队列只有一个消费者。
此时消费者相当于一个监听器,只要发现队列中有消息,消费者就进行消费。而可以有多个生产者同时进行生产。
2.2.2 工作模式
多个消费者监听同一个队列,但多个消费者中只有一个消费者会成功的消费消息。
2.2.3 订阅模式
一个交换机绑定多个消息队列,每个消息队列有一个消费者进行监听,消息生产者发送的消息可以被每个消费者接收。此时交换机工作模式为fanout。
2.2.4 路由模式
一个交换机绑定多个消息队列,每个消息队列都有自己唯一的key,每个消息队列都有一个消费者进行监听,消息可以进入指定队列中。此时交换机工作模式为redirect。
2.3 普通maven项目中使用rabbitmq
为了避免重复操作,抽取MQUtils工具类。
public static Connection getConnection() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("ip");
factory.setPort(端口(5672));
factory.setVirtualHost("主机名");
factory.setUsername("用户名");
factory.setPassword("密码");
Connection connection = factory.newConnection();
return connection;
}
2.3.1 简单模式
生产者:
public static void main(String[] args) throws IOException, TimeoutException {
String msg="hello sgl";
Connection connection = MQUtils.getConnection();
Channel channel = connection.createChannel();
//参数1,交换机名称,不需要为""
//参数2,队列名称
//参数3,消息属性,不需要为null
//参数4,消息内容
channel.basicPublish("","queue1",null,msg.getBytes());
System.out.println(msg);
channel.close();
connection.close();
}
消费者:
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = MQUtils.getConnection();
Channel channel = connection.createChannel();
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//body就是接受的消息数据
String msg=new String(body);
System.out.println(msg);
}
};
channel.basicConsume("queue1",true,consumer);
}
生产者进行生产后
消费者进行消费
idea控制台打印
2.3.2 工作模式
创建如图所示结构:
其中生产者与消费者的具体代码与简单模式中的相同。
首先运行两个消费者,使得两个消费者进行等待,然后运行生产者,两个消费者都有可能对这条消息进行消费,观察消息被哪个消费者进行消费。
运行两次,一次被Consumer1消费,一次被Consumer2消费。
2.3.3 订阅模式
生产者:
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("请输入消息:");
Scanner scanner = new Scanner(System.in);
String msg = scanner.nextLine();
//String msg="hello sgl";
Connection connection = MQUtils.getConnection();
Channel channel = connection.createChannel();
//参数1,交换机名称,不需要为""
//参数2,队列名称
//参数3,消息属性,不需要为null
//参数4,消息内容
channel.basicPublish("ex1","",null,msg.getBytes());
System.out.println("生产消息"+msg);
channel.close();
connection.close();
}
消费者1:
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = MQUtils.getConnection();
Channel channel = connection.createChannel();
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//body就是接受的消息数据
String msg=new String(body);
System.out.println("Consumer1接收"+msg);
}
};
channel.basicConsume("queue3",true,consumer);
}
消费者2:
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = MQUtils.getConnection();
Channel channel = connection.createChannel();
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//body就是接受的消息数据
String msg=new String(body);
System.out.println("Consumer2接收"+msg);
}
};
channel.basicConsume("queue4",true,consumer);
}
运行结果:
两个Consumer均进行了消息的消费。
2.3.4 路由模式
生产者:
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("请输入消息:");
Scanner scanner = new Scanner(System.in);
String msg = scanner.nextLine();
//String msg="hello sgl";
Connection connection = MQUtils.getConnection();
Channel channel = connection.createChannel();
//参数1,交换机名称,不需要为""
//参数2,交换机名称为空,为队列名,名称不为空,为key值
//参数3,消息属性,不需要为null
//参数4,消息内容
if (msg.startsWith("a")) {
channel.basicPublish("ex2", "a", null, msg.getBytes());
}else if (msg.startsWith("b")){
channel.basicPublish("ex2", "b", null, msg.getBytes());
}
System.out.println("生产消息"+msg);
channel.close();
connection.close();
}
消费者1:
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = MQUtils.getConnection();
Channel channel = connection.createChannel();
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//body就是接受的消息数据
String msg=new String(body);
System.out.println("Consumer1接收"+msg);
}
};
channel.basicConsume("queue5",true,consumer);
}
消费者2:
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = MQUtils.getConnection();
Channel channel = connection.createChannel();
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//body就是接受的消息数据
String msg=new String(body);
System.out.println("Consumer2接收"+msg);
}
};
channel.basicConsume("queue6",true,consumer);
}
运行结果:
消费者1消费aaa,消费者2消费bbb。
2.4 SpringBoot项目中使用rabbitmq
2.4.1 生产者
添加依赖
配置yml
Service层
@Service
public class TestService {
@Autowired
private AmqpTemplate amqpTemplate;
public void sendMsg(String msg){
if (msg.startsWith("q_")){
//发送消息到队列
amqpTemplate.convertAndSend("queue1",msg);
}else if (msg.startsWith("f_")){
//发送消息到交换机(订阅交换机)
amqpTemplate.convertAndSend("ex1","",msg);
}else if (msg.startsWith("r_")){
if (msg.startsWith("r_a")){
//发送消息到交换机(路由交换机)
amqpTemplate.convertAndSend("ex2","a",msg);
}else if (msg.startsWith("r_b")){
amqpTemplate.convertAndSend("ex2","b",msg);
}
}
}
}
2.4.2 消费者
同样添加依赖和配置yml文件,如果消费者和生产者需要同时运行,需要改端口。
Service层:
@Service
//监听queue1的消息
@RabbitListener(queues = "queue1")
public class ReceiveMsgService {
@RabbitHandler
public void ReceiveMsg(String msg){
System.out.println("接收:"+msg);
}
}
2.4.3 使用RabbitMQ传递对象
使用序列化对象进行传输:
生产者(实体类需实现Serializable接口):
@Service
public class MQService {
@Autowired
private AmqpTemplate amqpTemplate;
public void sendGoodsToMQ(Goods goods){
//消息队列发送字符串、字节数组和序列化对象
amqpTemplate.convertAndSend("","queue1",goods);
}
}
消费者(实体类需实现Serializable接口):
@Service
@RabbitListener(queues = "queue1")
public class ReceiveService {
// @RabbitHandler
// public void receiveMsg(String msg){
// System.out.println(msg);
// }
@RabbitHandler
public void receiveMsg(Goods goods){
System.out.println(goods);
}
}
使用序列化数组进行传输:
生产者(实体类需实现Serializable接口):
@Service
public class MQService {
@Autowired
private AmqpTemplate amqpTemplate;
public void sendGoodsToMQ(Goods goods){
//消息队列发送字符串、字节数组和序列化对象
//amqpTemplate.convertAndSend("","queue1",goods);
byte[] bytes = SerializationUtils.serialize(goods);
amqpTemplate.convertAndSend("","queue1",bytes);
}
}
消费者(实体类需实现Serializable接口):
@Service
@RabbitListener(queues = "queue1")
public class ReceiveService {
// @RabbitHandler
// public void receiveMsg(Goods goods){
// System.out.println(goods);
// }
@RabbitHandler
public void receiveMsg(byte[] bytes){
Goods goods= (Goods) SerializationUtils.deserialize(bytes);
System.out.println(goods);
}
}
使用json进行传输:
生产者:
public void sendGoodsToMQ(Goods goods) throws JsonProcessingException {
//消息队列发送字符串、字节数组和序列化对象
ObjectMapper objectMapper = new ObjectMapper();
String msg = objectMapper.writeValueAsString(goods);
amqpTemplate.convertAndSend("","queue1",msg);
}
消费者:
@RabbitHandler
public void receiveMsg(String msg) throws JsonProcessingException {
ObjectMapper m = new ObjectMapper();
Goods goods = m.readValue(msg, Goods.class);
System.out.println(goods);
}
2.5 消息的可靠性
2.5.1 RabbitMQ事务
当在消息发送过程中添加了事务,效率降低几十倍甚至上百倍。
channel.txSelect();//开启事务
try{
channel.basicPublish("ex4","k1",null,msg.getBytes());
channel.txCommit();//提交事务
}catch(Execption e){
channel.txRollback();//事务回滚
}
2.5.2 消息确认和return机制
消息确认:确认生产者是否成功发送消息到交换机。
return机制:确认到达交换机的数据是否成功分发到队列。
对于普通maven项目
- 消息确认
b为true发送成功,b为false发送失败。
channel.confirmSelect();//发送之前开启消息确认
channel.basicPublish("ex1","queue1",null,msg.getBytes());
boolean b = channel.waitForConfirms();//接收消息确认
System.out.println(b);
当批量进行接收时,发送的所有消息中如果有一条消息是失败的,则所有消息发送均失败。抛出异常。
当必须收到确认消息才能进行下一步时,选上面的方式进行确认。如果不需要收到确认消息,就可以进行下一步操作,就可以使用下面的这种方式:监听器异步confirm。
channel.confirmSelect();//发送之前开启消息确认
//异步确认
channel.addConfirmListener(new ConfirmListener() {
//参数1 long l 返回消息
//参数2 boolean b 是否为批量confirm
//不需进行channel和connection的关闭
public void handleAck(long l, boolean b) throws IOException {
System.out.println("消息成功发送到交换机");
}
public void handleNack(long l, boolean b) throws IOException {
System.out.println("消息发送到交换机失败");
}
});
- return机制
channel.confirmSelect();//发送之前开启消息确认
//参数1,交换机名称,不需要为""
//参数2,队列名称
//(true)表示开启return机制
//参数3,消息属性,不需要为null
//参数4,消息内容
channel.basicPublish("ex2","c",true,null,msg.getBytes());
channel.addConfirmListener(new ConfirmListener() {
//参数1 long l 返回消息
//参数2 boolean b 是否为批量confirm
//不需进行channel和connection的关闭
public void handleAck(long l, boolean b) throws IOException {
System.out.println("消息成功发送到交换机");
}
public void handleNack(long l, boolean b) throws IOException {
System.out.println("消息发送到交换机失败");
}
});
channel.addReturnListener(new ReturnListener() {
public void handleReturn(int i, String s, String s1, String s2, AMQP.BasicProperties basicProperties, byte[] bytes) throws IOException {
//return机制,如果交换机分发消息到队列失败,则会执行此方法 s1和s2分别表示交换机和发送失败的key
System.out.println(i);
System.out.println(s);
System.out.println(s1);
System.out.println(s2);
System.out.println(new String(bytes));
}
});
由于ex2并没有key为c的队列,所以执行失败,执行handleReturn方法。如下图:
对于SpringBoot项目
在消费端确认(保证每个消息被正确消费,此时才可以删除这个消息)
-
默认消费端为自动确认,只要消息接收到,客户端就会自动确认,服务器就会移除这个消息。
-
自动确认存在的问题:我们收到很多消息,自动回复给服务器ack,只有一个消息处理成功,宕机了。发生消息丢失。
-
综上,应该开启消费者手动确认模式,即使Consumer宕机,消息不会丢失,会重新变为ready,下次consumer上线后重新进行消费。
-
手动确认消息签收?
#配置文件
spring.rabbitmq.listener.simple.acknowledge-mode=manual
//签收,业务完成后就应该签收
channel.basicAck(deliveryTag,false);
//拒签,业务失败,拒签
channel.basicNack(deliveryTag,false,true);
3. 延迟机制
3.1 延迟队列
消息进入到队列之后,延迟指定的时间才能被消费。
Rabbitmq不支持延迟队列,那么怎么实现消息延迟呢?
通过TTL(Time To Live)特性模拟延迟队列功能。
在创建队列时可以设置RabbitMQ中消息或者队列的过期时间,如果到期仍然没被消费,消息会被销毁。这个消息称为死信,TTL就是消息或者队列的存活时间。RabbitMQ可以分别对队列和消息设置存活时间。一般只设置队列的TTL。
实现延迟队列:A服务将消息发到交换机,指定key1后发到queue1中,设置30分钟过期,由于这个队列为死信队列(消息会过期,无人接收),所以可以设置在队列到期时将消息发送到queue2中,而B服务对queue2进行监听,发现有消息立即进行消费,这样就实现了消息的延迟机制。
3.2 使用延迟队列实现订单监控
1.创建路由交换机
2.创建两个队列,其中队列1为死信队列。
3.队列绑定交换机。
4.测试。
生产者
String msg="hello sgl";
Connection connection = MQUtils.getConnection();
Channel channel = connection.createChannel();
//参数1,交换机名称,不需要为""
//参数2,交换机名称为空,为队列名,名称不为空,为key值
//参数3,消息属性,不需要为null
//参数4,消息内容
channel.basicPublish("delay_exchange", "k1", null, msg.getBytes());
System.out.println("生产消息"+msg);
channel.close();
connection.close();
消费者
Connection connection = MQUtils.getConnection();
Channel channel = connection.createChannel();
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//body就是接受的消息数据
String msg=new String(body);
System.out.println("Consumer1接收"+msg);
}
};
channel.basicConsume("delay_queue2",true,consumer);
运行结果
运行后生产者立即开始生产消息,当过10s后消费者delay_queue2进行了消息的消费。测试成功。
4. 消息队列使用场景
4.1 解耦
场景说明:用户下单之后,订单系统通知库存系统进行库存的修改。
传统方式
订单系统直接调用库存系统提供的接口,如果库存系统出现故障会导致订单系统失败。
使用消息队列
进行解耦,如果库存系统出现了问题,消息存在消息队列中,不会被消费。待库存系统恢复正常后再进行消费。
4.2 异步
场景说明:用户注册成功后,需要发送注册邮件和短信进行提醒。
传统方式
响应时间较长。
使用消息队列
响应时间减少。
4.3 消息通信
场景说明:应用系统之间的通信,如聊天室。
4.4 流量削峰
场景说明:秒杀系统。
大量的请求不会主动请求秒杀业务,而是存在消息队列中(缓存)。当秒杀业务还有能力处理时,从消息队列中取消息进行处理。传统方式会将全部请求直接打到秒杀业务中,当秒杀业务没有能力进行处理时,就会造成系统的崩溃。
4.5 日志处理
场景说明:系统中大量的日志处理。