深入解析RabbitMQ,实现发送安全,手动ACK,错误数据持久化。队列不同时段重试

最近挺忙的,时间有限我就不写原理了,直接贴代码。是最近项目需要我整理的。

yml配置

  #rabbit配置
spring:
  rabbitmq:
    host: 172.17.236.101
    port: 5672
    username: XXX
    password: XXX
#rabbit扩展配置配置
spring:
  rabbitmq:
    virtual-host: /
    # 开启发送确认
    publisher-confirms: true
    # 开启发送失败退回
    publisher-returns: true
    listener:
      simple:
        retry:
          enabled: false #消费者端的重试
        acknowledge-mode: manual         # 开启ACK
        auto-startup: true  #启动时自动启动容器	true

发送确认配置

/**
 * 设置rabbit配置
 * @author 大仙
 */
@Component
@Slf4j
public class RabbitTemplateConfig implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback{

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    @PostConstruct
    public void init(){
        rabbitTemplate.setConfirmCallback(this::confirm);
        rabbitTemplate.setReturnCallback(this::returnedMessage);
    }


    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        //发送成功
        if(ack){
            //不做处理,等待消费成功,清楚缓存
            log.info(correlationData.getId()+":发送成功");
        }else{
            //持久化到数据库
            log.error(correlationData.getId()+":发送失败");
            log.info("备份内容:"+redisTemplate.opsForValue().get(correlationData.getId()));

        }
        //不管成功与否读删除redis里面备份的数据
        redisTemplate.delete(correlationData.getId());
    }

    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        log.error("消息主体 message : "+message);
        log.error("描述:"+replyText);
        log.error("消息使用的交换器 exchange : "+exchange);
        log.error("消息使用的路由键 routing : "+routingKey);
    }
}

config配置:

@Configuration
public class RabbitConfig{
    /**
     * direct直连模型
     * fanout无路由模式,使用场景广播消息
     * topic 模糊路由模式,适用业务分组
     * fanout>direct>topic 这里是多消费模式,topic和fanout都能实现,通过性能对比选择fanout  11>10>6
     */
    /**
     * 消息交换机的名字
     * */
    public static final String ORDER_FANOUT_EXCHANGE = "order.success";
    /**
     * 队列的名字
     * */
    public static final String ORDER_FANOUT_QUEUE = "order.success.order";

    /**
     * 1.队列名字
     * 2.durable="true" 是否持久化 rabbitmq重启的时候不需要创建新的队列
     * 3.auto-delete    表示消息队列没有在使用时将被自动删除 默认是false
     * 4.exclusive      表示该消息队列是否只在当前connection生效,默认是false
     */
    @Bean
    public Queue fanoutOrderQueue() {
        return new Queue(ORDER_FANOUT_QUEUE,true,false,false);
    }
    /**
     * 1.交换机名字
     * 2.durable="true" 是否持久化 rabbitmq重启的时候不需要创建新的交换机
     * 3.autoDelete    当所有消费客户端连接断开后,是否自动删除队列
     */
    @Bean
    public FanoutExchange fanoutOrderExchange(){
        return new FanoutExchange(ORDER_FANOUT_EXCHANGE,true,false);
    }
    /**
     * 绑定
     * @return
     */
    @Bean
    public Binding bindingFanoutOrderQueue() {
        return BindingBuilder.bind(fanoutOrderQueue()).to(fanoutOrderExchange());
    }

    //########################################渠道数据汇总开始########################################################//
    /**
     * 渠道活动数据统计
     */
    public final static String EXCHANGE_ACTIVITY_COLLECT = "activity.collect";
    public final static String ROUTING_ACTIVITY_COLLECT = "activity.collect";
    public final static String QUEUE_ACTIVITY_COLLECT_CMP = "activity.collect.cmp";
    /**
     * 渠道活动统计
     */
    @Bean
    public DirectExchange exchangeActivityCollect() {
        return new DirectExchange(EXCHANGE_ACTIVITY_COLLECT);
    }

    @Bean
    public Queue queueActivityCollect() {
        return new Queue(QUEUE_ACTIVITY_COLLECT_CMP);
    }

