【mq】從零開始實現 mq-03-引入 broker 中間人

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

前景回顧

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

【mq】從零開始實現 mq-02-如何實現生產者呼叫消費者?

【mq】從零開始實現 mq-03-引入 broker 中間人

上一節我們學習瞭如何實現生產者給消費者傳送訊息,但是是通過直連的方式。

那麼如何才能達到解耦的效果呢?

答案就是引入 broker,訊息的中間人。

broker

MqBroker 實現

核心啟動類

類似我們前面 consumer 的啟動實現:

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

/**
 * @author binbin.hou
 * @since 1.0.0
 */
public class MqBroker extends Thread implements IMqBroker {

    // 省略
    private ChannelHandler initChannelHandler() {
        MqBrokerHandler handler = new MqBrokerHandler();
        handler.setInvokeService(invokeService);
        handler.setRegisterConsumerService(registerConsumerService);
        handler.setRegisterProducerService(registerProducerService);
        handler.setMqBrokerPersist(mqBrokerPersist);
        handler.setBrokerPushService(brokerPushService);
        handler.setRespTimeoutMills(respTimeoutMills);

        return handler;
    }

    @Override
    public void run() {
        // 啟動服務端
        log.info("MQ 中間人開始啟動服務端 port: {}", port);

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

        try {
            final ByteBuf delimiterBuf = DelimiterUtil.getByteBuf(DelimiterUtil.DELIMITER);
            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 DelimiterBasedFrameDecoder(DelimiterUtil.LENGTH, delimiterBuf))
                                    .addLast(initChannelHandler());
                        }
                    })
                    // 這個引數影響的是還沒有被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(BrokerRespCode.RPC_INIT_FAILED);
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

}

initChannelHandler 中有不少新面孔,我們後面會詳細介紹。

MqBrokerHandler 處理邏輯

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

import java.util.List;

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

    private static final Log log = LogFactory.getLog(MqBrokerHandler.class);

    /**
     * 呼叫管理類
     * @since 1.0.0
     */
    private IInvokeService invokeService;

    /**
     * 消費者管理
     * @since 0.0.3
     */
    private IBrokerConsumerService registerConsumerService;

    /**
     * 生產者管理
     * @since 0.0.3
     */
    private IBrokerProducerService registerProducerService;

    /**
     * 持久化類
     * @since 0.0.3
     */
    private IMqBrokerPersist mqBrokerPersist;

    /**
     * 推送服務
     * @since 0.0.3
     */
    private IBrokerPushService brokerPushService;

    /**
     * 獲取響應超時時間
     * @since 0.0.3
     */
    private long respTimeoutMills;

    //set 方法


    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf byteBuf = (ByteBuf) msg;
        byte[] bytes = new byte[byteBuf.readableBytes()];
        byteBuf.readBytes(bytes);

        RpcMessageDto rpcMessageDto = null;
        try {
            rpcMessageDto = JSON.parseObject(bytes, RpcMessageDto.class);
        } catch (Exception exception) {
            log.error("RpcMessageDto json 格式轉換異常 {}", new String(bytes));
            return;
        }

        if (rpcMessageDto.isRequest()) {
            MqCommonResp commonResp = this.dispatch(rpcMessageDto, ctx);

            if(commonResp == null) {
                log.debug("當前訊息為 null,忽略處理。");
                return;
            }

            // 寫回響應,和以前類似。     
            writeResponse(rpcMessageDto, commonResp, ctx);
        } else {
            final String traceId = rpcMessageDto.getTraceId();

            // 丟棄掉 traceId 為空的資訊
            if(StringUtil.isBlank(traceId)) {
                log.debug("[Server Response] response traceId 為空,直接丟棄", JSON.toJSON(rpcMessageDto));
                return;
            }

            // 新增訊息
            invokeService.addResponse(traceId, rpcMessageDto);
        }
    }

    /**
     * 非同步處理訊息
     * @param mqMessage 訊息
     * @since 0.0.3
     */
    private void asyncHandleMessage(MqMessage mqMessage) {
        List<Channel> channelList = registerConsumerService.getSubscribeList(mqMessage);
        if(CollectionUtil.isEmpty(channelList)) {
            log.info("監聽列表為空,忽略處理");
            return;
        }

        BrokerPushContext brokerPushContext = new BrokerPushContext();
        brokerPushContext.setChannelList(channelList);
        brokerPushContext.setMqMessage(mqMessage);
        brokerPushContext.setMqBrokerPersist(mqBrokerPersist);
        brokerPushContext.setInvokeService(invokeService);
        brokerPushContext.setRespTimeoutMills(respTimeoutMills);

        brokerPushService.asyncPush(brokerPushContext);
    }
}

