原因是线上一场时间不精准问题导致的。
总的来说,为了让消息队列消息更加健壮,于是配置了超时时间和死信队列。但是出现的问题是,配置队列的TTL,总有一些消息在超过TTL时间后,进入不了死信队列,影响及时的业务通知系统。
问题在什么地方呢?
prefetch: 1
属性配置上。
以下是问题重现,与解决过程
消息队列配置类:
@Configuration
public class RabbitMQConfig {
public static final String BUSINESS_EXCHANGE_NAME = "letter.demo.simple.business.exchange";
public static final String BUSINESS_QUEUEA_NAME = "letter.demo.simple.business.queuea";
public static final String BUSINESS_QUEUEB_NAME = "letter.demo.simple.business.queueb";
public static final String DEAD_LETTER_EXCHANGE = "letter.demo.simple.deadletter.exchange";
public static final String DEAD_LETTER_QUEUEA_ROUTING_KEY = "letter.demo.simple.deadletter.queuea.routingkey";
public static final String DEAD_LETTER_QUEUEB_ROUTING_KEY = "letter.demo.simple.deadletter.queueb.routingkey";
public static final String DEAD_LETTER_QUEUEA_NAME = "letter.demo.simple.deadletter.queuea";
public static final String DEAD_LETTER_QUEUEB_NAME = "letter.demo.simple.deadletter.queueb";
// 声明业务Exchange
@Bean("businessExchange")
public FanoutExchange businessExchange(){
return new FanoutExchange(BUSINESS_EXCHANGE_NAME);
}
// 声明死信Exchange
@Bean("deadLetterExchange")
public DirectExchange deadLetterExchange(){
return new DirectExchange(DEAD_LETTER_EXCHANGE);
}
// 声明业务队列A
@Bean("businessQueueA")
public Queue businessQueueA(){
Map args = new HashMap<>(2);
// x-dead-letter-exchange 这里声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
// x-dead-letter-routing-key 这里声明当前队列的死信路由key
args.put("x-dead-letter-routing-key", DEAD_LETTER_QUEUEA_ROUTING_KEY);
// 设置队列最大存活时间
args.put("x-message-ttl", 6000);
return QueueBuilder.durable(BUSINESS_QUEUEA_NAME).withArguments(args).build();
}
// 声明业务队列B
@Bean("businessQueueB")
public Queue businessQueueB(){
Map args = new HashMap<>(2);
// x-dead-letter-exchange 这里声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
// x-dead-letter-routing-key 这里声明当前队列的死信路由key
args.put("x-dead-letter-routing-key", DEAD_LETTER_QUEUEB_ROUTING_KEY);
args.put("x-message-ttl", 6000);
return QueueBuilder.durable(BUSINESS_QUEUEB_NAME).withArguments(args).build();
}
// 声明死信队列A
@Bean("deadLetterQueueA")
public Queue deadLetterQueueA(){
return new Queue(DEAD_LETTER_QUEUEA_NAME);
}
// 声明死信队列B
@Bean("deadLetterQueueB")
public Queue deadLetterQueueB(){
return new Queue(DEAD_LETTER_QUEUEB_NAME);
}
// 声明业务队列A绑定关系
@Bean
public Binding businessBindingA(@Qualifier("businessQueueA") Queue queue,
@Qualifier("businessExchange") FanoutExchange exchange){
return BindingBuilder.bind(queue).to(exchange);
}
// 声明业务队列B绑定关系
@Bean
public Binding businessBindingB(@Qualifier("businessQueueB") Queue queue,
@Qualifier("businessExchange") FanoutExchange exchange){
return BindingBuilder.bind(queue).to(exchange);
}
// 声明死信队列A绑定关系
@Bean
public Binding deadLetterBindingA(@Qualifier("deadLetterQueueA") Queue queue,
@Qualifier("deadLetterExchange") DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(DEAD_LETTER_QUEUEA_ROUTING_KEY);
}
// 声明死信队列B绑定关系
@Bean
public Binding deadLetterBindingB(@Qualifier("deadLetterQueueB") Queue queue,
@Qualifier("deadLetterExchange") DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(DEAD_LETTER_QUEUEB_ROUTING_KEY);
}
}
业务监听类:
@Slf4j
@Component
public class BusinessMessageReceiver {
@RabbitListener(queues = BUSINESS_QUEUEA_NAME)
public void receiveA(Message message, Channel channel) throws IOException {
/**
* 模拟休息
*/
try {
Thread.sleep(1000*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String msg = new String(message.getBody());
log.info("收到业务消息A:{}", msg);
boolean ack = true;
Exception exception = null;
try {
if (msg.contains("deadletter")){
throw new RuntimeException("dead letter exception");
}
} catch (Exception e){
ack = false;
exception = e;
}
if (!ack){
log.error("消息消费发生异常,error msg:{}", exception.getMessage(), exception);
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
} else {
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
@RabbitListener(queues = BUSINESS_QUEUEB_NAME)
public void receiveB(Message message, Channel channel) throws IOException {
/**
* 模拟休息
*/
try {
Thread.sleep(1000*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("收到业务消息B:" + new String(message.getBody()));
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
死信队列监听类:
@Component
public class DeadLetterMessageReceiver {
@RabbitListener(queues = DEAD_LETTER_QUEUEA_NAME)
public void receiveA(Message message, Channel channel) throws IOException {
System.out.println("收到死信消息A:" + new String(message.getBody()));
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
@RabbitListener(queues = DEAD_LETTER_QUEUEB_NAME)
public void receiveB(Message message, Channel channel) throws IOException {
System.out.println("收到死信消息B:" + new String(message.getBody()));
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
消息发送类:
@Component
public class BusinessMessageSender {
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendMsg(String msg){
rabbitTemplate.convertSendAndReceive(BUSINESS_EXCHANGE_NAME, "", msg);
}
}
消息发送接口:
@RequestMapping("rabbitmq")
@RestController
public class RabbitMQMsgController {
@Autowired
private BusinessMessageSender sender;
@RequestMapping("sendmsg")
public void sendMsg(String msg){
sender.sendMsg(msg);
}
}
application.yml:
server:
port: 8088
spring:
rabbitmq:
host: 192.168.83.11
username: nc
password: nc
virtual-host: /nc
listener:
type: simple
simple:
default-requeue-rejected: false
acknowledge-mode: manual
application:
name: miaosha-service
datasource:
url: jdbc:mysql://localhost:3306/ttt?useUnicode=true&characterEncoding=UTF-8
username: root
password: root
hikari:
max-lifetime: 28830000 # 一个连接的生命时长(毫秒),超时而且没被使用则被释放(retired),缺省:30分钟,建议设置比数据库超时时长少30秒,参考MySQL wait_timeout参数(show variables like '%timeout%';)
maximum-pool-size: 9 # 连接池中允许的最大连接数。缺省值:10;推荐的公式:((core_count * 2) + effective_spindle_count)
redis:
host: 192.168.83.11
pom:
nc-item
com.nc.item
1.0.0-SNAPSHOT
4.0.0
nc-test-sha
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.boot
spring-boot-starter-web
org.mybatis.spring.boot
mybatis-spring-boot-starter
tk.mybatis
mapper-spring-boot-starter
com.github.pagehelper
pagehelper-spring-boot-starter
org.springframework.boot
spring-boot-starter-jdbc
mysql
mysql-connector-java
com.nc.item
nc-item-interface
1.0.0-SNAPSHOT
org.springframework.boot
spring-boot-starter-actuator
com.nc.common
nc-common
1.0.0-SNAPSHOT
org.springframework.boot
spring-boot-starter-amqp
org.springframework.boot
spring-boot-starter-test
com.alibaba
fastjson
1.2.54
org.projectlombok
lombok
provided
1.16.22
说明,其实这里配置了TTL过期时间是6s。客户端连接后,进行线程暂停,时间超过TTL时间。于是会发现,应该转移到死信队列的消息,一直停留在原队列中。
点击查看消费者消息:
也就是说,你认为消息应该死了,但是实际上它还驻留在原队列中没有被消费。
当监听者进行消费时,如果出现了系统资源紧张,程序出错,长期hang住,那么死信队列的消息就不能及时让死信队列监听者消费,影响后续业务逻辑。
可问题是,为什么这些TTL的消息不进入配置的死信队列呢?
这个你自己复习一下好了,我不再赘述
rabbitmq默认的参数是250,也就是说监听者会一次性抓取250条消息进行批量消费,这样效率更高
我们实验场景消息队列基本没有积压,就是1,2条消息。客户端再消费第一条消息的时候,进行了休眠,导致后面的消息在同一个package中,说消费完了,也没有,一直就是在消费中的状态。。。但是也一直没有ack掉。所以,你的消息一直进入不了死信队列。
适当的调整prefetch参数
本例中,将prefetch=1设置上,基本不会出现问题了
那是不是,业务中要设置成为1呢?很显然,不是的。因为你要兼顾效率呀。
千万不要人云亦云,一定要上手实践,多看官网。