Springboot2(44)集成canal

源碼地址

springboot2教程系列

canal高可用部署安裝和配置參數詳解

前言

canal是阿里巴巴的基於數據庫增量日誌解析,提供增量數據訂閱&消費,目前主要支持了mysql。

可以用於比如數據庫數據變化的監聽從而同步緩存(如Redis)數據等。

由於項目中基本都是使用的Spring-Boot,所以寫了一個基於Spring-Boot的starter方便使用。

特點

使用方便。可以通過簡單的配置就可以開始使用,當對某些操作感興趣的時候可以通過註解或者注入接口實現的方式監聽對應的事件。

實現

註解方式 (insert爲例)

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ListenPoint(eventType = CanalEntry.EventType.INSERT)
public @interface InsertListenPoint {

    /**
     * canal 指令
     * default for all
     *
     */
    @AliasFor(annotation = ListenPoint.class)
    String destination() default "";

    /**
     * 數據庫實例
     *
     */
    @AliasFor(annotation = ListenPoint.class)
    String[] schema() default {};

    /**
     * 監聽的表
     * default for all
     *
     */
    @AliasFor(annotation = ListenPoint.class)
    String[] table() default {};

}

創建canal客戶端(連接實例)

private CanalConnector processInstanceEntry(Map.Entry<String, CanalProperties.Instance> instanceEntry) {
		//獲取配置
		CanalProperties.Instance instance = instanceEntry.getValue();
		//聲明連接
		CanalConnector connector;
		//是否是集羣模式
		if (instance.isClusterEnabled()) {
			//zookeeper 連接集合
			for (String s : instance.getZookeeperAddress()) {
				String[] entry = s.split(":");
				if (entry.length != 2) {
					throw new CanalClientException("zookeeper 地址格式不正確,應該爲 ip:port....:" + s);
				}
			}
			//若集羣的話,使用 newClusterConnector 方法初始化
			connector = CanalConnectors.newClusterConnector(StringUtils.join(instance.getZookeeperAddress(), ","), instanceEntry.getKey(), instance.getUserName(), instance.getPassword());
		} else {
			//若不是集羣的話,使用 newSingleConnector 初始化
			connector = CanalConnectors.newSingleConnector(new InetSocketAddress(instance.getHost(), instance.getPort()), instanceEntry.getKey(), instance.getUserName(), instance.getPassword());
		}
		//canal 連接
		connector.connect();
		if (!StringUtils.isEmpty(instance.getFilter())) {
			//canal 連接訂閱,包含過濾規則
			connector.subscribe(instance.getFilter());
		} else {
			//canal 連接訂閱,無過濾規則
			connector.subscribe();
		}
		
		//canal 連接反轉
		connector.rollback();
		//返回 canal 連接
		return connector;
	}

獲取數據

public void run() {
		//錯誤重試次數
		int errorCount = config.getRetryCount();
		//捕獲信息的心跳時間
		final long interval = config.getAcquireInterval();
		//當前線程的名字
		final String threadName = Thread.currentThread().getName();
		//若線程正在進行
		while (running && !Thread.currentThread().isInterrupted()) {
			try {
				//獲取消息
				Message message = connector.getWithoutAck(config.getBatchSize());
				//獲取消息 ID
				long batchId = message.getId();
				//消息數
				int size = message.getEntries().size();
				//debug 模式打印消息數
				if (logger.isDebugEnabled()) {
					logger.debug("{}: 從 canal 服務器獲取消息: >>>>> 數:{}", threadName, size);
				}
				//若是沒有消息
				if (batchId == -1 || size == 0) {
					if (logger.isDebugEnabled()) {
						logger.debug("{}: 沒有任何消息啊,我休息{}毫秒", threadName, interval);
					}
					//休息
					Thread.sleep(interval);
				} else {
					//處理消息
					distributeEvent(message);
				}
				//確認消息已被處理完
				connector.ack(batchId);
				//若是 debug模式
				if (logger.isDebugEnabled()) {
					logger.debug("{}: 確認消息已被消費,消息ID:{}", threadName, batchId);
				}
			} catch (CanalClientException e) {
				//每次錯誤,重試次數減一處理
				errorCount--;
				logger.error(threadName + ": 發生錯誤!! ", e);
				try {
					//等待時間
					Thread.sleep(interval);
				} catch (InterruptedException e1) {
					errorCount = 0;
				}
			} catch (InterruptedException e) {
				//線程中止處理
				errorCount = 0;
				connector.rollback();
			} finally {
				//若錯誤次數小於 0
				if (errorCount <= 0) {
					//停止 canal 客戶端
					stop();
					logger.info("{}: canal 客戶端已停止... ", Thread.currentThread().getName());
				}
			}
		}
		//停止 canal 客戶端
		stop();
		logger.info("{}: canal 客戶端已停止. ", Thread.currentThread().getName());
	}

