【mq】從零開始實現 mq-01-生產者、消費者啟動

老馬嘯西風發表於2022-04-22

MQ 是什麼?

MQ(Message Queue)訊息佇列,是基礎資料結構中“先進先出”的一種資料結構。

指把要傳輸的資料(訊息)放在佇列中,用佇列機制來實現訊息傳遞——生產者產生訊息並把訊息放入佇列,然後由消費者去處理。

消費者可以到指定佇列拉取訊息,或者訂閱相應的佇列,由MQ服務端給其推送訊息。

MQ 的作用?

訊息佇列中介軟體是分散式系統中重要的元件,主要解決應用解耦,非同步訊息,流量削鋒等問題,實現高效能,高可用,可伸縮和最終一致性架構。

解耦:一個業務需要多個模組共同實現,或者一條訊息有多個系統需要對應處理,只需要主業務完成以後,傳送一條MQ,其餘模組消費MQ訊息,即可實現業務,降低模組之間的耦合。

非同步:主業務執行結束後從屬業務通過MQ,非同步執行,減低業務的響應時間,提高使用者體驗。

削峰:高併發情況下,業務非同步處理,提供高峰期業務處理能力,避免系統癱瘓。

ps: 以上內容摘選自百科。

實現 mq 的準備工作

maven 引入

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.42.Final</version>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.76</version>
</dependency>

模組劃分

The message queue in java. 作為 mq 的從零開始的學習專案,目前已開源。

專案的模組如下:

模組說明
mq-common公共程式碼
mq-broker註冊中心
mq-producer訊息生產者
mq-consumer訊息消費者

訊息消費者

介面定義

package com.github.houbb.mq.consumer.api;

/**
 * @author binbin.hou
 * @since 1.0.0
 */
public interface IMqConsumer {

    /**
     * 訂閱
     * @param topicName topic 名稱
     * @param tagRegex 標籤正則
     */
    void subscribe(String topicName, String tagRegex);

    /**
     * 註冊監聽器
     * @param listener 監聽器
     */
    void registerListener(final IMqConsumerListener listener);

}

IMqConsumerListener 作為訊息監聽類的介面,定義如下:

public interface IMqConsumerListener {


    /**
     * 消費
     * @param mqMessage 訊息體
     * @param context 上下文
     * @return 結果
     */
    ConsumerStatus consumer(final MqMessage mqMessage,
                            final IMqConsumerListenerContext context);

}

ConsumerStatus 代表訊息消費的幾種狀態。

訊息體

啟動訊息體 MqMessage 定義如下:

package com.github.houbb.mq.common.dto;

import java.util.Arrays;
import java.util.List;

/**
 * @author binbin.hou
 * @since 1.0.0
 */
public class MqMessage {

    /**
     * 標題名稱
     */
    private String topic;

    /**
     * 標籤
     */
    private List<String> tags;

    /**
     * 內容
     */
    private byte[] payload;

    /**
     * 業務標識
     */
    private String bizKey;

    /**
     * 負載分片標識
     */
    private String shardingKey;

    // getter&setter&toString

}

push 消費者策略實現

消費者啟動的實現如下:

/**
 * 推送消費策略
 *
 * @author binbin.hou
 * @since 1.0.0
 */
public class MqConsumerPush extends Thread implements IMqConsumer  {

    // 省略...

    @Override
    public void run() {
        // 啟動服務端
        log.info("MQ 消費者開始啟動服務端 groupName: {}, port: {}, brokerAddress: {}",
                groupName, port, brokerAddress);

        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(workerGroup, bossGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<Channel>() {
                        @Override
                        protected void initChannel(Channel ch) throws Exception {
                            ch.pipeline().addLast(new MqConsumerHandler());
                        }
                    })
                    // 這個引數影響的是還沒有被accept 取出的連線
                    .option(ChannelOption.SO_BACKLOG, 128)
                    // 這個引數只是過一段時間內客戶端沒有響應,服務端會傳送一個 ack 包,以判斷客戶端是否還活著。
                    .childOption(ChannelOption.SO_KEEPALIVE, true);

            // 繫結埠,開始接收進來的連結
            ChannelFuture channelFuture = serverBootstrap.bind(port).syncUninterruptibly();
            log.info("MQ 消費者啟動完成,監聽【" + port + "】埠");

            channelFuture.channel().closeFuture().syncUninterruptibly();
            log.info("MQ 消費者關閉完成");
        } catch (Exception e) {
            log.error("MQ 消費者啟動異常", e);
            throw new MqException(ConsumerRespCode.RPC_INIT_FAILED);
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }


    }


    // 省略...

}

ps: 初期我們把 consumer 作為服務端,後續引入 broker 則只有 broker 是服務端。

MqConsumerHandler 處理類

這個類是一個空的實現。

public class MqConsumerHandler extends SimpleChannelInboundHandler {

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object object) throws Exception {
        //nothing
    }

}

測試程式碼

MqConsumerPush mqConsumerPush = new MqConsumerPush();
mqConsumerPush.start();

啟動日誌:

