1.为什么会有这个
异步
为了提升系统的性能,同步变异步。
以前调用
是 调用接口 (5s)--》1方法(5s)-》2方法(5s)-》3方法(5s)。 ==20s
异步是
调用接口(5s) --》1方法(5s)
--》2方法(5s)
--》3方法(5s) ==10s
消息中间件异步
调用接口(5s)-->消息中间件(1s)--》1方法(5s)
--》2方法(5s)
--》3方法(5s) --》6s
一对比就可以看出消息中间件的异步更加快捷
解耦
以下订单减库存为例,下完订单就要调用减库存的方法,如果要进行系统升级,下订单的参数改变,那么减库存的方法也得改,麻烦。 下完订单,将消息发给消息中间件,不用关心别的接口设计成什么样。 减库存自己去取消息,自己减库存就ok。
削峰
以秒杀举例,当我们大量请求进来以后,我们可以给前端返回秒杀成功,然后,将消息发给消息中间件,剩下的那些业务去订阅消息中间件的秒杀请求,挨个处理下订单,减库存。
rabbitmq兼容jms(java message service)协议,类似于jdbc,并且实现amqp
里面定义了一些api。
消费模式
1、点对点消费 生产者将消息发给消息代理,消息代理将其发送到队列当中,消息接收者从队列中获取消息内容,消息从队列中移除。
消息只能有唯一的生产者和消费者,但是可以有多个消费者去接收。
2、发布订阅 是基于topic的,一个发布了,所有订阅的主机都会接收到相应的信息。
rabbitmq里的组件
生产者 ————>产出消息的人
消息————>由message头和体组成,头里面有消息的各种设置,体就是具体的消息内容,里面还有一个routeKey,表示发送给那个路由
消息代理————>里面有路由器exchange,exchange负责接收消息,并将消息转发给对应的queue。
无论是生产者还是消费者都需要创建一个长连接来连接中间件,长连接里面有很多信道,可以负责消息的传送,
docker安装rabbitmq
docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672rabbitmq:management
4369,25672 (Erlang发现&集群端口)5672,5671(AMQP端口)
15672(web管理后台端口)
61613,61614(STOMP协议端口)1883,8883(MQTT协议端口)
https:/ /www.rabbitmq.com/networking.html rabbitmq官网
rabbitmq运行机制
生产者产生消息,发送给broker,broker里面的exchange接受消息,并发送给绑定的queue,然后消费者从queue中取消息。
exchange的类型
根据交换机的类型的不同发送给的queue也不一样。 接下来来看一下常见的exchange类型,direct,点对点。 topic,发布订阅,fanout扇出。
direct类型
fanout类型
topic类型
topic是根据模式匹配的,转发到对应的queue。
springboot整合rabbitmq
引入maven
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
然后再测试类里面测试,创建exchange、queue、binding。
通过amqpAdmin来创建。
@Autowired
AmqpAdmin amqpAdmin;
@Autowired
RabbitTemplate rabbitTemplate;
@Test
void createExchange() throws Exception{
DirectExchange exchange = new DirectExchange("com.bai.exchange",true,true);
amqpAdmin.declareExchange(exchange);
}
@Test
void createQueue() throws Exception{
//String name, boolean durable, boolean exclusive, boolean autoDelete, @Nullable Map<String, Object> arguments
Queue queue = new Queue("hello-java",true,false,false);
amqpAdmin.declareQueue(queue);
}
@Test
void createBinding() throws Exception{
//String destination, DestinationType destinationType, String exchange, String routingKey, @Nullable Map<String, Object> arguments) {
Binding binding = new Binding("hello-java", Binding.DestinationType.QUEUE,"com.bai.exchange","hello-java",null);
amqpAdmin.declareBinding(binding);
}
@Test
void sendMessage() throws Exception{
//String exchange, String routingKey, Object object, @Nullable CorrelationData correlationData)
User user = new User();
user.setName("user");
user.setPassword("123456");
user.setGender(1);
rabbitTemplate.convertAndSend("com.bai.exchange","hello-java",user);
}
通过测试
已经创建成功。 但是!
如果传的是对象的话,会发生这种现象:
上面传的是字符串,下面是对象,对象被序列化了。 这是为什么呢?
因为rabbitmq里面自动配置的就是将对象转换成流,如果你没有配置相关的规则的话。
就是因为这个
他会把对象转换成字节流。
所以我们可以换一种方式,
这个messageconverter是接口类型的,ctrl+h,显示出他的实现类。
接下来,我们添加一个新的配置类,用json的转换器。
@Configuration
public class RabbitMQConfig {
public MessageConverter getMessageConverter(){
return new Jackson2JsonMessageConverter();
}
}
竟然还是这个?仔细一看,竟然是没有加@bean
加上之后再来
终于,变成json格式的了。
然后,就是
接收消息,可以把要接受的消息类型卸载后面,参数会自动接收到消息。
RabbitHandler的使用 可以使用rabbithandnler来接收不同的实体类。
就比如,你发请求的时候,发送的实体类不一样,但是都发的是那个队列。 那接收的时候呢?不好接收,这个时候就可以用rabbithandler。
用它来重载一个方法。
这里没有手敲,直接截的图。
@rabbitListener是用在类或方法上,监听那个类的消息。 @RabbitHandler用在方法上,可以重写接收的参数。
消息确认机制
如果rabbitmq发送消息失败了怎么办呢?这里引入了rabbitmq的消息确认机制。
官网上说,可以使用事务的操作,但是会是性能下降250倍。
当p发送到broker的时候,会有一个确认回调。
交换机也就是e发送到queue之后,会再有一个回调也就是returnCallback
可靠抵达
编写配置类
@Configuration
public class RabbitMQConfig2 {
@Resource
RabbitTemplate rabbitTemplate;
/*
* 定制RabbitTemplate
* */
@PostConstruct //myrabbitConfig创建完成后。执行这个方法
public void initRabbitTemplate(){
//确认回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
*
只要消息抵达代理broker 就返回true
* @param correlationData 当前消息的唯一关联数据(这个是消息的唯一id)
* @param b 消息是否收到
* @param s 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
System.out.println("correlationData: [" + correlationData+"] ack ["+b+"] case["+s+"]");
}
});
}
}
这个不能和上一个配置写到一起会有循环注入的问题,所以单独另写了一个配置类。
至于为什么会有这个循环注入的问题,先留个疑问。
然后在application.properties里面配置
#开启发送确认
spring.rabbitmq.publisher-confirm-type=correlated
这个就是开启了确认回调。
然后,再次启动。 我们查看。
发送数据
然后观察到。全部成功。
然后就是设置消息失败的回调
spring.rabbitmq.publisher-returns=true
spring.rabbitmq.template.mandatory=true
/*
* 设置消息抵达队列的确认回调
* */
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
/**
* 只要消息没有投递给指定队列,就出发失败回调
* @param returnedMessage
*/
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
/*
* private final Message message; 投递失败的消息详细信息
private final int replyCode; 回复的状态码
private final String replyText; 回复的文本内容
private final String exchange; 当时这个消息发送给那个交换机
private final String routingKey; 当时这个消息用那个路由键
* */
System.out.println("FailMessage:["+returnedMessage.getMessage()+"] replyCode["+returnedMessage.getReplyCode()
+"] replyText["+returnedMessage.getReplyText()+"] exchange:["+returnedMessage.getExchange()+"] routingKey"+returnedMessage.getRoutingKey()+"]");
}
});
做一些修改,让i%2==0的时候,发送错误的路由键,看看会不会调用回调方法
这个是结果,错误的就会回调错误的方法、
创建用户1
FailMessage:[(Body:'[B@3f685507(byte[80])' MessageProperties [headers={__TypeId__=com.example.springrabbitmqdemo1.entity.User}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0])] replyCode[312] replyText[NO_ROUTE] exchange:[com.bai.exchange] routingKeyhello-java22]
correlationData: [null] ack [true] case[null]
创建用户2
创建用户3
FailMessage:[(Body:'[B@35903e77(byte[80])' MessageProperties [headers={__TypeId__=com.example.springrabbitmqdemo1.entity.User}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0])] replyCode[312] replyText[NO_ROUTE] exchange:[com.bai.exchange] routingKeyhello-java22]
correlationData: [null] ack [true] case[null]
创建用户4
创建用户5
correlationData: [null] ack [true] case[null]
correlationData: [null] ack [true] case[null]
FailMessage:[(Body:'[B@7b904429(byte[80])' MessageProperties [headers={__TypeId__=com.example.springrabbitmqdemo1.entity.User}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0])] replyCode[312] replyText[NO_ROUTE] exchange:[com.bai.exchange] routingKeyhello-java22]
创建用户6
创建用户7
correlationData: [null] ack [true] case[null]
FailMessage:[(Body:'[B@7557b52b(byte[80])' MessageProperties [headers={__TypeId__=com.example.springrabbitmqdemo1.entity.User}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0])] replyCode[312] replyText[NO_ROUTE] exchange:[com.bai.exchange] routingKeyhello-java22]
correlationData: [null] ack [true] case[null]
创建用户8
correlationData: [null] ack [true] case[null]
创建用户9
correlationData: [null] ack [true] case[null]
FailMessage:[(Body:'[B@71682f59(byte[80])' MessageProperties [headers={__TypeId__=com.example.springrabbitmqdemo1.entity.User}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0])] replyCode[312] replyText[NO_ROUTE] exchange:[com.bai.exchange] routingKeyhello-java22]
correlationData: [null] ack [true] case[null]
correlationData: [null] ack [true] case[null]
消费端确认
消费端确认默认是自动确认,只要消息接收到,客户端会自动确认,服务端就会移除这个消息。
那么这么做有什么问题呢?
当我们生产完消息之后,去消费,消费完一个,宕机,那么后续的所有没有消费的消息都就会消失。
也就是说:我们收到很多消息,自动回复ack,只处理成功一个,宕机了,发生消息丢失。
比如
我们启动debug模式。
等待消费的有六十条。
放行两条之后
接下来停止服务
里面的消息没有了。
产生消息丢失现象。
解决办法:
我们可以设置为手动接收
#手动ack
spring.rabbitmq.listener.simple.acknowledge-mode=manual
然后设置接收
@RabbitListener(queues = {"hello-java"})
public void receive(Message msg, User user, Channel channel) throws Exception {
System.out.println("接收到的消息是" + msg + "信息是" + user);
long deliveryTag = msg.getMessageProperties().getDeliveryTag();
System.out.println("deliveryTag ===>" + deliveryTag);
//签收货物,非批量模式
try {
if (deliveryTag % 2 == 0) {
channel.basicAck(deliveryTag, false);
System.out.println("签收了...." + deliveryTag);
}
} catch (Exception e) {
//网络异常
e.printStackTrace();
}
}
也就是说,delivertag%2==0的时候,就不会签收。
看看具体效果。
签收了2,4
终止服务后
发现他没有消失。
if (deliveryTag % 2 == 0) {
channel.basicAck(deliveryTag, false);
System.out.println("签收了...." + deliveryTag);
}else
// long deliveryTag 标签, boolean multiple是否批量去收默认为false,如果设置为true这个货物以前的所有货物全部被拒
// , boolean requeue 是否重新入队,如果为false,则丢弃消息.如果为true,则重新发回服务器,服务器重新入队
channel.basicNack(deliveryTag,false,true);
}
这里也可以做一些其他的操作。 就是如果,没有成功的或可以Nack
总结
延时队列--实现定时任务
延时队列的使用举例:下订单。
生成一个订单之后,30分钟,未支付,关闭订单。40分钟后检查,订单不存在或取消,解锁库存。
为啥是40分钟呢,是怕这个订单的延时,所以晚10m去扫描。
死信队列
Dead Letter Exchanges (DLX) ·一个消息在满足如下条件下,会进死信路由,记住这里是路由而不是队列, 一个路由可以对应很多队列。(什么是死信)
一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不会被再次放在队列里,被其他消费者使用。
(basic.reject/ basic.nack) requeue=false-上面的消息的TTL到了,消息过期了。
一队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信路由上Dead Letter Exchange其实就是一种普通的exchange,和创建其他exchange没有两样。
只是在某一个设置Dead Letter Exchange的队列中有消息过期了,会自动触发消息的转发,发送到Dead Letter Exchange中去。
我们既可以控制消息在一段时间后变成死信,又可以控制变成死信的消息被路由到某一个指定的交换机,结合二者,其实就可以实现一个延时队列。
第一种实现延时队列就是给队列设置过期时间。
第二种实现方式就是给消息设置过期时间。
我们推荐给队列设置过期时间,因为我们的rabbitmq是惰性过期时间。
他会检查第一个过期时间发现是5m,后面的就不检查,5m之后,第一个过期之后,才检查之后的,那后面只有1m,或者2m过期的,就晚了,应该早过期的,结果产生了延时。
所以一般都用第一种,给队列设置过期时间。
具体使用场景
队列自动创建(RabbitMQ 自动创建队列/交换器/绑定_林老师带你学编程的博客-CSDN博客_rabbittemplate创建队列)
/* 容器中的Queue、Exchange、Binding 会自动创建(在RabbitMQ)不存在的情况下 */
/**
* 死信队列
*
* @return
*/@Bean
public Queue orderDelayQueue() {
/*
Queue(String name, 队列名字
boolean durable, 是否持久化
boolean exclusive, 是否排他
boolean autoDelete, 是否自动删除
Map<String, Object> arguments) 属性
*/
HashMap<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", "order-event-exchange");
arguments.put("x-dead-letter-routing-key", "order.release.order");
arguments.put("x-message-ttl", 60000); // 消息过期时间 1分钟
Queue queue = new Queue("order.delay.queue", true, false, false, arguments);
return queue;
}
/**
* 普通队列
*
* @return
*/
@Bean
public Queue orderReleaseQueue() {
Queue queue = new Queue("order.release.order.queue", true, false, false);
return queue;
}
/**
* TopicExchange
*
* @return
*/
@Bean
public Exchange orderEventExchange() {
/*
* String name,
* boolean durable,
* boolean autoDelete,
* Map<String, Object> arguments
* */
return new TopicExchange("order-event-exchange", true, false);
}
@Bean
public Binding orderCreateBinding() {
/*
* String destination, 目的地(队列名或者交换机名字)
* DestinationType destinationType, 目的地类型(Queue、Exhcange)
* String exchange,
* String routingKey,
* Map<String, Object> arguments
* */
return new Binding("order.delay.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.create.order",
null);
}
@Bean
public Binding orderReleaseBinding() {
return new Binding("order.release.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.order",
null);
}
/**
* 订单释放直接和库存释放进行绑定
* @return
*/
@Bean
public Binding orderReleaseOtherBinding() {
return new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.other.#",
null);
}
/**
* 商品秒杀队列
* @return
*/
@Bean
public Queue orderSecKillOrrderQueue() {
Queue queue = new Queue("order.seckill.order.queue", true, false, false);
return queue;
}
@Bean
public Binding orderSecKillOrrderQueueBinding() {
//String destination, DestinationType destinationType, String exchange, String routingKey,
// Map<String, Object> arguments
Binding binding = new Binding(
"order.seckill.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.seckill.order",
null);
return binding;
}
当我们写完之后,重启一下项目,发现
该生成的已经生成了。
但是!但是!但是!
用@bean生成的消息队列,当我们改变了原有的属性的时候,他不会去覆盖已有的队列,也就是说,我们只能手动的去删除 已有的交换机和队列。