1 简介

说到消息中间件,我想大家应该并不陌生,或多或少都有所接触。其实通俗的理解就是,消息中间件也就是一种开发好的系统,并且独立部署,然后我们业务系统通过它来发消息和收消息以至于达到异步调用的效果。而消息中间件最常见的实现方式是通过队列来实现,也就是所谓的消息队列(Message Queue),简称 MQ。

2 功能

2.1 异步

        消息队列最初设计的目的就是为了解决异步请求。

        假设有两个系统A和B,其中A系统处理业务大概需要 10 毫秒,而B系统处理业务需要100毫秒,如果是同步去调用,那么需要花费 110 毫秒。但是如果使用了消息中间件的话,则是A系统处理业务10毫秒,发送消息到消息队列中,花费1毫秒,然后就直接返回给用户了,B系统什么时候去MQ中去消费信息,A系统是无需关心的,于是对于用户来说只花费了11毫秒。

2.2 解耦   

        消息队列另一个最大的好处,就是实现系统间的解耦。

        比如有多个系统需要和 A 系统打交道,那么每次系统的变更和新增,都需要更新 A 系统,耦合度高。而添加了消息队列之后,其他系统的变更对于 A 系统来说都是无感知的,也是 A 系统无需关心的,这就实现了解耦的功能。

2.3 大流量削峰

        一个比较典型的场景就是秒杀业务。

kafka 远程连接nodenotreadyerror_kafka

 

        秒杀开始,请求进来可能有上百万个,但是最终处理的请求可能只有 100 个。另外上百万个请求同时进来,下游系统处理资源的能力是有限的,所以出现峰值的时候,很容易导致服务器宕机,用户无法访问。

        于是就有了流量削峰。本质上来说,就是更多地延缓用户请求,以及层层过滤用户的访问需求,遵从“最后落地到数据库的请求数要尽量少”的原则。

        一般由两种解决方案。

        方案一,用消息队列来缓冲瞬时流量,把同步的直接调用改为异步的推送,中间通过一个队列在一端承接瞬时的流量洪峰,在另一端平稳地将消息推送给下游。

        方案二则是采用漏斗式设计来处理请求,与本文讨论内容关系不大,在此不再赘述。

3 市面上常见的消息中间件

kafka 远程连接nodenotreadyerror_消息队列_02

 

4 各种 MQ 的比较

特性

rabbitMQ

rocketMQ

kafka

开发语言

erlang

java

scala

单机吞吐量

万/秒

10万/秒

10万/秒

时效性

微秒

这是rabbit 最大优势,延迟低

毫秒

毫秒

可用性

高,主从架构

非常高,分布式架构

非常高,分布式架构。数据多副本,不会丢数据,不会不可用。

功能特性

erlang开发,并发强,性能极好,延迟低

MQ功能较为齐全,扩展好

功能简单,主要用于大数据实时计算和日志采集,事实标准

缺点

1 erlang开发,很难去看懂源码,基本职能依赖于开源社区的快速维护和修复bug,不利于做二次开发和维护。

2 RabbitMQ确实吞吐量会低一些,这是因为他做的实现机制比较重。

3 需要学习比较复杂的接口和协议,学习和维护成本较高。

1 支持的客户端语言不多,目前是java及c++,其中c++不成熟;

2 社区活跃度一般

3 没有在 mq 核心中去实现JMS等接口,有些系统要迁移需要修改大量代码


1 Kafka单机超过64个队列/分区,Load会发生明显的飙高现象,队列越多,load越高,发送消息响应时间变长

2 使用短轮询方式,实时性取决于轮询间隔时间;

3 消费失败不支持重试;

4 支持消息顺序,但是一台代理宕机后,就会产生消息乱序;

5 社区更新较慢;

5 消息队列选择建议

