前言
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!