消费者手动确认
一般情况下我们是不会使用消费者的自动确认模式的,通常我们会手动确认消息是否消费。
我们使用channel.basicAck或者channel.basicNack 来进行消息的确认
代码示例
public void consumerDirect(){
Connection connection = Common.getConnection();
try {
Channel channel = connection.createChannel();
channel.basicConsume(DIRECT_QUEUE_1,new QueueingConsumer(channel){
@SneakyThrows
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, StandardCharsets.UTF_8);
Thread.sleep(20);
//模拟消费失败
if((Math.random()*100)>90){
retryMsg(channel,properties,envelope,msg);
}else{
System.out.println("消费者确认 " + Thread.currentThread().getId() + " 收到消息" + msg + " 来源交换器:" + envelope.getExchange());
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
使用死信交换器
死信交换器是 RabbitMQ 对 AMQP 规范的一个扩展,往往用在对问题消息的诊断上(主要针对消费者),还有延时队列的功能。
消息变成死信一般是以下三种情况:
- 消息被拒绝,并且设置 requeue 参数为 false
- 消息过期(默认情况下 Rabbit
中的消息不过期,但是可以设置队列的过期时间和消息的过期时间以达到消息过期的效果) - 队列达到最大长度(一般当设置了最大队列长度或大小并达到最大值时)
死信交换器仍然只是一个普通的交换器,创建时并没有特别要求和操作。在创建队列的时候,声明该交换器将用作保存被拒绝的消息即可,相关的
参数是 x-dead-letter-exchange。
代码示例
//声明死信队列
channel.queueDeclare(FANOUT_DLX_QUEUE,true,false,false,null);
// 声明死信交换器,当队列,就会使用这个交换器
channel.exchangeDeclare( FANOUT_DLX_EXCHANGE,"fanout",true,false,null);
//队列绑定,匹配所有的路右键
channel.queueBind(FANOUT_DLX_QUEUE,FANOUT_DLX_EXCHANGE,"#");
Map<String,Object> argsMap = new HashMap<String,Object>();
argsMap.put("x-dead-letter-exchange",FANOUT_DLX_EXCHANGE);
//这里把死信交换器绑定了过来,当消息在一个队列里面变成死信(过期,超出内存,被拒绝不能重新入队)的时候就会进入死信队列
channel.queueDeclare(DIRECT_QUEUE_1,true,false,false,argsMap);
消费者中的注意事项
QOS预取
在消费者进行消费的时候,应当尽量使用QOS预取的模式,默认是没有设置的,也就是无限大,这样会导致消费者一下子接受的数据太多,导致内存溢出
使用示例
public void consumerConfirm(){
Connection connection = Common.getConnection();
try {
Channel channel = connection.createChannel();
//设置QOS预取数量为200,默认是不限制,限制QoS可以减少消费者的压力,避免消息过多直接内存溢出
//测试场景:在设置堆内存为5M的情况下,如果生成这连续发送1000条消息,消费者如果不设置QoS会导致内存溢出
channel.basicQos(100);
channel.basicConsume(DIRECT_CONSUMER_CONFIRM_QUEUE_1,new QueueingConsumer(channel){
@SneakyThrows
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, StandardCharsets.UTF_8);
Thread.sleep(20);
//模拟消费失败
if((Math.random()*100)>90){
retryMsg(channel,properties,envelope,msg);
}else{
System.out.println("消费者确认 " + Thread.currentThread().getId() + " 收到消息" + msg + " 来源交换器:" + envelope.getExchange());
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
保证消息不能重复消费
我们应当尽量在消费者端保证消息的幂等性来避免消息的重复消费
private void dealMsg(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body, Channel channel,String consumer) throws IOException, InterruptedException {
String msg = new String(body, StandardCharsets.UTF_8);
if(isExist(msg)){
System.out.println(consumer+" 重复消费消息:"+msg);
channel.basicAck(envelope.getDeliveryTag(),true);
}else {
System.out.println(consumer + " " + Thread.currentThread().getId() + " 收到消息" + msg + " 来源交换器:" + envelope.getExchange());
//延迟处理
Thread.sleep(500);
channel.basicAck(envelope.getDeliveryTag(), true);
EXIST_SET.add(msg);
}
}
/**
*
* 幂等校验,其方式多样,可以根据数据库的唯一主键,也可以利用缓存等等方式
* @param msg
* @return
*/
private boolean isExist(String msg){
return EXIST_SET.contains(msg);
}
失败重试机制
当消息确认失败的时候,应该进行一定次数的重试,而不是直接丢弃或者重新入队
/**
* @Title:
* @MethodName:
* @param
* @Return
* @Exception
* @Description:
* 消费者确认
* 1、使用basicAck或basicNack手动确认
* 2、使用QoS预取
* 3、进行消息的重试。三次失败的进入死信队列
* @author: jenkin
* @date: 2020-04-11 11:05
*/
public void consumerConfirm(){
Connection connection = Common.getConnection();
try {
Channel channel = connection.createChannel();
//设置QOS预取数量为200,默认是不限制,限制QoS可以减少消费者的压力,避免消息过多直接内存溢出
//测试场景:在设置堆内存为5M的情况下,如果生成这连续发送1000条消息,消费者如果不设置QoS会导致内存溢出
channel.basicQos(100);
channel.basicConsume(DIRECT_CONSUMER_CONFIRM_QUEUE_1,new QueueingConsumer(channel){
@SneakyThrows
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, StandardCharsets.UTF_8);
Thread.sleep(20);
//模拟消费失败
if((Math.random()*100)>90){
retryMsg(channel,properties,envelope,msg);
}else{
System.out.println("消费者确认 " + Thread.currentThread().getId() + " 收到消息" + msg + " 来源交换器:" + envelope.getExchange());
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 失败重试
* @param channel
* @param properties
* @param envelope
* @param msg
* @throws IOException
*/
private void retryMsg(Channel channel, AMQP.BasicProperties properties, Envelope envelope, String msg) throws IOException {
Map<String, Object> headers = properties.getHeaders();
Object retryTimes = headers.get("retryTimes");
int times = retryTimes==null?0:Integer.parseInt(String.valueOf(retryTimes));
System.out.println("重试次数 "+times);
Map<String, Object> propertiesMap = new HashMap<>();
propertiesMap.put("retryTimes", times +1);
AMQP.BasicProperties persistentTextPlain = MessageProperties.PERSISTENT_TEXT_PLAIN.builder().headers(propertiesMap).build();
System.out.println("消费者Nack " + Thread.currentThread().getId() + " 收到消息" + msg + " 来源交换器:" + envelope.getExchange());
if(!(times>=3)) {
channel.basicAck(envelope.getDeliveryTag(), false);
channel.basicPublish(DIRECT_EXCHANGE, DIRECT_CONSUMER_CONFIRM_QUEUE_1, true, persistentTextPlain, ("重试: " + msg).getBytes());
}else{
channel.basicNack(envelope.getDeliveryTag(), false,false);
}
}