5.1 Kafka

        Kafka主要特点是基于Pull的模式来处理消息消费,追求高吞吐量,一开始的目的就是用于日志收集和传输,适合产生大量数据的互联网服务的数据收集业务。

        大型公司建议可以选用,如果有日志采集功能,肯定是首选kafka了。

5.2 RocketMQ

        天生为金融互联网领域而生,对于可靠性要求很高的场景,尤其是电商里面的订单扣款,以及业务削峰,在大量交易涌入时,后端可能无法及时处理的情况。

        RoketMQ在稳定性上可能更值得信赖,这些业务场景在阿里双11已经经历了多次考验,如果你的业务有上述并发场景,建议可以选择RocketMQ。

5.3 RabbitMQ

        RabbitMQ :结合erlang语言本身的并发优势,性能较好,社区活跃度也比较高,但是不利于做二次开发和维护。不过,RabbitMQ的社区十分活跃,可以解决开发过程中遇到的bug。

        如果你的数据量没有那么大,小公司优先选择功能比较完备的RabbitMQ。

6 消息队列的部署与集成

        接下来我会简单介绍这几种队列的部署(采用docker单机部署方式)及与 Spring 集成的方式

6.1 RabbitMQ

6.1.1 部署配置

rabbit.yml

version: '3.7'
services:

  rabbit:
    image: rabbitmq:3.7.14-management-alpine
    networks:
      - loc_net
    ports:
      - 5672:5672
      - 15672:15672
    volumes:
      - "/data/rabbit/data:/var/lib/rabbitmq"


networks:
  loc_net:
    external: true

然后通过 http://ip:15672 来访问,可以进入web管理页面,默认账号密码 guest guest

6.1.2 Springboot + RabbitMQ

pom.xml

<!-- rabbit 基于 amqp 协议的 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

application.yml

spring:
  rabbitmq:
    username: guest
    password: guest
    port: 5672
    addresses: ip

RabbitMQComponent.java

@Component
public class RabbitMQComponent {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 普通消息
     */
    public void sendMsg(String topic, String key, String payload) {
        rabbitTemplate.convertAndSend(topic, key, payload);
    }

}

Test.java

@Test
public void sendMsgV3() {
    rabbitMQComponent.sendMsg("demo", "demo", "{\"test\":\"test\"}");
}

消费者 DemoRabbitListener.java

@Component
public class DemoRabbitListener {

    @RabbitListener(queues = "demo")
    public void onMessage(Message message) {
        System.out.println("Receive message:" + new String(message.getBody()));
    }

}

6.2 RocketMQ

6.2.1 部署配置

rocketmq.yml

version: '3.7'
services:
  rocketmq-namesrv:
    image: rocketmqinc/rocketmq
    container_name: rmqnamesrv
    restart: always
    ports:
      - 9876:9876
    volumes:
      - /data/rocketmq/logs:/home/rocketmq/logs
      - /data/rocketmq/store:/home/rocketmq/store
    command: sh mqnamesrv
  rocketmq-broker:
    image: rocketmqinc/rocketmq
    container_name: rmqbroker
    restart: always
    ports:
      - 10909:10909
      - 10911:10911
      - 10912:10912
    volumes:
      - /data/rocketmq/logs:/home/rocketmq/logs
      - /data/rocketmq/store:/home/rocketmq/store
      - /data/rocketmq/conf/broker.conf:/opt/rocketmq-4.4.0/conf/broker.conf
    command: sh mqbroker -n ip:9876 -c /opt/rocketmq-4.4.0/conf/broker.conf
    networks:
      - loc_net
    environment:
      - JAVA_HOME=/usr/lib/jvm/jre
  rocketmq-console:
    image: styletang/rocketmq-console-ng
    container_name: rocketmq-console-ng
    restart: always
    ports:
      - 8076:8080
    networks:
      - loc_net
    environment:
      - JAVA_OPTS= -Dlogging.level.root=info -Drocketmq.namesrv.addr=ip:9876 -Dcom.rocketmq.sendMessageWithVIPChannel=false