訊息分發

broker 接收到訊息以後,dispatch 實現如下:

/**
 * 訊息的分發
 *
 * @param rpcMessageDto 入參
 * @param ctx 上下文
 * @return 結果
 */
private MqCommonResp dispatch(RpcMessageDto rpcMessageDto, ChannelHandlerContext ctx) {
    try {
        final String methodType = rpcMessageDto.getMethodType();
        final String json = rpcMessageDto.getJson();
        String channelId = ChannelUtil.getChannelId(ctx);
        final Channel channel = ctx.channel();
        log.debug("channelId: {} 接收到 method: {} 內容:{}", channelId,
                methodType, json);

        // 生產者註冊
        if(MethodType.P_REGISTER.equals(methodType)) {
            BrokerRegisterReq registerReq = JSON.parseObject(json, BrokerRegisterReq.class);
            return registerProducerService.register(registerReq.getServiceEntry(), channel);
        }
        // 生產者登出
        if(MethodType.P_UN_REGISTER.equals(methodType)) {
            BrokerRegisterReq registerReq = JSON.parseObject(json, BrokerRegisterReq.class);
            return registerProducerService.unRegister(registerReq.getServiceEntry(), channel);
        }
        // 生產者訊息傳送
        if(MethodType.P_SEND_MSG.equals(methodType)) {
            MqMessage mqMessage = JSON.parseObject(json, MqMessage.class);
            MqMessagePersistPut persistPut = new MqMessagePersistPut();
            persistPut.setMqMessage(mqMessage);
            persistPut.setMessageStatus(MessageStatusConst.WAIT_CONSUMER);
            MqCommonResp commonResp = mqBrokerPersist.put(persistPut);
            this.asyncHandleMessage(mqMessage);
            return commonResp;
        }
        // 生產者訊息傳送-ONE WAY
        if(MethodType.P_SEND_MSG_ONE_WAY.equals(methodType)) {
            MqMessage mqMessage = JSON.parseObject(json, MqMessage.class);
            MqMessagePersistPut persistPut = new MqMessagePersistPut();
            persistPut.setMqMessage(mqMessage);
            persistPut.setMessageStatus(MessageStatusConst.WAIT_CONSUMER);
            mqBrokerPersist.put(persistPut);
            this.asyncHandleMessage(mqMessage);
            return null;
        }

        // 消費者註冊
        if(MethodType.C_REGISTER.equals(methodType)) {
            BrokerRegisterReq registerReq = JSON.parseObject(json, BrokerRegisterReq.class);
            return registerConsumerService.register(registerReq.getServiceEntry(), channel);
        }
        // 消費者登出
        if(MethodType.C_UN_REGISTER.equals(methodType)) {
            BrokerRegisterReq registerReq = JSON.parseObject(json, BrokerRegisterReq.class);
            return registerConsumerService.unRegister(registerReq.getServiceEntry(), channel);
        }
        // 消費者監聽註冊
        if(MethodType.C_SUBSCRIBE.equals(methodType)) {
            ConsumerSubscribeReq req = JSON.parseObject(json, ConsumerSubscribeReq.class);
            return registerConsumerService.subscribe(req, channel);
        }
        // 消費者監聽登出
        if(MethodType.C_UN_SUBSCRIBE.equals(methodType)) {
            ConsumerUnSubscribeReq req = JSON.parseObject(json, ConsumerUnSubscribeReq.class);
            return registerConsumerService.unSubscribe(req, channel);
        }

        // 消費者主動 pull
        if(MethodType.C_MESSAGE_PULL.equals(methodType)) {
            MqConsumerPullReq req = JSON.parseObject(json, MqConsumerPullReq.class);
            return mqBrokerPersist.pull(req, channel);
        }
        throw new UnsupportedOperationException("暫不支援的方法型別");
    } catch (Exception exception) {
        log.error("執行異常", exception);
        MqCommonResp resp = new MqCommonResp();
        resp.setRespCode(MqCommonRespCode.FAIL.getCode());
        resp.setRespMessage(MqCommonRespCode.FAIL.getMsg());
        return resp;
    }
}

