RabbitMQ 消息可靠性投递(实际项目改造)

RabbitMQ 消息可靠性投递(实际项目改造)

1.前言

目前业务系统,RabbitMQ出现了如下两个问题

  • 消息没有持久化机制
  • 消息传递的可靠性不高

业务要求不能丢失一条消息!!!

2.解决方案

2.1 rabbitmq 消息传递的流程图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QJpXVfJX-1578474138039)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200107100439728.png)]

从上图可知,在以下 4 个地方有可能出现消息丢失的情况

  • ①表示 消息从生产者发送到 Broker(rabbitmq服务器)

  • ②表示 消息从 Exchange(交换机) 路由到 Queue

  • ③表示 消息存储在 Queue 中,是存储在内存中,重启消息丢失

  • ④表示 消费者订阅 Queue 并消费消息

2.2 第①处修改

采用服务端确认方式

rabbitmq 服务端确认收到消息;分为两种模式

第一:Transaction模式
  • 阻塞

  • 性能低

  • 严重浪费服务器资源

第二:Confirm模式又分为三种
  • 发送一条,确认一条(性能低)

  • 发送N条,批量确认(N这个值不好把握),一条失败,这N条全部重新发送

  • 异步确认(推荐,每次批量确认的条数不一样)

小结

根据目前业务的特性,大量的消息会发送到 rabbitmq 服务端,且数量不固定,选择性能和可靠性适中的异步确认模式

生产者端RabbitMQConfig配置类的代码修改

  • template.setConfirmCallback()
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate template = new RabbitTemplate(connectionFactory);

        //异步确认服务端是否收到消息 如果未收到抛出异常 会重新发送消息
        template.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                if (!ack) {
                    System.out.println("发送消息失败:" + cause);
                    throw new RuntimeException("发送异常:" + cause);
                }
            }
        });

        template.setMessageConverter(new Jackson2JsonMessageConverter());

        return template;
    }

2.3 第②处修改

路由保证

在第二步中,交换机的消息有可能不能正确路由到对应的队列上,比如 路由键 错误或者队列不存在等等

  • 可以设置消息会回发到生产者服务端,再进行后续处理(推荐)

  • 也可以设置备份交换机(本文并未采用,修改量相对比较复杂)

小结

采用消息回发

原因:比较简单

生产者端的代码修改

  • template.setMandatory(true);
  • template.setReturnCallback()
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate template = new RabbitTemplate(connectionFactory);
        //路由失败 回发消费者端
        template.setMandatory(true);
        template.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            public void returnedMessage(Message message,
                                        int replyCode,
                                        String replyText,
                                        String exchange,
                                        String routingKey) {
                service.insert("replyCode: " + replyCode + "replyText: " + replyText + "exchange: " + exchange + "routingKey: " + routingKey,
                        1, new String(message.getBody()));
            }
        });

        //异步确认服务端是否收到消息 如果未收到抛出异常 会重新发送消息
        template.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                if (!ack) {
                    System.out.println("发送消息失败:" + cause);
                    throw new RuntimeException("发送异常:" + cause);
                }
            }
        });

        template.setMessageConverter(new Jackson2JsonMessageConverter());

        return template;
    }

建议将回发的数据写入数据库,后续处理的代码,有数据之后再进行开发也可以

service.insert() 为插入数据库的代码
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dHWWIo1c-1578474138041)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200108163725441.png)]

2.4 第③处修改

持久化