networks:
  loc_net:
    external: true

然后通过 http://ip:8076 来访问,可以进入web管理页面

6.2.2 Springboot + RocketMQ

pom.xml

<!-- https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-spring-boot-starter -->
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.2.0</version>
</dependency>

application.yml

rocketmq:
  nameServer: ip:9876
  producer:
    group: demo-group

RocketMQComponent.java

@Component
public class RocketMQComponent {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    /**
     * 普通消息
     */
    public void sendMsg(String topic, String key, String payload) {
        String destination = topic;
        if (StringUtils.isNotBlank(key)) {
            destination = destination + ":" + key;
        }
        rocketMQTemplate.convertAndSend(destination, payload);
    }

}

Test.java

@Test
public void sendMsg() {
    rocketMQComponent.sendMsg("demo", "TagA", "Hello MQ");
}

消费者 DemoRocketListener.java

@Component
@RocketMQMessageListener(topic = "demo", consumerGroup = "consumer-msg-group")
public class DemoRocketListener implements RocketMQListener<String> {

    @Override
    public void onMessage(String message) {
        System.out.println("Receive message:" + message);
    }
}

6.3 Kafka

6.3.1 部署配置

kafka.yml

version: '3.7'
services:
  zk:
    container_name: zk
    image: wurstmeister/zookeeper
    volumes:
      - /data/zk:/data
    ports:
      - 2181:2181
    networks:
      - loc_net

  kafka:
    container_name: kafka
    image: wurstmeister/kafka
    ports:
      - 9092:9092
    environment:
      KAFKA_BROKER_ID: 0
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://ip:9092
      KAFKA_ZOOKEEPER_CONNECT: ip:2181
      KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092
    volumes:
      - /data/kafka/logs:/kafka
    networks:
      - loc_net

  kafka-web:
    container_name: kafka-web
    image: freakchicken/kafka-ui-lite
    ports:
      - 8889:8889
    networks:
      - loc_net

networks:
  loc_net:
    external: true

然后通过 http://ip:8889 来访问,可以进入web管理页面

6.3.2 Springboot + Kafka

pom.xml

<!-- kafka -->
<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>

application.yml

spring:
  kafka:
    bootstrap-servers: "ip:9092"
    producer: # 生产者
      retries: 3 # 设置大于0的值,则客户端会将发送失败的记录重新发送
      batch-size: 16384
      buffer-memory: 33554432
      acks: 1
      # 指定消息key和消息体的编解码方式
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
    consumer:
      group-id: default-group
      enable-auto-commit: false
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
    listener:
      ack-mode: manual_immediate

KafkaMQComponent.java

@Component
public class KafkaMQComponent {

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    /**
     * 普通消息
     */
    public void sendMsg(String topic, String key, String payload) {
        kafkaTemplate.send(topic, key, payload);
    }

}

Test.java

@Test
public void sendMsgV2() {
    kafkaMQComponent.sendMsg("demo", "key", " {\"test\":\"test\"}");
}

消费者 DemoKafkaListener.java

@Component
public class DemoKafkaListener {

    @KafkaListener(topics = "demo", groupId = "kafka-consumer")
    public String onMessage(ConsumerRecord<String, String> cr, Acknowledgment acknowledgment) {
        System.out.println("Receive message:" + cr.value());
        // 手动提交,表示该消息已经收到
        acknowledgment.acknowledge();
        return "successful";
    }
}

        以上demo 只给出了简单的信息收发方式,具体还有许多高级功能由于篇幅就不展开讲了,总的体验下来,kafka 是最简单的,rabbitmq 是功能最丰富的。

7 总结

        消息队列是分布式系统中的重要组件,适用于各种场景。每一个功能,每一款产品单拉出来都可以延伸出很多内容。此篇文章权当抛砖引玉,如有说错或者不足的地方,欢迎指出。日后有机会,会开个专栏,对其中内容一一展开介绍。