訊息推送

this.asyncHandleMessage(mqMessage); 是 broker 接收到訊息之後的處理邏輯。

/**
 * 非同步處理訊息
 * @param mqMessage 訊息
 * @since 0.0.3
 */
private void asyncHandleMessage(MqMessage mqMessage) {
    List<Channel> channelList = registerConsumerService.getSubscribeList(mqMessage);
    if(CollectionUtil.isEmpty(channelList)) {
        log.info("監聽列表為空,忽略處理");
        return;
    }

    BrokerPushContext brokerPushContext = new BrokerPushContext();
    brokerPushContext.setChannelList(channelList);
    brokerPushContext.setMqMessage(mqMessage);
    brokerPushContext.setMqBrokerPersist(mqBrokerPersist);
    brokerPushContext.setInvokeService(invokeService);
    brokerPushContext.setRespTimeoutMills(respTimeoutMills);
    brokerPushService.asyncPush(brokerPushContext);
}

推送的核心實現如下:

package com.github.houbb.mq.broker.support.push;

/**
 * @author binbin.hou
 * @since 0.0.3
 */
public class BrokerPushService implements IBrokerPushService {

    private static final Log log = LogFactory.getLog(BrokerPushService.class);

    private static final ExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadExecutor();

    @Override
    public void asyncPush(final BrokerPushContext context) {
        EXECUTOR_SERVICE.submit(new Runnable() {
            @Override
            public void run() {
                log.info("開始非同步處理 {}", JSON.toJSON(context));
                final List<Channel> channelList = context.getChannelList();
                final IMqBrokerPersist mqBrokerPersist = context.getMqBrokerPersist();
                final MqMessage mqMessage = context.getMqMessage();
                final String messageId = mqMessage.getTraceId();
                final IInvokeService invokeService = context.getInvokeService();
                final long responseTime = context.getRespTimeoutMills();

                for(Channel channel : channelList) {
                    try {
                        String channelId = ChannelUtil.getChannelId(channel);

                        log.info("開始處理 channelId: {}", channelId);
                        //1. 呼叫
                        mqMessage.setMethodType(MethodType.B_MESSAGE_PUSH);
                        MqConsumerResultResp resultResp = callServer(channel, mqMessage,
                                MqConsumerResultResp.class, invokeService, responseTime);

                        //2. 更新狀態
                        mqBrokerPersist.updateStatus(messageId, resultResp.getConsumerStatus());

                        //3. 後期新增重試策略

                        log.info("完成處理 channelId: {}", channelId);
                    } catch (Exception exception) {
                        log.error("處理異常");
                        mqBrokerPersist.updateStatus(messageId, ConsumerStatus.FAILED.getCode());
                    }
                }

                log.info("完成非同步處理");
            }
        });
    }
}

此處在訊息推送之後,需要更新訊息的 ACK 狀態。

訊息生產者處理類

