數據的異構實戰(二)手寫迷你版同步工程

上一期講到了通過canal訂閱mysql的binlog日誌並且轉換爲對象,那麼這一次我們將訂閱來的對象通過RocketMQ發送消息,接收方接受消息之後同時存儲到其他類型的數據源當中,完成一個簡單的數據異構的過程。
在這裏插入圖片描述

什麼是Java消息服務?
兩個應用程序之間進行異步通信的API,它爲標準消息協議和消息服務提供了一組通用接口,包括創建、發送、讀取消息等,用於支持JAVA應用程序開發。在J2EE中,當兩個應用程序使用JMS進行通信時,它們之間並不是直接相連的,而是通過一個共同的消息收發服務連接起來,可以達到解耦的效果,我們將會在接下來的教程中詳細介紹。

jms的消息傳送模型
常見的消息傳送模型有以下兩種:

點對點消息傳送模型
在點對點消息傳送模型中,應用程序由消息隊列,發送者,接收者組成。每一個消息發送給一個特殊的消息隊列,該隊列保存了所有發送給它的消息(除了被接收者消費掉的和過期的消息)。如下圖所示:
在這裏插入圖片描述
發佈訂閱消息傳送模型
在發佈訂閱模型中,消費者需要訂閱相關的topic才能接收到生產者的信息。生產者會將信息傳輸到topic中,然後消費者只需要從topic中獲取數據即可。如下圖所示:
在這裏插入圖片描述

RocketMQ消息隊列使用
這次使用的消息中間件爲RocketMQ的使用場景。RocketMQ是阿里巴巴在2012年開源的分佈式消息中間件,目前已經捐贈給Apache基金會,並於2016年11月成爲 Apache 孵化項目。

RocketMQ在使用之前,需要我們引入相關的依賴配置:

    <!-- 整合RocketMq -->
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-client</artifactId>
            <version>${rocketmq.version}</version>
        </dependency>

關於RocketMQ的安裝在這裏就不做過多的講解了。

通過mq的方式來進行數據異構通常是比較簡單的方案,首先我們需要在項目裏面獨立一個模塊專門用於監聽mysql的binlog日誌,這個模塊我暫且稱之爲datahandle-core模塊
在這裏插入圖片描述
整個工程採用了springboot的結構來構建,主要的核心也是在core工程中。
首先是監聽canal的日誌狀態模塊了,採用了上一節中講解到的客戶端代碼進行數據監聽,並且將其轉換爲對象然後發送往mq中:

package com.sise.datahandle.core;

import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.Message;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.net.InetSocketAddress;

import static com.sise.datahandle.constants.CanalConstants.*;

/**
 * @author idea
 * @date 2019/10/20
 */
@Component
@Slf4j
public class CanalListener implements CommandLineRunner {

    @Autowired
    private CanalClient canalClient;

    @Override
    public void run(String... args) throws Exception {
      log.info("=============canal監聽器開啓===============");
        CanalConnector canalConnector = CanalConnectors.newSingleConnector(
                new InetSocketAddress(SERVER_ADDRESS, PORT), DESTINATION, USERNAME, PASSWORD);
        canalConnector.connect();
        canalConnector.subscribe(".*\\..*");
        canalConnector.rollback();
        for (; ; ) {
            Message message = canalConnector.getWithoutAck(100);
            long batchId = message.getId();
            if (batchId != -1) {
                canalClient.entityHandle(message.getEntries());
            }
        }
    }
}

ps:這裏面的CanalClient代碼主要來自上一篇的canal客戶端代碼,文末會有完整項目代碼鏈接,需要的讀者可以前往查看。
在CanalClient裏面,有一個函數是專門用於處理將訂閱的數據發送到mq消息隊列中:

package com.sise.datahandle.core;

import com.alibaba.fastjson.JSON;
import com.alibaba.otter.canal.protocol.CanalEntry;
import com.google.protobuf.InvalidProtocolBufferException;
import com.sise.datahandle.handler.CanalDataHandler;
import com.sise.datahandle.model.TypeDTO;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.remoting.exception.RemotingException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * canal監聽客戶端變化
 *
 * @author idea
 * @date 2019/10/12
 */
