Canal 是阿里开源的框架,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。
Canal的工作原理
把自己伪装成MySQL slave,模拟MySQL slave的交互协议向MySQL Mater发送 dump协议,MySQL mater收到canal发送过来的dump请求,开始推送bin_log给canal,然后canal解析bin_log,再发送到存储目的地,比如MySQL,Kafka,Elastic Search等等。
Canal的适用场景
- 数据库实时备份和维护
- 带业务逻辑的增量数据处理
本地启动 MySQL,并修改对应的配置
下面就是对应的 docker-compose.yaml
version: '2'
services:
mysql:
image: mariadb:10.3.34
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_USER: mysql
MYSQL_PASSWORD: mysql
MYSQL_DATABASE: test
MYSQL_BIND_ADDRESS: 0.0.0.0
TZ: Asia/Shanghai
command:
- --lower_case_table_names=1
- --character-set-server=utf8
ports:
- 3306:3306
volumes: ## 这里挂载目录修改为自己的目录
- "/data/docker-script/mariadb_data:/var/lib/mysql"
修改 /etc/mysql/my.cnf 配置
server-id=1
log_bin=mysql-bin
binlog_format=row
重启 mysql
docker restart {mysql container id}
创建 canal 用户,并赋予权限
create user 'canal'@'%' identified by 'Canal@123456';
grant SELECT, REPLICATION SLAVE, REPLICATION CLIENT on *.* to 'canal'@'%' identified by 'Canal@123456';
本地启动 Canal Server
拉取 canal-server 镜像
docker pull canal/canal-server:1.1.4
启动
构建一个destination name为test的队列
docker run -d -it -h 127.0.0.1 -e canal.auto.scan=false \
-e canal.destinations=test \
-e canal.instance.master.address=127.0.0.1:3306 \
-e canal.instance.dbUsername=canal \
-e canal.instance.dbPassword=Canal@123456 \
-e canal.instance.connectionCharset=UTF-8 \
-e canal.instance.tsdb.enable=true \
-e canal.instance.gtidon=false \
--name=canal-server -p 11110:11110 -p 11111:11111 -p 11112:11112 -p 9100:9100 -m 4096m canal/canal-server
本地启动 Canal Client
添加 canal maven
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.4</version>
</dependency>
添加 Canal Client Code
public class SimpleCanalClientExample {
public static void main(String args[]) throws CanalClientException {
// 创建链接 指定 canal server 地址
CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("127.0.0.1",
11111), "test", "", "");
int batchSize = 1000;
int emptyCount = 0;
try {
connector.connect();
connector.subscribe("tsoc\..*");
connector.rollback();
int totalEmptyCount = 1200;
while (emptyCount < totalEmptyCount) {
Message message = connector.getWithoutAck(batchSize); // 获取指定数量的数据
long batchId = message.getId();
int size = message.getEntries().size();
if ((batchId == -1 || size == 0) && emptyCount % 10 == 0) {
emptyCount++;
System.out.println("-- empty count : " + emptyCount);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
}
} else {
emptyCount = 0;
// System.out.printf("message[batchId=%s,size=%s] \n", batchId, size);
printEntry(message.getEntries());
}
connector.ack(batchId); // 提交确认
// connector.rollback(batchId); // 处理失败, 回滚数据
}
System.out.println("empty too many times, exit");
} finally {
connector.disconnect();
}
}
private static void printEntry(List<Entry> entrys) {
for (Entry entry : entrys) {
if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) {
continue;
}
RowChange rowChage = null;
try {
rowChage = RowChange.parseFrom(entry.getStoreValue());
String sql = rowChage.getSql();
System.out.println(sql);
} catch (Exception e) {
throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(),
e);
}
EventType eventType = rowChage.getEventType();
// System.out.println(String.format("================> binlog[%s:%s] , name[%s,%s] , eventType : %s",
// entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(),
// entry.getHeader().getSchemaName(), entry.getHeader().getTableName(),
// eventType));
for (RowData rowData : rowChage.getRowDatasList()) {
if (eventType == EventType.DELETE) {
printColumn(rowData.getBeforeColumnsList());
} else if (eventType == EventType.INSERT) {
printColumn(rowData.getAfterColumnsList());
} else {
// System.out.println("-------> before");
printColumn(rowData.getBeforeColumnsList());
// System.out.println("-------> after");
printColumn(rowData.getAfterColumnsList());
}
}
}
}
private static void printColumn(List<Column> columns) {
for (Column column : columns) {
// System.out.println(column.getName() + " : " + column.getValue() + " update=" + column.getUpdated());
}
}
}