IBrokerProducerService 介面定義如下:

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

/**
 * <p> 生產者註冊服務類 </p>
 *
 * @author houbinbin
 * @since 0.0.3
 */
public interface IBrokerProducerService {

    /**
     * 註冊當前服務資訊
     * (1)將該服務通過 {@link ServiceEntry#getGroupName()} 進行分組
     * 訂閱了這個 serviceId 的所有客戶端
     * @param serviceEntry 註冊當前服務資訊
     * @param channel channel
     * @since 0.0.8
     */
    MqCommonResp register(final ServiceEntry serviceEntry, Channel channel);

    /**
     * 登出當前服務資訊
     * @param serviceEntry 註冊當前服務資訊
     * @param channel 通道
     * @since 0.0.8
     */
    MqCommonResp unRegister(final ServiceEntry serviceEntry, Channel channel);

    /**
     * 獲取服務地址資訊
     * @param channel channel
     * @return 結果
     * @since 0.0.3
     */
    ServiceEntry getServiceEntry(final Channel channel);

}

實現如下:

本地基於 map 儲存請求過來的基本資訊。

package com.github.houbb.mq.broker.support.api;

/**
 * <p> 生產者註冊服務類 </p>
 *
 * @author houbinbin
 * @since 0.0.3
 */
public class LocalBrokerProducerService implements IBrokerProducerService {

    private static final Log log = LogFactory.getLog(LocalBrokerProducerService.class);

    private final Map<String, BrokerServiceEntryChannel> registerMap = new ConcurrentHashMap<>();

    @Override
    public MqCommonResp register(ServiceEntry serviceEntry, Channel channel) {
        final String channelId = ChannelUtil.getChannelId(channel);
        BrokerServiceEntryChannel entryChannel = InnerChannelUtils.buildEntryChannel(serviceEntry, channel);
        registerMap.put(channelId, entryChannel);


        MqCommonResp resp = new MqCommonResp();
        resp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
        resp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
        return resp;
    }

    @Override
    public MqCommonResp unRegister(ServiceEntry serviceEntry, Channel channel) {
        final String channelId = ChannelUtil.getChannelId(channel);
        registerMap.remove(channelId);

        MqCommonResp resp = new MqCommonResp();
        resp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
        resp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
        return resp;
    }

    @Override
    public ServiceEntry getServiceEntry(Channel channel) {
        final String channelId = ChannelUtil.getChannelId(channel);
        return registerMap.get(channelId);
    }

}

訊息消費者處理類

介面定義如下:

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

/**
 * <p> 消費者註冊服務類 </p>
 *
 * @author houbinbin
 * @since 0.0.3
 */
public interface IBrokerConsumerService {

    /**
     * 註冊當前服務資訊
     * (1)將該服務通過 {@link ServiceEntry#getGroupName()} 進行分組
     * 訂閱了這個 serviceId 的所有客戶端
     * @param serviceEntry 註冊當前服務資訊
     * @param channel channel
     * @since 0.0.3
     */
    MqCommonResp register(final ServiceEntry serviceEntry, Channel channel);

    /**
     * 登出當前服務資訊
     * @param serviceEntry 註冊當前服務資訊
     * @param channel channel
     * @since 0.0.3
     */
    MqCommonResp unRegister(final ServiceEntry serviceEntry, Channel channel);

    /**
     * 監聽服務資訊
     * (1)監聽之後,如果有任何相關的機器資訊發生變化,則進行推送。
     * (2)內建的資訊,需要傳送 ip 資訊到註冊中心。
     *
     * @param serviceEntry 客戶端明細資訊
     * @param clientChannel 客戶端 channel 資訊
     * @since 0.0.3
     */
    MqCommonResp subscribe(final ConsumerSubscribeReq serviceEntry,
                   final Channel clientChannel);