@Slf4j
@Service
public class CanalClient {


    @Autowired
    private DefaultMQProducer rocketMqProducer;


    /**
     * 處理binlog日誌的監聽
     *
     * @param entries
     */
    public void entityHandle(List<CanalEntry.Entry> entries) {
        for (CanalEntry.Entry entry : entries) {
            if (entry.getEntryType() != CanalEntry.EntryType.ROWDATA) {
                continue;
            }
            try {
                CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
                for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
                    switch (rowChange.getEventType()) {
                        case INSERT:
                            String tableName = entry.getHeader().getTableName();
                            //測試選用t_type這張表進行映射處理
                            if ("t_type".equals(tableName)) {
                                TypeDTO typeDTO = CanalDataHandler.convertToBean(rowData.getAfterColumnsList(), TypeDTO.class);
                                org.apache.rocketmq.common.message.Message message = new org.apache.rocketmq.common.message.Message();
                                message.setTopic("canal-test-topic");
                                message.setTags("canal-test-tag");
                                String json = JSON.toJSONString(typeDTO);
                                message.setBody(json.getBytes());
                                SendResult sendResult = rocketMqProducer.send(message);
                                log.info("[mq消息發送結果]----" + sendResult);
                            }
                            break;
                        default:
                            break;
                    }
                }
            } catch (InvalidProtocolBufferException e) {
                log.error("[CanalClient]監聽數據過程出現異常,異常信息爲{}", e);
            } catch (InterruptedException | RemotingException | MQClientException | MQBrokerException e) {
                log.error("[CanalClient] mq發送信息出現異常:{}", e);
            }
        }
    }

}

這裏面主要是監聽binlog記錄爲插入數據事件的時候做發送mq操作。

接下來便是常見的mq配置了,本工程主要是一個模擬的簡單案例,因此我將consumer和producer都放在了一起方便測試。
在這裏插入圖片描述
通過springboot自身的properties文件對mq進行參數初始化配置之後便可以構建一個基本的consumer和producer了。這裏我們哪一個TypeDto類來進行樹異構的測試,consumer端的核心代碼爲:

package com.sise.datahandle.mq.rocketmq.consumer;

import com.sise.datahandle.model.TypeDTO;
import com.sise.datahandle.mq.rocketmq.producer.RocketMqMsgHandle;
import com.sise.datahandle.redis.RedisService;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.List;

/**
 * @author idea
 * @date 2019/10/20
 */
@Component
@Slf4j
public class RocketMqConsumeMsgListenerProcessor implements MessageListenerConcurrently {

    @Autowired
    private RedisService redisService;

    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
        if(CollectionUtils.isEmpty(msgs)){
            log.info("接受到的消息爲空,不處理,直接返回成功");
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }
        MessageExt messageExt = msgs.get(0);
        System.out.println("接受到的消息爲:"+messageExt.toString());
        if("canal-test-topic".equals(messageExt.getTopic())){
            if("canal-test-tag".equals(messageExt.getTags())){
                int reconsume = messageExt.getReconsumeTimes();
                if(reconsume ==3){//消息已經重試了3次,如果不需要再次消費,則返回成功
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                }
                TypeDTO typeDTO = RocketMqMsgHandle.parseMessage(messageExt,TypeDTO.class);
                //存儲進入redis中
                redisService.setObject("typeDTO-"+System.currentTimeMillis(),typeDTO);
            }
        }
        // 如果沒有return success ,consumer會重新消費該消息,直到return success
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
}

通過訂閱mq的信息,讀取相關的數據再次寫入到redis裏面,完成一個簡單過程的數據異構。
整個迷你工程寫下來,比較核心的地方就在於對binlog日誌的解析器部分,如何將日誌訂閱之後轉換爲相應的對象進行處理。通常採用mq的方式進行數據異構會相對簡單,實際上是在監聽binlog爲寫DB的同時去寫一次MQ,但是這種方式不能夠保證數據一致性,就是不能保證跨資源的事務。注:調用第三方遠程RPC的操作一定不要放到事務中。

完整案例的代碼鏈接如下:
https://gitee.com/IdeaHome_admin/wfw

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章