[DEBUG] [2022-04-21 19:16:41.343] [main] [c.g.h.l.i.c.LogFactory.setImplementation] - Logging initialized using 'class com.github.houbb.log.integration.adaptors.stdout.StdOutExImpl' adapter.
[INFO] [2022-04-21 19:16:41.356] [Thread-0] [c.g.h.m.c.c.MqConsumerPush.run] - MQ 消費者開始啟動服務端 groupName: C_DEFAULT_GROUP_NAME, port: 9527, brokerAddress: 
[INFO] [2022-04-21 19:16:43.196] [Thread-0] [c.g.h.m.c.c.MqConsumerPush.run] - MQ 消費者啟動完成,監聽【9527】埠

訊息生產者

介面定義

最基本的訊息傳送介面。

package com.github.houbb.mq.producer.api;

import com.github.houbb.mq.common.dto.MqMessage;
import com.github.houbb.mq.producer.dto.SendResult;

/**
 * @author binbin.hou
 * @since 1.0.0
 */
public interface IMqProducer {

    /**
     * 同步傳送訊息
     * @param mqMessage 訊息型別
     * @return 結果
     */
    SendResult send(final MqMessage mqMessage);

    /**
     * 單向傳送訊息
     * @param mqMessage 訊息型別
     * @return 結果
     */
    SendResult sendOneWay(final MqMessage mqMessage);

}

生產者實現

MqProducer 啟動的實現如下,基於 netty。

package com.github.houbb.mq.producer.core;

/**
 * 預設 mq 生產者
 * @author binbin.hou
 * @since 1.0.0
 */
public class MqProducer extends Thread implements IMqProducer {

    //省略...

    @Override
    public void run() {
        // 啟動服務端
        log.info("MQ 生產者開始啟動客戶端 GROUP: {}, PORT: {}, brokerAddress: {}",
                groupName, port, brokerAddress);

        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap();
            ChannelFuture channelFuture = bootstrap.group(workerGroup)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.SO_KEEPALIVE, true)
                    .handler(new ChannelInitializer<Channel>(){
                        @Override
                        protected void initChannel(Channel ch) throws Exception {
                            ch.pipeline()
                                    .addLast(new LoggingHandler(LogLevel.INFO))
                                    .addLast(new MqProducerHandler());
                        }
                    })
                    .connect("localhost", port)
                    .syncUninterruptibly();

            log.info("MQ 生產者啟動客戶端完成,監聽埠:" + port);
            channelFuture.channel().closeFuture().syncUninterruptibly();
            log.info("MQ 生產者開始客戶端已關閉");
        } catch (Exception e) {
            log.error("MQ 生產者啟動遇到異常", e);
            throw new MqException(ProducerRespCode.RPC_INIT_FAILED);
        } finally {
            workerGroup.shutdownGracefully();
        }
    }

    //省略...
}

MqProducerHandler 處理類

預設的空實現,什麼都不做。

package com.github.houbb.mq.producer.handler;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

/**
 * @author binbin.hou
 * @since 1.0.0
 */
public class MqProducerHandler extends SimpleChannelInboundHandler {

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object object) throws Exception {
        //do nothing now
    }

}

啟動程式碼

MqProducer mqProducer = new MqProducer();
mqProducer.start();

啟動日誌:

[DEBUG] [2022-04-21 19:17:11.960] [main] [c.g.h.l.i.c.LogFactory.setImplementation] - Logging initialized using 'class com.github.houbb.log.integration.adaptors.stdout.StdOutExImpl' adapter.
[INFO] [2022-04-21 19:17:11.974] [Thread-0] [c.g.h.m.p.c.MqProducer.run] - MQ 生產者開始啟動客戶端 GROUP: P_DEFAULT_GROUP_NAME, PORT: 9527, brokerAddress: 
四月 21, 2022 7:17:13 下午 io.netty.handler.logging.LoggingHandler channelRegistered
資訊: [id: 0x5cb48145] REGISTERED
四月 21, 2022 7:17:13 下午 io.netty.handler.logging.LoggingHandler connect
資訊: [id: 0x5cb48145] CONNECT: localhost/127.0.0.1:9527
四月 21, 2022 7:17:13 下午 io.netty.handler.logging.LoggingHandler channelActive
資訊: [id: 0x5cb48145, L:/127.0.0.1:57740 - R:localhost/127.0.0.1:9527] ACTIVE
[INFO] [2022-04-21 19:17:13.833] [Thread-0] [c.g.h.m.p.c.MqProducer.run] - MQ 生產者啟動客戶端完成,監聽埠:9527

小結

基於 netty 最基本的服務端啟動、客戶端啟動到這裡就結束了。

千里之行,始於足下。

我們下一節將和大家一起學習,如何實現客戶端與服務端之間的互動。

希望本文對你有所幫助,如果喜歡,歡迎點贊收藏轉發一波。

我是老馬,期待與你的下次重逢。

開源地址

The message queue in java.(java 簡易版本 mq 實現) : https://github.com/houbb/mq

擴充閱讀

rpc-從零開始實現 rpc: https://github.com/houbb/rpc

【mq】從零開始實現 mq-01-生產者、消費者啟動

相關文章