    /**
     * 取消監聽服務資訊
     * (1)監聽之後,如果有任何相關的機器資訊發生變化,則進行推送。
     * (2)內建的資訊,需要傳送 ip 資訊到註冊中心。
     *
     * @param serviceEntry 客戶端明細資訊
     * @param clientChannel 客戶端 channel 資訊
     * @since 0.0.3
     */
    MqCommonResp unSubscribe(final ConsumerUnSubscribeReq serviceEntry,
                   final Channel clientChannel);

    /**
     * 獲取所有匹配的消費者
     * 1. 同一個 groupName 只返回一個,注意負載均衡
     * 2. 返回匹配當前訊息的消費者通道
     *
     * @param mqMessage 訊息體
     * @return 結果
     */
    List<Channel> getSubscribeList(MqMessage mqMessage);

}

預設實現:

package com.github.houbb.mq.broker.support.api;

/**
 * @author binbin.hou
 * @since 1.0.0
 */
public class LocalBrokerConsumerService implements IBrokerConsumerService {

    private final Map<String, BrokerServiceEntryChannel> registerMap = new ConcurrentHashMap<>();

    /**
     * 訂閱集合
     * key: topicName
     * value: 對應的訂閱列表
     */
    private final Map<String, Set<ConsumerSubscribeBo>> subscribeMap = new ConcurrentHashMap<>();

    @Override
    public MqCommonResp register(ServiceEntry serviceEntry, Channel channel) {
        final String channelId = ChannelUtil.getChannelId(channel);
        BrokerServiceEntryChannel entryChannel = InnerChannelUtils.buildEntryChannel(serviceEntry, channel);
        registerMap.put(channelId, entryChannel);

        MqCommonResp resp = new MqCommonResp();
        resp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
        resp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
        return resp;
    }

    @Override
    public MqCommonResp unRegister(ServiceEntry serviceEntry, Channel channel) {
        final String channelId = ChannelUtil.getChannelId(channel);
        registerMap.remove(channelId);

        MqCommonResp resp = new MqCommonResp();
        resp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
        resp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
        return resp;
    }

    @Override
    public MqCommonResp subscribe(ConsumerSubscribeReq serviceEntry, Channel clientChannel) {
        final String channelId = ChannelUtil.getChannelId(clientChannel);
        final String topicName = serviceEntry.getTopicName();

        Set<ConsumerSubscribeBo> set = subscribeMap.get(topicName);
        if(set == null) {
            set = new HashSet<>();
        }
        ConsumerSubscribeBo subscribeBo = new ConsumerSubscribeBo();
        subscribeBo.setChannelId(channelId);
        subscribeBo.setGroupName(serviceEntry.getGroupName());
        subscribeBo.setTopicName(topicName);
        subscribeBo.setTagRegex(serviceEntry.getTagRegex());
        set.add(subscribeBo);

        subscribeMap.put(topicName, set);

        MqCommonResp resp = new MqCommonResp();
        resp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
        resp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
        return resp;
    }

    @Override
    public MqCommonResp unSubscribe(ConsumerUnSubscribeReq serviceEntry, Channel clientChannel) {
        final String channelId = ChannelUtil.getChannelId(clientChannel);
        final String topicName = serviceEntry.getTopicName();

        ConsumerSubscribeBo subscribeBo = new ConsumerSubscribeBo();
        subscribeBo.setChannelId(channelId);
        subscribeBo.setGroupName(serviceEntry.getGroupName());
        subscribeBo.setTopicName(topicName);
        subscribeBo.setTagRegex(serviceEntry.getTagRegex());

        // 集合
        Set<ConsumerSubscribeBo> set = subscribeMap.get(topicName);
        if(CollectionUtil.isNotEmpty(set)) {
            set.remove(subscribeBo);
        }

        MqCommonResp resp = new MqCommonResp();
        resp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
        resp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
        return resp;
    }

