生产者可靠
某些情况会发送失败,交换机写错了,key写错了等等
mq有回退模式和确认模式,回退模式return当放入队列中会调用return回调,确认模式confirm不保证放入到队列,当放入broker(进入mq服务)交换机就会调用confirm回调返回ack,unack
开启确认机制:
编写回调方法,然后就可以记录日志或者把这条消息放到db,并保存错误原因
消费者可靠
确认机制
Mq由消费者确认机制:
默认是none,发到了直接销毁
mq默认消费者收到消息就销毁消息是不安全的,所以使用手动ack的方式告诉Mq我成功了,可以销毁消息了
首先新建一个测试队列test.queue
生产者:
消费者:
此时所有消息都会发生异常,然后重新投递到队首,也就是说,即使你就往这个队列发了1条消息,那么由于发生异常会重新入队,且在队头,那么又继续消费这条消息,又会发生异常,就会发生死循环。
手动nack,指定死信交换机
于是针对这个问题,我们可以将该队列绑定一个死信交换机,设置requeue参数(第三参数)为false,那么就不会重新入队,转而投递到死信交换机
死信交换机设置,首先设置好死信交换机和队列dlx.direct,dlx.queue,并指定key(我这里就是设置的dead),然后在创建队列时绑定死信交换机,并指定新的key(dead)以在私信交换机中路由
手动nack
这样所有失败的消息都会发到死信队列,然后就可以搞一个消费者去消费这些异常信息,比如可以将信息放入db,定时任务重新发送,然后重新发送也可能再失败,那么加个字段重试次数,达到一定次数就报警,人工处理
使用task表控制幂等
加一个task表,消费时,先往task插入消息,初始status为create,且msg_id加唯一索引,一旦重复消费就抛出异常处理,接着就执行业务代码,如果没有问题就标记task状态为success,一旦有问题就标记为fail,然后人工处理
redis实现短时间重试
@Slf4j
@Component
public class TestListener {
@Resource
RedissonClient redissonClient;
@RabbitListener(queues = "test.queue")
public void receive(Message message, Channel channel) throws Exception {
//SolutionEnum.RETRY.process(message,channel,redissonClient);
SolutionEnum.DIEMSG.process(message,channel,redissonClient);
}
}
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Slf4j
enum SolutionEnum{
RETRY(1,"重试"){
@Override
public void process(Message message, Channel channel,RedissonClient redissonClient) throws Exception {
String jsonStr=new String(message.getBody(),"UTF-8");
Msg bean = JSONUtil.toBean(jsonStr, Msg.class);
Long id = bean.getMsgId();
log.info("消息:{}",jsonStr);
try{
//redis校验幂等
boolean lock = redissonClient.getBucket("mq_msg_lock:msg_id:" + id).setIfAbsent(1, Duration.ofSeconds(5));
if(lock){
//业务
int i=1/0;
}else{
log.info("重复消费");
}
}catch (Exception e){
//重试前判断
Long res= redissonClient.getAtomicLong("mq_error_message_retry_times:message_id:"+id).incrementAndGet();
redissonClient.getAtomicLong("mq_error_message_retry_times:message_id:"+id).expire(Duration.ofSeconds(10));
log.error("当前重试次数:{}",res-1);
if(res>=5+1){//重试5次
MessageProperties messageProperties = message.getMessageProperties();
log.error("重试失败,消息:{},exchange:{},key:{},队列:{}",
jsonStr,
messageProperties.getReceivedExchange(),messageProperties.getReceivedRoutingKey(),
messageProperties.getConsumerQueue());
//直接ack
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
//保存db,告警
}else{
//继续重试
redissonClient.getBucket("mq_msg_lock:msg_id:" + id).delete();//释放锁,否则重试不了
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
}
},
DIEMSG(2,"走死信交换机"){
@Override
public void process(Message message, Channel channel,RedissonClient redissonClient) throws Exception {
try{
//业务
int i=1/0;
}catch (Exception e){
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
}
}
};
private int code;
private String desc;
public abstract void process(Message message, Channel channel,RedissonClient redissonClient) throws Exception;
}
幂等处理
各种各样的原因,可能会导致短时间内一条消息重复消费多次,那么此时我们就需要在消息内加一个唯一标识,然后在消费的时候,先加锁,保证在短时间内这条消息只会被消费一次,如果是插入操作那么可以在db加唯一索引