    @Bean
    public Binding routingActivityCollect() {
        return BindingBuilder
                .bind(queueActivityCollect())
                .to(exchangeActivityCollect())
                .with(ROUTING_ACTIVITY_COLLECT);
    }
    //########################################渠道数据汇总结束########################################################//

}

抽象实体配置:

/**
 * @Author: 朱维
 * @Date 11:42 2019/12/10
 */
@Data
public class MqMessage implements Serializable {
    /**
     * 消息ID
     */
    protected  String id = UUID.randomUUID().toString();
}

发送业务接口配置

/**
 * @Author: 朱维
 * @Date 15:50 2019/12/10
 */
public interface ProducerService {
    /**
     * 发送消息
     * @param content
     * @param exchangeName
     * @param routingKey
     */
    void sendMsg(MqMessage content, String exchangeName, String routingKey);
}

发送业务实现类

/**
 * @Author: 朱维
 * @Date 15:51 2019/12/10
 */
@Service
public class ProducerServiceImpl implements ProducerService {


    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    @Override
    public void sendMsg (MqMessage content, String exchangeName, String routingKey){
        Message message = MessageBuilder.withBody(JSONObject.toJSONString(content).getBytes())
                .setContentType(MessageProperties.CONTENT_TYPE_JSON)
                .setCorrelationId(content.getId()).build();
        CorrelationData data = new CorrelationData(content.getId());
        //存储到redis
        redisTemplate.opsForValue().set(data.getId(),JSONObject.toJSONString(content));
        rabbitTemplate.convertAndSend(exchangeName,routingKey,message,data);
    }
}

接收监听抽象类

/**
 * 消费者
 * @author 大仙
 */
public abstract class AbstractReceiver {


    protected static Logger logger = LoggerFactory.getLogger(AbstractReceiver.class);

    @Autowired
    protected ReceiveService receiveService;

    /**
     * 业务执行方法
     * @param content
     */
    protected abstract void execute(JSONObject content)throws Exception;

    /**
     * 失败执行
     * @param content
     */
    protected void failExecute(JSONObject content){
        logger.info("开始失败补偿======");
    }

    protected void receiveMessage(Message message, Channel channel) throws IOException{
        /**
         * 防止重复消费,可以根据传过来的唯一ID先判断缓存数据库中是否有数据
         * 1、有数据则不消费,直接应答处理
         * 2、缓存没有数据,则进行消费处理数据,处理完后手动应答
         * 3、如果消息处理异常则,可以存入数据库中,手动处理(可以增加短信和邮件提醒功能)
         */
        try{
            JSONObject content = receiveService.getContent(message);
            //已经消费,直接返回
            if(receiveService.canConsume(content,message.getMessageProperties().getConsumerQueue())){
                logger.info(message.getMessageProperties().getConsumerQueue()+"已经消费过");
                channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
            }else{
                //消费当前消息
                execute(content);
                logger.info(message.getMessageProperties().getConsumerQueue()+"消费成功");
                channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
            }
        }catch (Exception e){
            e.printStackTrace();
             try {
                if(receiveService.dealFailAck(message,channel)){
                    logger.info("回归队列:"+message);
                }else{
                    logger.error("消费失败:"+message);
                    failExecute(receiveService.getContent(message));
                }
            }catch (Exception e1){
                //扔掉数据
                channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
                logger.error("重试消费失败:"+message);
                failExecute(receiveService.getContent(message));
            }
        }
    }

}

接收业务接口

/**
 * @Author: 朱维
 * @Date 18:10 2019/12/10
 */
public interface ReceiveService {

    /**
     * 获取内容
     * @param message
     * @return
     */
    JSONObject getContent(Message message);
    /**
     * 是否能消费,防止重复消费
     * @param content
     * @return
     */
    Boolean canConsume(JSONObject content, String queueName);

    /**
     * 处理ack
     * @param message
     * @param channel
     */
    Boolean dealFailAck(Message message, Channel channel) throws IOException, InterruptedException;
}

接收业务实现

/**
 * @Author: 朱维
 * @Date 18:11 2019/12/10
 */
@Service
@Slf4j
public class ReceiveServiceImpl implements ReceiveService {

    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    private static final String ENCODING = Charset.defaultCharset().name();

    private static final SerializerMessageConverter SERIALIZER_MESSAGE_CONVERTER = new SerializerMessageConverter();