    @Override
    public List<Channel> getSubscribeList(MqMessage mqMessage) {
        final String topicName = mqMessage.getTopic();
        Set<ConsumerSubscribeBo> set = subscribeMap.get(topicName);
        if(CollectionUtil.isEmpty(set)) {
            return Collections.emptyList();
        }

        //2. 獲取匹配的 tag 列表
        final List<String> tagNameList = mqMessage.getTags();

        Map<String, List<ConsumerSubscribeBo>> groupMap = new HashMap<>();
        for(ConsumerSubscribeBo bo : set) {
            String tagRegex = bo.getTagRegex();

            if(hasMatch(tagNameList, tagRegex)) {
                //TODO: 這種設定模式,統一新增處理
                String groupName = bo.getGroupName();
                List<ConsumerSubscribeBo> list = groupMap.get(groupName);
                if(list == null) {
                    list = new ArrayList<>();
                }
                list.add(bo);

                groupMap.put(groupName, list);
            }
        }

        //3. 按照 groupName 分組之後,每一組只隨機返回一個。最好應該調整為以 shardingkey 選擇
        final String shardingKey = mqMessage.getShardingKey();
        List<Channel> channelList = new ArrayList<>();

        for(Map.Entry<String, List<ConsumerSubscribeBo>> entry : groupMap.entrySet()) {
            List<ConsumerSubscribeBo> list = entry.getValue();

            ConsumerSubscribeBo bo = RandomUtils.random(list, shardingKey);
            BrokerServiceEntryChannel entryChannel = registerMap.get(bo.getChannelId());
            channelList.add(entryChannel.getChannel());
        }

        return channelList;
    }

    private boolean hasMatch(List<String> tagNameList,
                             String tagRegex) {
        if(CollectionUtil.isEmpty(tagNameList)) {
            return false;
        }

        Pattern pattern = Pattern.compile(tagRegex);

        for(String tagName : tagNameList) {
            if(RegexUtils.match(pattern, tagName)) {
                return true;
            }
        }

        return false;
    }

}

getSubscribeList 的邏輯可能稍微複雜點,其實就是訊息過來,找到匹配的訂閱消費者而已。

因為同一個 groupName 的消費者訊息只消費一次,所以需要一次分組。

訊息持久化

介面如下:

package com.github.houbb.mq.broker.support.persist;

/**
 * @author binbin.hou
 * @since 0.0.3
 */
public interface IMqBrokerPersist {

    /**
     * 儲存訊息
     * @param mqMessage 訊息
     * @since 0.0.3
     */
    MqCommonResp put(final MqMessagePersistPut mqMessage);

    /**
     * 更新狀態
     * @param messageId 訊息唯一標識
     * @param status 狀態
     * @return 結果
     * @since 0.0.3
     */
    MqCommonResp updateStatus(final String messageId,
                              final String status);

    /**
     * 拉取訊息
     * @param pull 拉取訊息
     * @return 結果
     */
    MqConsumerPullResp pull(final MqConsumerPullReq pull, final Channel channel);

}

本地預設實現:

package com.github.houbb.mq.broker.support.persist;

/**
 * 本地持久化策略
 * @author binbin.hou
 * @since 1.0.0
 */
public class LocalMqBrokerPersist implements IMqBrokerPersist {

    private static final Log log = LogFactory.getLog(LocalMqBrokerPersist.class);

    /**
     * 佇列
     * ps: 這裡只是簡化實現,暫時不考慮併發等問題。
     */
    private final Map<String, List<MqMessagePersistPut>> map = new ConcurrentHashMap<>();

    //1. 接收
    //2. 持久化
    //3. 通知消費
    @Override
    public synchronized MqCommonResp put(MqMessagePersistPut put) {
        log.info("put elem: {}", JSON.toJSON(put));

        MqMessage mqMessage = put.getMqMessage();
        final String topic = mqMessage.getTopic();

        List<MqMessagePersistPut> list = map.get(topic);
        if(list == null) {
            list = new ArrayList<>();
        }
        list.add(put);
        map.put(topic, list);

        MqCommonResp commonResp = new MqCommonResp();
        commonResp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
        commonResp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
        return commonResp;
    }

