坚持原创,共同进步!请关注我,后续分享更精彩!!!
canal是阿里开源的一款mysql数据同步工具。纯java开发,基于数据库增量日志解析,提供增量数据的订阅&消费,主要支持mysql。可应用以下业务场景:
mysql工作原理:
从上层来看,复制分成三步:
canal工作原理:
通过以上原理,很容易理解canal的数据同步逻辑:就是把canal实例伪装为mysql的一个从库实例,通过mysql自有的数据同步机制来达到数据同步的目的。
上图可知canal内部设计为server-client的结构。
可把同步数据直接透传到kafka和rocket mq队列。
可通过client端api定制消费mysql同步数据。可也通过adapter api扩展实现HBASE、Rdb、ES的数据同步。
本文通过java client api方式来讲解canal数据消费。
自建 MySQL , 需要先开启 Binlog 写入功能,配置 binlog-format 为 ROW 模式,my.cnf 中配置如下
[mysqld]log-bin=mysql-bin # 开启 binlogbinlog-format=ROW # 选择 ROW 模式server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复
注意:针对阿里云 RDS for MySQL , 默认打开了 binlog , 并且账号默认具有 binlog dump 权限 , 不需要任何权限或者 binlog 设置,可以直接跳过这一步
授权 canal 连接 MySQL 账号具有作为 MySQL slave 的权限, 如果已有账户可直接 grant
CREATE USER canal IDENTIFIED BY 'canal'; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';-- GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' ;FLUSH PRIVILEGES;
canal image tag可参考以下地址:
https://hub.docker.com/r/canal/canal-server/tags/
运行docker容器
docker run -d -p 9100:9100 -p 11110:11110 -p 11111:11111 -p 11112:11112 -e canal.auto.scan=false -e canal.destinations=demo -e canal.instance.master.address=192.168.**.**:3306 -e canal.instance.dbUsername=canal -e canal.instance.dbPassword=canal -e canal.instance.connectionCharset=UTF-8 -e canal.instance.tsdb.enable=true -e canal.instance.gtidon=false -e canal.instance.filter.regex=.*..* -v /data/canal-server/logs:/home/admin/canal-server/logs --restart=always --name=canal-server canal/canal-server:v1.1.4
canal.destinations :canal server实例名。
canal.instance.master.address :mysql主库地址。请确保mysql服务器3306端口对外开放。
canal.instance.dbUsername:mysql主库访问账号
canal.instance.dbPassword:mysql主库访问密码
canal.instance.filter.regex:mysql变更sql过滤规则,支持正则表达式。.*..* 表示所有主库的库和表的变更均会同步。
其他canal配置项可参考官方文档:
https://github.com/alibaba/canal/wiki/AdminGuide
java项目添加依赖
com.alibaba.otter canal.client 1.1.4
编写测试类
public class CanalTest { @Test public void testCanalClient(){ // 创建链接 CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("192.168.0.114", 11111), "demo", "", ""); int batchSize = 1000; int emptyCount = 0; try { connector.connect(); connector.subscribe(".*..*"); connector.rollback(); int totalEmptyCount = 120; while (emptyCount < totalEmptyCount) { Message message = connector.getWithoutAck(batchSize); // 获取指定数量的数据 long batchId = message.getId(); int size = message.getEntries().size(); if (batchId == -1 || size == 0) { emptyCount++; System.out.println("empty count : " + emptyCount); try { Thread.sleep(1000); } catch (InterruptedException e) { } } else { emptyCount = 0; // System.out.printf("message[batchId=%s,size=%s] ", 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 entrys) { for (Entry entry : entrys) { if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) { continue; } RowChange rowChage = null; try { rowChage = RowChange.parseFrom(entry.getStoreValue()); } 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 columns) { for (Column column : columns) { System.out.println(column.getName() + " : " + column.getValue() + " update=" + column.getUpdated()); } }}
执行test类,新增mysql主库表记录。
查看执行结果:
新增的sql数据打印在console面板中,数据的增量更新已同步。
本文介绍了canal的原理以及框架结构。通过docker安装canal server实例,并用client api方式消费canal端同步的数据。Canal巧妙的设计,灵活的client api可满足各类数据同步的业务场景。
最后希望本文讲解能给大家以参考和帮助。