上一期講到了通過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