官方文档: https://www.rabbitmq.com/tutorials/tutorial-one-go.html
消息队列类似数据结构队列,队列只在内存中(私有),而分布式项目需要有一个公有的队列,服务可以给队列发送消息,服务也可以监听/订阅消息队列来接收/处理消息
市面上有很多种消息队列,主要分为采用JMS(Java Message Service)和AMQP(高级消息队列协议)的,AMQP兼容了JMS,RabbitMQ采用AMQP
秒杀系统的高并发请求,每接收一个用户请求就放入消息队列,即可响应用户,而生成订单等服务就订阅消息队列,每取一个消息就处理,不至于大量请求直接让服务器宕机
订单服务,库存服务是2个服务
订单服务通过库存服务提供的API调用库存服务,如果库存服务修改了API,那么订单服务也需要修改调用的API
使用消息队列则可以解耦,订单服务将消息写入消息队列,而库存服务在消息队列中取出消息处理,无论API怎么样变,订单服务再也不用修改原来代码
消息发送者发送消息到消息代理,由消息代理将消息转发到目的地
消息发送者 ==消息=》 消息代理 ==消息=》目的地
消息发送者:某个发送消息的服务 (类似客户端)
消息代理:安装了消息队列的服务(类似服务端)
目的地:接收消息并处理的服务 (类似客户端)
可以把发送者,接收者理解为生产者,消费者(客户端)
消息代理,安装了消息队列的服务 就是服务端
每个服务即是生产者也是消费者,因为它可能有时发送消息有时接收消息
客户端与服务端之间会先建立持续连接,连接上分很多信道,发送消息,接收消息在这些信道上(1个客户端只与服务端建立一条连接)
消息分为消息头,消息体(类似报文),生产者将消息通过信道发送到消息代理后,由消息代理的交换机接收
交换机绑定了很多消息队列,交换机根据消息头的路由键信息,把消息分发到某个消息队列中
消息队列再将消息通过信道发送给消费者,并且它可以感知消费者的状态(如果消费者死了就不会把消息发送出去)
消息代理中还可以划分虚拟主机,可以弄一套开发环境,生产环境的交换机与消息队列或是一套让Java语言使用;一套让PHP语言用,互不影响
总结: 生产者==消息=> 交换机 ==消息=> 消息队列 ==消息=> 消费者
docker run -d --name rabbitmq -p 5672:5672 -p 5671:5671 -p 4396:4396 -p 15672:15672 -p 15671:15671 -p 25672:25672 rabbitmq:management
端口说明
IP:15672进入后台管理系统,默认账号密码为guest
AMQP相比于JMS增加了exchange和bindings,exchange可以绑定不同的queues,根据消息的路由键将消息转发到不同的队列中
exchange有headers,direct,fanout,topic
类型,对应着不同的转发策略
headers类型匹配的不是路由键而是整个消息头,与direct功能类似,但是性能差很多,现在不用
direct
路由键完全匹配,单播,一对一fanout
不使用路由键匹配,exchange将消息广播到所有绑定它的队列topic
路由键模糊匹配,可以使用#(匹配0或多个单词)或*(匹配一或多个单次)
进行匹配,主题(发布/订阅模式)导入依赖后自动配置了很多组件
AmqpAdmin
创建交换机,消息队列,绑定等
RabbitTemplate
发送消息
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
编写配置文件
spring.rabbitmq.host=xx.xx.xx.xx
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/
开启功能@EnableRabbit
@Autowired
AmqpAdmin amqpAdmin;
/**
* 创建交换机
*/
@Test
public void createExchange() {
// DirectExchange(String name 交换机名,
// boolean durable 是否持久化,
// boolean autoDelete 是否自动删除,
// Map arguments参数 )
DirectExchange directExchange = new DirectExchange("exchange.direct.java", true, false, null);
amqpAdmin.declareExchange(directExchange);
log.info("创建交换机,{}", directExchange);
}
/**
* 创建队列
*/
@Test
public void createQueue() {
// Queue(String name队列名,
// boolean durable是否持久化,
// boolean exclusive是否排它(独占:只能有一个消费者),
// boolean autoDelete是否自动删除,
// Map arguments参数)
Queue queue = new Queue("tcl.java", true, false, false, null);
amqpAdmin.declareQueue(queue);
log.info("创建队列,{}", queue);
}
/**
* 绑定交换机和队列
*/
@Test
public void createBinding(){
// Binding(String destination 目的地名,
// DestinationType destinationType 目的地类型(交换机或队列),
// String exchange 要绑定的交换机,
// String routingKey 设置路由键,
// Map arguments 参数)
Binding binding = new Binding("tcl.java", Binding.DestinationType.QUEUE, "exchange.direct.java", "tcl.java", null);
amqpAdmin.declareBinding(binding);
log.info("创建绑定,{}", binding);
}
@Autowired
RabbitTemplate rabbitTemplate;
/**
* 发送消息
*/
@Test
public void sendMessage(){
OrderEntity orderEntity = new OrderEntity();
orderEntity.setCreateTime(new Date());
orderEntity.setAutoConfirmDay(1);
orderEntity.setCouponId(2L);
//convertAndSend(String exchange交换机名, String routingKey路由键, Object object消息)
//会先对消息进行转换,如果消息是对象默认使用Java序列化,可以向容器中添加组件换成JSON序列化
rabbitTemplate.convertAndSend("exchange.direct.java","tcl.java",orderEntity);
log.info("发送消息,{}",orderEntity);
}
添加JSON序列化组件
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
在类上使用@RabbitListener标识要监听的队列
在方法上使用@RabbitHandler标识该队列的消息可能是多种类型(类似重载,区分类型)
@Service("rabbitMQService")
@RabbitListener(queues = "tcl.java")
//在类上使用@RabbitListener标识要监听的队列
//在方法上使用@RabbitHandler标识该队列的消息可能是多种类型(类似重载,区分类型)
public class RabbitMQServiceImpl implements RabbitMQService {
/**
* 接收消息可以使用的参数
* 1. Message 原生的消息对象 消息头+体 org.springframework.amqp.core
* 2. T 消息类型
* 3. Channel 消息传输的信道 com.rabbitmq.client;
*/
@RabbitHandler
public void receiveMessageOrder(Message message,
OrderEntity orderEntity,
Channel channel){
System.out.println("接收消息成功,消息头:"+message.getMessageProperties()+"消息体:"+orderEntity);
}
@RabbitHandler
public void receiveMessageMqMessage(Message message,
MqMessageEntity mqMessageEntity,
Channel channel){
System.out.println("接收消息成功,消息头:"+message.getMessageProperties()+"消息体:"+mqMessageEntity);
}
}
接收消息可以使用的参数
一个消息只会由一个客户端(消费者)处理
消费者处理完成消息才会去再取一个消息处理
消息的传输可能丢失,如果开启事物机制,性能将下降很多,此时确认机制可以保证可靠的传输
生产者==消息=>消息代理: 确认回调
开启确认回调
spring.rabbitmq.publisher-confirms=true
设置确认回调方法
交换机==消息=> 消息队列: 返回回调
开启返回回调
spring.rabbitmq.publisher-returns=true
#消息抵达队列时,优先异步回调returenconfim
spring.rabbitmq.template.mandatory=true
设置返回回调方法
确认回调 和 返回回调方法
@PostConstruct //创建完GulimallRabbitConfig对象执行@PostConstruct方法
public void initRabbitTemplate() {
//设置确认回调 (生产者->消息代理)
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
* 每次收到消息就会执行该方法
* @param correlationData 消息唯一id (发送时可以设置这个唯一id)
* @param ack 是否成功受到
* @param cause 失败原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("correlationData:" + correlationData + " ack:" + ack + " cause:" + cause);
}
});
//设置返回回调
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
/**
* 交换机->消息队列 失败会执行该方法
* @param message 消息信息
* @param replyCode 回复状态码
* @param replyText 恢复文本
* @param exchange 发送消息的交换机
* @param routingKey 路由键
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("message:"+message+" replyCode:"+replyCode+" replyText:"+ replyText+" exchange:"+exchange+" routingKey:"+routingKey);
}
});
}
消息队列==消息=> 消费者: ack手动确认
开启ack手动确认
spring.rabbitmq.listener.simple.acknowledge-mode=manual
消费者处理成功手动确认,处理失败拒绝确认
basicAck(long deliveryTag确认收货的消息标签, boolean multiple 是否连续确认)
channel.basicReject();也是拒绝确认 不能连续
basicNack(long deliveryTag确认收货的消息标签, boolean multiple是否连续拒绝, boolean requeue是否放回队列)
//获取收货标签 收获标签自增
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
if (deliveryTag % 2 != 0) {
//手动确认
//basicAck(long deliveryTag确认收货的消息标签, boolean multiple 是否连续确认)
channel.basicAck(deliveryTag, false);
System.out.println("手动确认,标签:"+deliveryTag);
} else {
//拒绝确认
//channel.basicReject();也是拒绝确认 不能连续
//basicNack(long deliveryTag确认收货的消息标签, boolean multiple是否连续拒绝, boolean requeue是否放回队列)
channel.basicNack(deliveryTag,false,true);
System.out.println("拒绝确认,标签:"+deliveryTag);
}
} catch (IOException e) {
//网络中断
e.printStackTrace();
}