把數據注入到相應的註解方法處理

/**
	 * 處理註解方式的 canal 監聽器
	 *
	 * @param destination canal 指令
	 * @param schemaName  實例名稱
	 * @param tableName   表名稱
	 * @param rowChange   數據
	 * @return
	 */
	protected void distributeByAnnotation(String destination,
										  String schemaName,
										  String tableName,
										  CanalEntry.RowChange rowChange) {

		//對註解的監聽器進行事件委託
		if (!CollectionUtils.isEmpty(annoListeners)) {
			annoListeners.forEach(point -> point
					.getInvokeMap()
					.entrySet()
					.stream()
					.filter(getAnnotationFilter(destination, schemaName, tableName, rowChange.getEventType()))
					.forEach(entry -> {
						Method method = entry.getKey();
						method.setAccessible(true);
						try {
							CanalMsg canalMsg = new CanalMsg();
							canalMsg.setDestination(destination);
							canalMsg.setSchemaName(schemaName);
							canalMsg.setTableName(tableName);

							Object[] args = getInvokeArgs(method, canalMsg, rowChange);
							method.invoke(point.getTarget(), args);
						} catch (Exception e) {
							
						}
					}));
		}
	}

註解監聽的使用

/**
 * 註解方法測試
 *
 */
@CanalEventListener
public class MyAnnoEventListener {
	
	@InsertListenPoint
	public void onEventInsertData(CanalMsg canalMsg, CanalEntry.RowChange rowChange) {
		System.out.println("======================註解方式(新增數據操作)==========================");
		List<CanalEntry.RowData> rowDatasList = rowChange.getRowDatasList();
		for (CanalEntry.RowData rowData : rowDatasList) {
			String sql = "use " + canalMsg.getSchemaName() + ";\n";
			StringBuffer colums = new StringBuffer();
			StringBuffer values = new StringBuffer();
			rowData.getAfterColumnsList().forEach((c) -> {
				colums.append(c.getName() + ",");
				values.append("'" + c.getValue() + "',");
			});
			
			
			sql += "INSERT INTO " + canalMsg.getTableName() + "(" + colums.substring(0, colums.length() - 1) + ") VALUES(" + values.substring(0, values.length() - 1) + ");";
			System.out.println(sql);
		}
		System.out.println("\n======================================================");
		
	}
	
	
}

使用方法

  • 注意:基於已經有了數據庫環境和canal-server環境的前提。

  • 獲取源碼。將源碼中的starter-canl項目打包引入或者通過maven安裝到倉庫。
    在自己的Spring-Boot項目中:
    加入配置

    #false時爲單機模式,true時爲zookeeper高可用模式
    canal.client.instances.example.clusterEnabled: true
    #canal.client.instances.example.host: 10.10.2.137
    #zookeeper 地址
    canal.client.instances.example.zookeeperAddress: 10.10.2.137:2181,10.10.2.138:2181,10.10.2.139:2181
    
    canal.client.instances.example.port: 11111
    canal.client.instances.example.batchSize: 1000
    canal.client.instances.example.acquireInterval: 1000
    canal.client.instances.example.retryCount: 20
    

    編寫自己的Listener(參照canal-test中的MyEventListener)
    啓動。—》OK!

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