    @Override
    public MqCommonResp updateStatus(String messageId, String status) {
        // 這裡效能比較差,所以不可以用於生產。僅作為測試驗證
        for(List<MqMessagePersistPut> list : map.values()) {
            for(MqMessagePersistPut put : list) {
                MqMessage mqMessage = put.getMqMessage();
                if(mqMessage.getTraceId().equals(messageId)) {
                    put.setMessageStatus(status);

                    break;
                }
            }
        }

        MqCommonResp commonResp = new MqCommonResp();
        commonResp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
        commonResp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
        return commonResp;
    }

    @Override
    public MqConsumerPullResp pull(MqConsumerPullReq pull, Channel channel) {
        //TODO... 待實現
        return null;
    }

}

ps: 後續將會基於 springboot+mysql 進行持久化策略實現。

消費者啟動調整

我們將生產者、消費者的啟動都進行調整,連線到 broker 中。

二者是類似的,此處以消費者為例。

核心啟動類

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

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

    // 屬性&設定

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

        //1. 引數校驗
        this.paramCheck();

        try {
            // channel handler
            ChannelHandler channelHandler = this.initChannelHandler();

            //channel future
            this.channelFutureList = ChannelFutureUtils.initChannelFutureList(brokerAddress, channelHandler);

            // register to broker
            this.registerToBroker();

            // 標識為可用
            enableFlag = true;
            log.info("MQ 消費者啟動完成");
        } catch (Exception e) {
            log.error("MQ 消費者啟動異常", e);
            throw new MqException(ConsumerRespCode.RPC_INIT_FAILED);
        }
    }

    //訂閱&取消訂閱

    @Override
    public void registerListener(IMqConsumerListener listener) {
        this.mqListenerService.register(listener);
    }

}

初始化 handler

private ChannelHandler initChannelHandler() {
    final ByteBuf delimiterBuf = DelimiterUtil.getByteBuf(DelimiterUtil.DELIMITER);

    final MqConsumerHandler mqConsumerHandler = new MqConsumerHandler(invokeService, mqListenerService);
    // handler 實際上會被多次呼叫,如果不是 @Shareable,應該每次都重新建立。
    ChannelHandler handler = new ChannelInitializer<Channel>() {
        @Override
        protected void initChannel(Channel ch) throws Exception {
            ch.pipeline()
                    .addLast(new DelimiterBasedFrameDecoder(DelimiterUtil.LENGTH, delimiterBuf))
                    .addLast(mqConsumerHandler);
        }
    };

    return handler;
}

註冊到服務端

/**
 * 註冊到所有的服務端
 * @since 0.0.3
 */
private void registerToBroker() {
    for(RpcChannelFuture channelFuture : this.channelFutureList) {
        ServiceEntry serviceEntry = new ServiceEntry();
        serviceEntry.setGroupName(groupName);
        serviceEntry.setAddress(channelFuture.getAddress());
        serviceEntry.setPort(channelFuture.getPort());
        serviceEntry.setWeight(channelFuture.getWeight());

        BrokerRegisterReq brokerRegisterReq = new BrokerRegisterReq();
        brokerRegisterReq.setServiceEntry(serviceEntry);
        brokerRegisterReq.setMethodType(MethodType.C_REGISTER);
        brokerRegisterReq.setTraceId(IdHelper.uuid32());

        log.info("[Register] 開始註冊到 broker:{}", JSON.toJSON(brokerRegisterReq));
        final Channel channel = channelFuture.getChannelFuture().channel();
        MqCommonResp resp = callServer(channel, brokerRegisterReq, MqCommonResp.class);
        log.info("[Register] 完成註冊到 broker:{}", JSON.toJSON(resp));
    }
}