持久化设置好之后,即使 rabbitmq 或者 web 应用挂掉了,重启相关应用队列中的消息还在,而且可以重新消费

  • 队列的持久化 (消费者配置类)

    • 第二个参数true表示持久化
        @Bean("employeeQueue")
        public Queue employeeQueue() {
            // 参数: 队列名称 是否持久化 是否独占 是否自动删除
            return new Queue("employee.queue", true, false, false);
        }
    
  • 交换机的持久化 (消费者配置类)

    • 第二个参数true表示持久化
        @Bean("mobileExchange")
        public DirectExchange exchange() {
            // 参数: 队列名称 是否持久化是否自动删除
            return new DirectExchange("mobile.direct", true, false);
        }
    
  • 消息的持久化 (生产者配置类)

    • 这个比较复杂,本文在实际生产环境做了一个封装类 MessageHelper
    • 关键代码 messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);设置消息的持久化
    public class MessageHelper {
    
        public static <T> T msgToObj(Message message, Class<T> clazz) {
            if (null == message || null == clazz) {
                return null;
            }
    
            String str = new String(message.getBody());
            T obj = JSONObject.parseObject(str, clazz);
    
            return obj;
        }
    
        public static <T> List<T> msgToList(Message message, Class<T> clazz) {
            if (null == message || null == clazz) {
                return null;
            }
            String str = new String(message.getBody());
            List<T> obj = JSONObject.parseArray(str, clazz);
            return obj;
        }
    
        public static Message objToMsg(Object obj) {
            if (null == obj) {
                return null;
            }
            MessageProperties messageProperties = new MessageProperties();
            messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
            messageProperties.setContentType(MessageProperties.CONTENT_TYPE_JSON);
            String str = JSONObject.toJSONString(obj);
            return new Message(str.getBytes(), messageProperties);
        }
    
    }
    
    • 发送消息代码修改
      • 注意 RabbitTemplate
      • rabbitTemplate.send方法
      • MessageHelper.objToMsg将原对象leaders转为Message对象
    @Service
    public class LeaderProducer implements ILeaderProducer {
    
    	@Autowired
    	RabbitTemplate rabbitTemplate;
    	
    	@Override
    	public void sendSaveQuotaScore(List<Leader> leaders) {
    		rabbitTemplate.send("mobile.direct", "employee", MessageHelper.objToMsg(leaders));
    	}
        
    }
    

2.5 第④处修改

消费者确认
代码意义
NONE自动应答
MANUAL手动应答
AUTO自动应答(在程序运行完)

AUTO有点区别

1.没有发生异常,正常 ACK 答应

2.手动抛出AmqpRejectAndRequeueException nack , requeue 拒绝,重回队列

3.手动抛出AmqpRejectAndDontRequeueException nack,不重新入队

消费者端修改

本文采用的 MANUAL, 监听类配置,设置为手动提交factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);

    @Bean
    public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        factory.setAutoStartup(true);
        return factory;
    }
监听类修改
  • @RabbitListener设置containerFactory = "rabbitListenerContainerFactory"
  • channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false); 表示手动确认
  • 出现异常,将信息插入到数据库
    @RabbitListener(queues = "employee.queue", containerFactory = "rabbitListenerContainerFactory")
    public void saveQuotaComment(Message msg, Channel channel) throws IOException {
        try {
            List<Leader> leaders = MessageHelper.msgToList(msg, Leader.class);
            leaderMapper.batchAddLeader(leaders);
        } catch (Exception e) {
            exceptionExecute(e, msg);
        } finally {
            channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false);
        }
    }

    void exceptionExecute(Exception e, Message msg) {
        //将异常信息插入到数据库
        String str = new String(msg.getBody());
        StackTraceElement[] stackTrace = e.getStackTrace();
        String exceptionContent = "";
        for (int i = 0; i < stackTrace.length; i++) {
            exceptionContent += stackTrace[i].toString();
        }
        exceptionMapper.insert(e.getMessage() + " >> " + exceptionContent.substring(0, 1000), 2, str);
        e.printStackTrace();
    }

3.集群

建议有条件的可以使用 HAproxy + keepalived+ 多个 rabbitmq 节点

4.总结

经过上面的 4 步可以确保每一条消息都被正确的消费,即使有异常也会被捕获,将异常信息和原始消息信息保存,以便后续处理。

如果还不放心,可以做一个数据核对的模块,在发送消息的时候,将主键插入日志表,在消息被消费完成的时候,再插入一条,两者进行对比。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值