消息队列mq
SpringAMQP收发消息
1.引入依赖 spring-boot-starter-ampq
2.配置rabbitmq服务端信息
3.利用RibbitTemplate 发送消息
4.利用@RibbitListener注解申明要监听的队列,监听消息
发送消息
@Autowired
private RabbitTemplate rabbitTemplate
public void testSimpleQueue(){
//队列名称
String queueName = "simple.queue";
//消息
String message = "hello,spring amqp"
//发送消息
rabbitTemplate.convertAndSend(queueNae,message);
}
接受消息
@Component
public class SpringRabbitListener{
//绑定队列
@RabbitListener(queues = "simple.queue")
public void listenSimplequeueMessage(String msg) throws InterruptedException{
log.info("Spring 消费者接受到消息:[" + msg + "]" );
if(true){
throw new essageConversionException("异常");
}
log.info("消息处理完成"):
}
消费者消息推送限制
默认情况下 RabbitMQ会将消息依次轮询投递给绑定在队列上的每一个消费者,但是没有考虑消费者是否已经处理完成消息,可能导致消息堆积
可以修改配置,设置preFetch的值为1,确保同一时刻最多投递给消费者1条信息
spring:
rabbitmq:
listener:
simple:
prefetch: 1
生产环境都会经过exchange来发送消息,而不是直接发送到队列,交换机的类别又三个
Fanout Exchange 广播模式 会将接受到的消息广播到每一个跟其绑定的queue
Direct 交换机 定向路由
- 每一个Queue都与Exchange设置一个BindingKey
- 发布者发送消息时,指定消息RoutingKey
- Exchange将消息路由到BindingKey与消息RountingKey一致的队列
toptic 可匹配交换机
#匹配0个或多个单词
*匹配多个单词
队列交换机创建
用bean直接new 或者用工厂创建
ExchangeBuilder.fanoutExchange("").build;
QueneBuilder.durable();//durable持久化
//简单创建用bean
@Bean
public Binding fanoutBindings(Queue q,FanoutExchange f){
return BindingBuilder.bind(q).to(f); //.with()绑定key
//return BindingBuilder.bind(Queue()).to(FanoutExchange());
}
2简化创建和绑定
基于消费者的RabbitListener注解
@RabbitListener(bindIngs =@QueueBinding(
value= @Queue(name = ""),
exchange = @Exchange(name = "",type = ExchangeTypes.DIRECT),
key = {}
))
public void lestenDirectQueue1(String msg){
System.out.println();
```}
消息转换器
当发送的消息是对象
spring的消息对象的处理是由org.springframework.amqp.support.converter.MessageConveerter 来处理的,而默认实现是SimplemessageConverter,基于JDK的ObjectOutputStream完成序列化
jdk存在的问题
jdk的序列化又安全风险
jdk序列化的消息太大
jdk序列化的消息可读性差
解决
建议采用json序列化替代
在publisher 和consumer中引入依赖
jackson-databind
在publisher 和consumer中配置MessageConveerter
```java
@Bean
public messageConverter messageConverter(){
return new Jackson2JsonMmessageConverter();
}
生产者重连
又得时候由于网络波动,可能会出现客户端连接MQ失败的情况,通过配置我们可以开启链接失败后的重连机制
这中是阻塞式的,会影响性能。
spring:
rabbitmq:
connection-timeout: 1s //设置mq的连接超时时间
template:
retry:
enabled: true // 开启超时重试机制
initiaal-interval: 1000ms //失败后的初始等待时间
multiplier: 1// 失败后下次的等待时间倍数 下次等待时间 = initial-interval * multiplier
max-attempts: 3 //最大重连次数
生产者确认
Rabbitmq 有两种确认机制 publisher confirm 和publisher return
- 消息投递到了mq,但是路由失败,此时回通过publisherReturn返回路由异常,然后返回ack
- 临时消息投递到了mq,并且成功入队,返回ack
- 持久消息投递到了mq,并且入队完成持久化,返回ack
- 其他情况返回nack,投递失败
在publisher微服务中添加配置
spring:
rabbitmq:
publisher-confirm-type : correlated //开启认证机制是设置confirm类型
publisher-returns: true // 开启publisher return 机制
publisher-confirm-type 三种模式
none :关闭
simple: 同步阻塞等待mq的回执消息
correlated: mq异步回调方式返回回执消息
void test() throws InterruptedException{
CorrelationData cd = new CorrelationData();
cd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>(){
@Override
public void onFailure(Throwable ex){
log.error();
}
public void onSuccess(CorrelationData.Confirm result){
'if(result.isAck()){
}else{}
log.error("",result.getReanson());
}
}
}
MQ的可靠性
在默认情况下,RabbitMQ会将接收到的信息保存在内存中意降低消息收发的延迟,但是这样会导致两个问题
一旦MQ掉线,内存的消息会丢失
内存的空间有限,当消费者故障或者处理过慢时,会导致消息积压,引发MQ阻塞
数据持久化
实现持久化有三个方面
交换机持久化
队列持久化
消息持久化
LazyQueue
从MQ的3.6.0版本开始,增加了LazyQueue概念
接收到消息后直接存入磁盘而非内存
消费者要消费消息时才会从磁盘读取并加载到内存
支持数百万条的额消息存储
3.12版本后所有队列都是LazyQueue 无法更改
设置一个队列为惰性队列
@Bean
public Queue LazyQueue(){
return QueueBuilder
.durable("lazy.queue")
.lazy()
.build();
}
@RabbitListener(queueToDeclare = @Queue(
name = "lazy.queue",
durable = "true",
arguments = @Argument(name = "x-queue-mode", value = "lazy"))
public void listenLazyQueue(String msg){
log.info();
}
开启持久化和生产者确认时,RabbitMQ只有在消息持久化完成后才会给生产者返回ACK回执
消费者确认
当消费者处理消息结束后,应该向RabbitMQ 发送一个回执,告知RabbitMQ
ack:当成功处理消息,RabbitMQ从队列中删除该消息
nack:消息处理失败,RabbitMQ需要再次投递消息
reject:消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息
springAMQP已经实现了消息确认功能,并允许我们通过配置文件选择ack处理方式 设置
spring:
rabbitmq:
listener:
simple:
acknowledge-mode : auto
none:不处理,
manual:手动模式
auto:自动模式,SpringAMQP利用对我们的消息处理逻辑做了环绕增强,当业务正常执行时则自动返回ack
当业务异常时,根据异常判断返回不同的结果
如果是业务异常,返回nack
如果是消息处理或校验异常,自动返回reject
失败重试机制
在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MwssageRecover接口来处理,他包含三种不同的实现
RejectAndDontRequeueRecoverer: 重试耗尽后,直接reject,丢弃消息,默认
ImmediateRequeuMessageRecoverer:重试耗尽后,返回nack,消息重新入队
RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机
定义RepulishMessageRecoverer
@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
return new RepulishMessageRecoverer(rabbitTemplate,"error.direct","error");
消费者如何保证消息一定被消费
- 开启消费者确认机制为auto,由spring确认消息处理成功后返回ack,异常时返回nack
- 开启消费者失败重试机制,并设置MessageRecoverer,多次重试失败后将消息投递到异常交换机,交由人工处理
业务幂等性
消费者收到同意消息,只进行一次处理
方案-1
给每一个消息都设置一个唯一的id,与消息一起投递给消费者
消费者接受到消息后处理自己的业务,业务处理成功后将消息id保存到数据库
如果下次有收到相同的消息,去数据库验证是否存在,存在则为重复消息放弃处理
@Bean
public MessageConvertoer messageConverter(){
//定义消息转换器
Jackson2JsonMessageConverter jjmc = new Jackson2JsonMessageConverter();
jjmc.setCreateMessageIds(true);
return jjmc;
}
方案二,结合业务逻辑,基于业务本身做判断,修改订单状态前先查询订单状态,做判断
如何保证支付服务与交易服务之间的订单状态一致性
首先,支付服务会在支付成功后利用mq同志交易服务完成订单状态同步
为了保证mq消息的可靠性,我们采用生产者确认机制,消费者确认,消费者失败重试等策略,确保消息传递和处理的可靠性,同时开启mq的持久化避免消息丢失
最后 在交易服务更新订单时做业务幂等性判断,避免因消息重复导致的订单状态异常
当交易服务消息处理失败
实现RepublishMessageRecoverer失败消息绑定交换机,人工处理
我们可以在交易服务设置定时任务,springtask 定期查询订
延迟消息
延时消息
延迟任务
用来解决支付订单一直支付状态
死信交换机
死信:
消费者使用basic.reject或者basic.nack声明消费失败,并且消息的requeue参数设置为false
消息是个过期消息(达到了队列或消息本身设置的过期时间)无人消费
要投递的队列消息堆积满了,最早的消息可能成为死信
如果队列通过dead-letter-exchange属性指定了一个交换机,那么该队列中的死信就会投递到这个交换机中
@RabbitListener(bindings = @QueueBinding(value = @Queue(name = "",durable = "true"),
exchangge = @Exchange(nae = "delay.direct",delayed = "true"),
key ="deley"
))
pbulic void listenDelayMessage(String sg){
log.info("",msg)
}
或者使用bean的方式
public DirectExchange delayExchange(){
return ExchangeBuilder
.directExchange("delay.direct")
.delayed(true)
.durable(true)
.build();
}
延迟消息插件
发送消息时需要通过消息头x-delay来设置过期时间
@Test
void testPublisherDelayMessage(){
String message = "";
rabbitTemplate.convertAndSend("delay.direct","delay",message, new MessagePostProcessor(){
@Override
public Message postProcessage(Message message) throws AmqpException{
message.getMessageProperties().setDelay(5000);
return message;
}
});
取消超时订单
设置30分钟后检测订单支付状态实现存在问题
如果并发较高,30分钟可能堆积消息过多,对mq的压力很大
大多数的订单下单1分钟内就会支付,但是要在mq等待30分钟,浪费资源
优化,可以将30分钟分成几十次
比如先10s 检验一次,然后10s继续校验 ,10s 15s …
这样使的留在mq中的消息就很少