源码地址
springboot2教程系列
canal高可用部署安装和配置参数详解
canal是阿里巴巴的基于数据库增量日志解析,提供增量数据订阅&消费,目前主要支持了mysql。
可以用于比如数据库数据变化的监听从而同步缓存(如Redis)数据等。
由于项目中基本都是使用的Spring-Boot,所以写了一个基于Spring-Boot的starter方便使用。
使用方便。可以通过简单的配置就可以开始使用,当对某些操作感兴趣的时候可以通过注解或者注入接口实现的方式监听对应的事件。
@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 {};
}
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!