    @Override
    public JSONObject getContent(Message message) {
        String body = getBodyContentAsString(message);
        JSONObject content = JSONObject.parseObject(body);
        return content;
    }

    @Override
    public Boolean canConsume(JSONObject content,String queueName) {
        if(redisTemplate.opsForValue().get(queueName+":"+content.getString("id"))==null){
            return false;
        }else{
            //存储消费标志
            redisTemplate.opsForValue().set(queueName+":"+content.getString("id"),"1",30, TimeUnit.MINUTES);
            return true;
        }
    }


    /**
     * 消息异常的时候处理
     * @param message
     */
    @Override
    public Boolean  dealFailAck(Message message,Channel channel) throws IOException, InterruptedException{
        JSONObject content = getContent(message);
        //单个消息控制
        String redisCountKey = "retry"+message.getMessageProperties().getConsumerQueue()+content.getString("id");
        String retryCount = redisTemplate.opsForValue().get(redisCountKey);
        long basic = 1000L;
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        //队列控制
        String queueKey = "retry"+message.getMessageProperties().getConsumerQueue();
        //没有重试过一次
        if(StringUtils.isBlank(retryCount)){
            if(!redisTemplate.opsForValue().setIfAbsent(queueKey,"lock")) {
                channel.basicNack(deliveryTag,false,true);
                log.info("deliveryTag:"+deliveryTag);
                return true;
            }
            redisTemplate.opsForValue().set(redisCountKey,"1");
            log.info("开始第一次尝试:");
        }else{
            switch (Integer.valueOf(retryCount)){
                case 1:
                    Thread.sleep(basic*8);
                    redisTemplate.opsForValue().set(redisCountKey,"2");
                    log.info("开始第二次尝试:");
                    break;
                case 2:
                    Thread.sleep(basic*15);
                    redisTemplate.opsForValue().set(redisCountKey,"3");
                    log.info("开始第三次尝试:");
                    break;
                case 3:
                    Thread.sleep(basic*30);
                    redisTemplate.opsForValue().set(redisCountKey,"4");
                    log.info("开始第四次尝试:");
                    break;
                default:
                    //扔掉消息,准备持久化
                    redisTemplate.delete(redisCountKey);
                    redisTemplate.delete(queueKey);
                    channel.basicNack(deliveryTag,false,false);
                    return false;
            }
        }
        channel.basicNack(deliveryTag,false,true);
        return true;
    }

    /**
     * 获取message的body
     * @param message
     * @return
     */
    private String getBodyContentAsString(Message message) {
        if (message.getBody() == null) {
            return null;
        }
        try {
            String contentType = (message.getMessageProperties() != null) ? message.getMessageProperties().getContentType() : null;
            if (MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT.equals(contentType)) {
                return SERIALIZER_MESSAGE_CONVERTER.fromMessage(message).toString();
            }
            if (MessageProperties.CONTENT_TYPE_TEXT_PLAIN.equals(contentType)
                    || MessageProperties.CONTENT_TYPE_JSON.equals(contentType)
                    || MessageProperties.CONTENT_TYPE_JSON_ALT.equals(contentType)
                    || MessageProperties.CONTENT_TYPE_XML.equals(contentType)) {
                return new String(message.getBody(), ENCODING);
            }
        }
        catch (Exception e) {
            // ignore
        }
        // Comes out as '[[email protected]' (so harmless)
        return message.getBody().toString() + "(byte[" + message.getBody().length + "])";
    }
}

消费者具体实现:

扫描二维码关注公众号,回复: 8609597 查看本文章
@Component
public class OrderPaySuccessListner extends AbstractReceiver {

    @Autowired
    private OrderService orderService;

    @Autowired
    private WeCodeOrderDao weCodeOrderDao;

    @Override
    protected void execute(JSONObject content) throws Exception {
        //处理具体业务
    }


    @Override
    @RabbitListener(queues = RabbitConfig.ORDER_FANOUT_QUEUE)
    protected void receiveMessage(Message message, Channel channel) throws IOException {
        super.receiveMessage(message,channel);
    }
}
发布了149 篇原创文章 · 获赞 36 · 访问量 14万+

猜你喜欢

转载自blog.csdn.net/zhuwei_clark/article/details/103498479