訂閱與取消訂閱

消費者對於關心的訊息,實現也比較簡單:

public void subscribe(String topicName, String tagRegex) {
    ConsumerSubscribeReq req = new ConsumerSubscribeReq();
    String messageId = IdHelper.uuid32();
    req.setTraceId(messageId);
    req.setMethodType(MethodType.C_SUBSCRIBE);
    req.setTopicName(topicName);
    req.setTagRegex(tagRegex);
    req.setGroupName(groupName);

    Channel channel = getChannel();

    MqCommonResp resp = callServer(channel, req, MqCommonResp.class);
    if(!MqCommonRespCode.SUCCESS.getCode().equals(resp.getRespCode())) {
        throw new MqException(ConsumerRespCode.SUBSCRIBE_FAILED);
    }
}

取消訂閱:

public void unSubscribe(String topicName, String tagRegex) {
    ConsumerUnSubscribeReq req = new ConsumerUnSubscribeReq();
    String messageId = IdHelper.uuid32();
    req.setTraceId(messageId);
    req.setMethodType(MethodType.C_UN_SUBSCRIBE);
    req.setTopicName(topicName);
    req.setTagRegex(tagRegex);
    req.setGroupName(groupName);

    Channel channel = getChannel();

    MqCommonResp resp = callServer(channel, req, MqCommonResp.class);
    if(!MqCommonRespCode.SUCCESS.getCode().equals(resp.getRespCode())) {
        throw new MqException(ConsumerRespCode.UN_SUBSCRIBE_FAILED);
    }
}

測試

broker 啟動

MqBroker broker = new MqBroker();
broker.start();

啟動日誌:

[DEBUG] [2022-04-21 20:36:27.158] [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 20:36:27.186] [Thread-0] [c.g.h.m.b.c.MqBroker.run] - MQ 中間人開始啟動服務端 port: 9999
[INFO] [2022-04-21 20:36:29.060] [Thread-0] [c.g.h.m.b.c.MqBroker.run] - MQ 中間人啟動完成,監聽【9999】埠

consumer 啟動

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

mqConsumerPush.subscribe("TOPIC", "TAGA");
mqConsumerPush.registerListener(new IMqConsumerListener() {
    @Override
    public ConsumerStatus consumer(MqMessage mqMessage, IMqConsumerListenerContext context) {
        System.out.println("---------- 自定義 " + JSON.toJSONString(mqMessage));
        return ConsumerStatus.SUCCESS;
    }
});

啟動日誌:

...
[INFO] [2022-04-21 20:37:40.985] [Thread-0] [c.g.h.m.c.c.MqConsumerPush.registerToBroker] - [Register] 完成註冊到 broker:{"respMessage":"成功","respCode":"0000"}

啟動時會註冊到 broker。

producer 啟動

MqProducer mqProducer = new MqProducer();
mqProducer.start();
String message = "HELLO MQ!";
MqMessage mqMessage = new MqMessage();
mqMessage.setTopic("TOPIC");
mqMessage.setTags(Arrays.asList("TAGA", "TAGB"));
mqMessage.setPayload(message);

SendResult sendResult = mqProducer.send(mqMessage);

System.out.println(JSON.toJSON(sendResult));

日誌:

...
[INFO] [2022-04-21 20:39:17.885] [Thread-0] [c.g.h.m.p.c.MqProducer.registerToBroker] - [Register] 完成註冊到 broker:{"respMessage":"成功","respCode":"0000"}
...

此時消費者消費到我們傳送的訊息。

---------- 自定義 {"methodType":"B_MESSAGE_PUSH","payload":"HELLO MQ!","tags":["TAGA","TAGB"],"topic":"TOPIC","traceId":"2237bbfe55b842328134e6a100e36364"}

小結

到這裡,我們就實現了基於中間人的生產者與消費者通訊。

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

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

開源地址

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

擴充閱讀

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

相關文章