消息队列MQ, rabbitMQ和rocketMQ的实现方式

MQ全称为Message Queue, 消息队列(MQ)是一种应用程序对应用程序的通信方法。应用程序通过读写出入队列的消息(针对应用程序的数据)来通信,而无需专用连接来链接它们。消息传递指的是程序之间通过在消息中发送数据进行通信,而不是通过直接调用彼此来通信,直接调用通常是用于诸如远程过程调用的技术。排队指的是应用程序通过 队列来通信。队列的使用除去了接收和发送应用程序同时执行的要求.

MQ是消费-生产者模型的一个典型的代表,一端往消息队列中不断写入消息,而另一端则可以读取或者订阅队列中的消息。

在项目中,将一些无需即时返回且耗时的操作提取出来,进行了异步处理,而这种异步处理的方式大大的节省了服务器的请求响应时间,从而提高了系统的吞吐量。

基本概念:

Broker:简单来说就是消息队列服务器实体。
Exchange:消息交换机,它指定消息按什么规则,路由到哪个队列。
Queue:消息队列载体,每个消息都会被投入到一个或多个队列。
Binding:绑定,它的作用就是把exchange和queue按照路由规则绑定起来。
Routing Key:路由关键字,exchange根据这个关键字进行消息投递。
vhost:虚拟主机,一个broker里可以开设多个vhost,用作不同用户的权限分离。
producer:消息生产者,就是投递消息的程序。
consumer:消息消费者,就是接受消息的程序。
channel:消息通道,在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务。

rocket MQ的实现:

第一阶段Prepared消息,会拿到消息的地址。
第二阶段执行本地事务,
第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。在业务方法内要向消息队列提交两次请求,一次发送消息和一次确认消息。如果确认消息发送失败了RocketMQ会定期扫描消息集群中的事务消息,这时候发现了Prepared消息,它会向消息发送者确认,所以生产方需要实现一个check接口,RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。

这是rocketmq的实现,但是参考官方问题,极端情况下需要手工介入的。

rabbitmq实现方式:

消息发送一致性:
1.执行本地业务事务,写消息表
2.获取消息表的内容,发送消息
3.消息发送确认成功,则再起事务更新消息表,如果不成功,则不更新。这个会存在这样一个问题:消息已经发送成功,但是rabbit mq没有返回,则无法更新数据库;导致下一次重复发送。这个需要消息接收方要做幂等性检查。

消息接收一致性:
1.rabbit mq发送消息
2.消息接收方接收消息处理,最后返回ack
3.rabbit mq接收到ack确认后,更新消息发送状态。这里有个问题,如果消息接收方成功处理消息,但是由于特殊情况没有返回ack, rabbit mq接收到ack,这条消息状态已经改变不会再发送,需要手工处理。

使用mq本来就是解决前面流量大后端流量小的问题,所有的mq都强调事后一致性,就算是阿里的rocketmq,号称支持分布式事务,但是实际上也不是

最需要考虑的两个问题:

1.消息消费的顺序问题:发送消息指定队列,消息消费者指定队列可以解决,消费者只能一个。
2.消息消费的重复问题:每次消费消息时候创建一消息表,在消费消息前先查询该表,如果消息存在就说明已经消费

消费者代码示例:

System.out.println("你好现在是 " + new Date() +"");
    System.out.println("HelloSender发送内容 : " + users.toString());


    /**
     * ConfirmCallback接口用于实现消息发送到RabbitMQ交换器后接收ack回调。
     * ReturnCallback接口用于实现消息发送到RabbitMQ交换器,但无相应队列与交换器绑定时的回调。
     */
    rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
        //Users users1 = (Users)message.getBody().toString();
        //String correlationId = message.getMessageProperties().getCorrelationId();

        System.out.println("Message : " + new String(message.getBody()));
        //System.out.println("Message : " + new String(message.getBody()));
        System.out.println("replyCode : " + replyCode);
        System.out.println("replyText : " + replyText);  //错误原因
        System.out.println("exchange : " + exchange);
        System.out.println("routingKey : " + routingKey);//queue名称

    });

    rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
        if (ack) {
            CorrelationDataEx c = (CorrelationDataEx)correlationData;
            System.out.println("发送消息: " + c.getMsg());
            System.out.println("HelloSender 消息发送成功 :" + correlationData.toString() );
            /**
             * 通过设置correlationData.id为业务主键,消息发送成功后去继续做候选业务。
             */
        } else {
            System.out.println("HelloSender消息发送失败" + cause);
        }
    });

    /**
     * CorrelationDataEx继承CorrelationData, 把需要发送消息的关键字段加入
     * 这样confirmcallback可以返回带有关键字段的correlationData,我们可以通过这个来确定发送的是那条业务记录
     */
    CorrelationDataEx c = new CorrelationDataEx();
    c.setId(users.getId().toString());
    c.setMsg(users.toString());

    /**
     * 加上这个,可以从returncallback参数中读取发送的json消息,否则是二进制bytes
     * 比如:如果returncallback触发,则表明消息没有投递到队列,则继续业务操作,比如将消息记录标志位未投递成功,记录投递次数
     */
    rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());

    rabbitTemplate.convertAndSend(EXCHANGE, QUEUE_TWO_ROUTING, users, c);

}

生产者向mq发送一条消息,mq接收成功会给生产者返回一个ack,表示成功接收,生产者可以提交事务。 但是这里面也是有一个问题,如果数据库提交失败,那么发送成功的消息是多余的,这个就要靠消费者消费的时候检查消息的幂等性来限制。

消费者代码示例

@RabbitHandler
@RabbitListener(queues = QUEUE_ONE_ROUTING) //containerFactory = "rabbitListenerContainerFactory", concurrency = "2")
public void process(Users users, Channel channel, Message message) throws IOException {
    System.out.println("HelloReceiver收到  : " + users.toString() + "收到时间" + new Date());

    try {
        //告诉服务器收到这条消息 已经被我消费了 可以在队列删掉 这样以后就不会再发了
        // 否则消息服务器以为这条消息没处理掉 后续还会在发
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        System.out.println("receiver success");
    } catch (IOException e) {
        e.printStackTrace();
        //丢弃这条消息,则不会重新发送了
        //channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
        System.out.println("receiver fail");
    }
}

@Bean
public MessageConverter jackson2JsonMessageConverter() {
    return new Jackson2JsonMessageConverter();
}

消费者成功消费了,需要调用basicAck来向mq发一个ack,这样mq就会知道这个消息已经消费了,删除之。如果不发ack,mq还有这个消息。

这是rocket mq和rabbit mq不同而已。这是rocket mq和rabbit mq不同而已。mq需要做持久化,这样宕机后起来会把未消费的消息重新读入。但是不管kafka 等等,最后还都是建议手工介入,如果碰上极端情况下。

你可能感兴趣的:(201811)