一、Canal介绍
canal可以用来监控数据库数据的变化,从而获得新增数据,或者修改的数据。
canal是应阿里巴巴存在杭州和美国的双机房部署,存在跨机房同步的业务需求而提出的。
阿里系公司开始逐步的尝试基于数据库的日志解析,获取增量变更进行同步,由此衍生出了增量订阅&消费的业务。
canal主要用途是基于 MySQL 数据库增量日志解析,并能提供增量数据订阅和消费,应用场景十分丰富。
目前canal主要支持mysql数据库。
github地址:https://github.com/alibaba/canal
版本下载地址:https://github.com/alibaba/canal/releases
文档地址:https://github.com/alibaba/canal/wiki/Docker-QuickStart
Canal应用场景
1)、电商场景下商品、用户实时更新同步到至Elasticsearch、solr等搜索引擎;
2)、价格、库存发生变更实时同步到redis;
3)、数据库异地备份、数据同步;
4)、代替使用轮询数据库方式来监控数据库变更,有效改善轮询耗费数据库资源。
MySQL主从复制原理
1)、MySQL master
将数据变更写入二进制日志( binary log
, 其中记录叫做二进制日志事件binary log events
,可以通过 show binlog events
进行查看)
2)、MySQL slave
将 master 的 binary log events
拷贝到它的中继日志(relay log
)
3)、MySQL slave
重放 relay log
中事件,将数据变更反映它自己的数据
Canal工作原理
1)、canal 模拟 MySQL slave
的交互协议,伪装自己为 MySQL slave
,向 MySQL master
发送dump 协议
2)、MySQL master
收到 dump 请求,开始推送 binary log
给 slave (即 canal )
3)、canal 解析 binary log
对象(原始为 byte 流)
二、环境部署
我的环境:mysql8.0.26,es6.2.1,canal-adapter 1.1.5,canal-deployer1.1.5
1、mysql开启binlog模式
(1)查看当前mysql是否开启binlog模式。
SHOW VARIABLES LIKE '%log_bin%'
如果 log_bin的值为OFF是未开启,为ON是已开启。binlog开启之后,mysql才会对它内部的添加修改删除操作都记录到binlog日志中。
(2)修改/etc/my.cnf 需要开启binlog模式。
修改mysql配置文件,找到 MySQL 配置文件的位置
Linux: /etc/my.cn,如果/etc 目录下没有,可以通过 locate my.cnf 查找位置
[mysqld] log-bin=mysql-bin binlog-format=ROW server_id=1
Windows: \my.ini,在 mysql 的配置文件下,修改配置
在[mysqld] 区块
log-bin=mysql-bin #添加这一行就ok binlog-format=ROW #选择row模式 server_id=1 #配置mysql replaction需要定义,不能和canal的slaveId重复 binlog-do-db=canal_manager # 需要监控的数据库名称
注意:(1)、监控的数据库名称一定要正确。(2)、server_id配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复
查看值得配置是否开启
SHOW VARIABLES LIKE 'binlog_format'; -- 结果应该是ROW SHOW VARIABLES LIKE 'log_bin'; -- 结果应该是 ON SHOW VARIABLES LIKE '%log%'; -- 所有binlog信息
(1)这个表示binlog日志的前缀是 mysql-bin , 以后生成的日志文件就是mysql-bin.123456 的文件后面的数字按顺序生成,每次 mysql 重启或者到达单个文件大小的阈值时,新生一个文件,按顺序编号。
(2)mysql binlog 的格式有三种,
binlog_format= statement|mixed|row
◼ statement
update tt set create_date=now()
如果用 binlog 日志进行恢复,由于执行时间不同可能产生的数据不同。
优点: 节省空间
缺点: 有可能造成数据不一致。
◼ row
行级, binlog 会记录每次操作后每行记录的变化。
优点:保持数据的绝对一致性。因为不管 sql 是什么,引用了什么函数,只记录执行后的效果。
缺点:占用较大空间。
◼ mixed
statement 的升级版,一定程度上解决了,因为一些情况而造成的 statement 模式不一致问题.
默认还是 statement,在某些情况下譬如:当函数中包含 UUID() 时; 包含 AUTO_INCREMENT 字段的表被更新时; 执行 INSERT DELAYED 语句时;用 UDF 时;会按照 ROW 的方式进行处理。
优点:节省空间,同时兼顾了一定的一致性。
缺点:还有些极个别情况依旧会造成不一致,另外 statement 和 mixed 对于需要对 binlog 的监控的情况都不方便。
综合上面对比,Cannel 想做监控分析,选择 row 格式比较合适
(3)、重启mysql服务
(4)、创建账号 用于测试使用
进入mysql
mysql -h localhost -u root -p
使用root账号创建用户并授予权限
create user canal@'%' IDENTIFIED by 'canal'; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%'; FLUSH PRIVILEGES;
结果:
三、Canal安装
参考文档:https://github.com/alibaba/canal/wiki/QuickStart
windows下载:https://github.com/alibaba/canal/releases/
canal1.1.6版本并不能兼容java8,只能用低版本的canal1.1.5
百度网盘下载地址:
链接:https://pan.baidu.com/s/1vRjd-Ra68cI8BmxDNUNi5w 提取码:d01u --来自百度网盘超级会员V5的分享
建议下载:canal.deployer-1.1.5-SNAPSHOT.tar.gz和canal.adapter-1.1.5-SNAPSHOT.tar.gz。
(1)、修改 exmaple下的实例配置
编辑instance.properties,
配置要监听的数据库服务地址和监听数据变化的数据库以及表,修改如下:
## mysql serverId , v1.0.26+ will autoGen canal.instance.mysql.slaveId=8888 # enable gtid use true/false canal.instance.gtidon=false # position info canal.instance.master.address=127.0.0.1:3306 canal.instance.master.journal.name= canal.instance.master.position= canal.instance.master.timestamp= canal.instance.master.gtid= # rds oss binlog canal.instance.rds.accesskey= canal.instance.rds.secretkey= canal.instance.rds.instanceId= # table meta tsdb info canal.instance.tsdb.enable=true #canal.instance.tsdb.url=jdbc:mysql://127.0.0.1:3306/canal_tsdb #canal.instance.tsdb.dbUsername=canal #canal.instance.tsdb.dbPassword=canal #canal.instance.standby.address = #canal.instance.standby.journal.name = #canal.instance.standby.position = #canal.instance.standby.timestamp = #canal.instance.standby.gtid= # username/password canal.instance.dbUsername=canal canal.instance.dbPassword=canal canal.instance.connectionCharset = UTF-8 # enable druid Decrypt database password canal.instance.enableDruid=false #canal.instance.pwdPublicKey=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALK4BUxdDltRRE5/zXpVEVPUgunvscYFtEip3pmLlhrWpacX7y7GCMo2/JM6LeHmiiNdH1FWgGCpUfircSwlWKUCAwEAAQ== # table regex canal.instance.filter.regex=canal_manager\\..* # table black regex canal.instance.filter.black.regex=mysql\\.slave_.* # table field filter(format: schema1.tableName1:field1/field2,schema2.tableName2:field1/field2) #canal.instance.filter.field=test1.t_product:id/subject/keywords,test2.t_company:id/name/contact/ch # table field black filter(format: schema1.tableName1:field1/field2,schema2.tableName2:field1/field2) #canal.instance.filter.black.field=test1.t_product:subject/product_image,test2.t_company:id/name/contact/ch # mq config canal.mq.topic=example # dynamic topic route by schema or table regex #canal.mq.dynamicTopic=mytest1.user,mytest2\\..*,.*\\..* canal.mq.partition=0
默认就是我们前面授权的 canal
指定监听数据库表的配置如下canal.instance.filter.regex
:
mysql 数据解析关注的表,Perl正则表达式. 多个正则之间以逗号(,)分隔,转义符需要双斜杠(\\) 常见例子: 1. 所有表:.* or .*\\..* 2. canal schema下所有表: canal\\..* 3. canal下的以canal打头的表:canal\\.canal.* 4. canal schema下的一张表:canal.test1 5. 多个规则组合使用:canal\\..*,mysql.test1,mysql.test2 (逗号分隔) 注意:此过滤条件只针对row模式的数据有效(ps. mixed/statement因为不解析sql,所以无法准确提取tableName进行过滤)
(2)、配置canal,打开canal.properties文件
将它的id属性修改成和mysql数据库中server-id不同的值,如下图:
canal.id = 3 # tcp bind ip canal.ip = 127.0.0.1 # register ip to zookeeper canal.register.ip = canal.port = 11111 canal.metrics.pull.port = 11112
如果没有特殊需求的单机,可以不配置。这里可以配置端口号、集群(集群依赖zk,需要配置zk地址),如图。
canal.id = 3 # tcp bind ip canal.ip = 127.0.0.1 # register ip to zookeeper canal.register.ip = canal.port = 11111 canal.metrics.pull.port = 11112
canal.id不要与mysql的server_id相同。
(3)、进入bin目录下,双击startup.bat即可启动
四、canal-1.1.5实时同步MySQL数据到Elasticsearch
(1)、创建数据库canal_manager和相关的表
https://github.com/alibaba/canal/blob/master/admin/admin-web/src/main/resources/canal_manager.sql
CREATE DATABASE /*!32312 IF NOT EXISTS*/ `canal_manager` /*!40100 DEFAULT CHARACTER SET utf8 COLLATE utf8_bin */; USE `canal_manager`; SET NAMES utf8; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for canal_adapter_config -- ---------------------------- DROP TABLE IF EXISTS `canal_adapter_config`; CREATE TABLE `canal_adapter_config` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `category` varchar(45) NOT NULL, `name` varchar(45) NOT NULL, `status` varchar(45) DEFAULT NULL, `content` text NOT NULL, `modified_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Table structure for canal_cluster -- ---------------------------- DROP TABLE IF EXISTS `canal_cluster`; CREATE TABLE `canal_cluster` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(63) NOT NULL, `zk_hosts` varchar(255) NOT NULL, `modified_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Table structure for canal_config -- ---------------------------- DROP TABLE IF EXISTS `canal_config`; CREATE TABLE `canal_config` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `cluster_id` bigint(20) DEFAULT NULL, `server_id` bigint(20) DEFAULT NULL, `name` varchar(45) NOT NULL, `status` varchar(45) DEFAULT NULL, `content` text NOT NULL, `content_md5` varchar(128) NOT NULL, `modified_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `sid_UNIQUE` (`server_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Table structure for canal_instance_config -- ---------------------------- DROP TABLE IF EXISTS `canal_instance_config`; CREATE TABLE `canal_instance_config` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `cluster_id` bigint(20) DEFAULT NULL, `server_id` bigint(20) DEFAULT NULL, `name` varchar(45) NOT NULL, `status` varchar(45) DEFAULT NULL, `content` text NOT NULL, `content_md5` varchar(128) DEFAULT NULL, `modified_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `name_UNIQUE` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Table structure for canal_node_server -- ---------------------------- DROP TABLE IF EXISTS `canal_node_server`; CREATE TABLE `canal_node_server` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `cluster_id` bigint(20) DEFAULT NULL, `name` varchar(63) NOT NULL, `ip` varchar(63) NOT NULL, `admin_port` int(11) DEFAULT NULL, `tcp_port` int(11) DEFAULT NULL, `metric_port` int(11) DEFAULT NULL, `status` varchar(45) DEFAULT NULL, `modified_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Table structure for canal_user -- ---------------------------- DROP TABLE IF EXISTS `canal_user`; CREATE TABLE `canal_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(31) NOT NULL, `password` varchar(128) NOT NULL, `name` varchar(31) NOT NULL, `roles` varchar(31) NOT NULL, `introduction` varchar(255) DEFAULT NULL, `avatar` varchar(255) DEFAULT NULL, `creation_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; SET FOREIGN_KEY_CHECKS = 1; -- ---------------------------- -- Records of canal_user -- ---------------------------- BEGIN; INSERT INTO `canal_user` VALUES (1, 'admin', '6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9', 'Canal Manager', 'admin', NULL, NULL, '2019-07-14 00:05:28'); COMMIT; SET FOREIGN_KEY_CHECKS = 1;
创建表es_test
CREATE TABLE `es_test` ( `id` int(0) NOT NULL AUTO_INCREMENT, `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, `content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
(2)、创建索引和映射
{ "properties": { "count": { "type": "text" }, "id": { "type": "integer" }, "name": { "type": "text", "store": true, "analyzer": "ik_smart" }, "color": { "type": "text" } } }
五、安装并启动Canal-adapter
要和上面的canal版本一致。下载后修改配置文件application.yml
(1)、解压并修改配置文件 conf/application.yml
server: port: 8081 spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 default-property-inclusion: non_null canal.conf: mode: tcp #tcp kafka rocketMQ rabbitMQ flatMessage: true zookeeperHosts: syncBatchSize: 1000 retries: -1 timeout: accessKey: secretKey: consumerProperties: # canal tcp consumer canal.tcp.server.host: 127.0.0.1:11111 canal.tcp.zookeeper.hosts: canal.tcp.batch.size: 500 canal.tcp.username: canal.tcp.password: # kafka consumer kafka.bootstrap.servers: 127.0.0.1:9092 kafka.enable.auto.commit: false kafka.auto.commit.interval.ms: 1000 kafka.auto.offset.reset: latest kafka.request.timeout.ms: 40000 kafka.session.timeout.ms: 30000 kafka.isolation.level: read_committed kafka.max.poll.records: 1000 # rocketMQ consumer rocketmq.namespace: rocketmq.namesrv.addr: 127.0.0.1:9876 rocketmq.batch.size: 1000 rocketmq.enable.message.trace: false rocketmq.customized.trace.topic: rocketmq.access.channel: rocketmq.subscribe.filter: # rabbitMQ consumer rabbitmq.host: rabbitmq.virtual.host: rabbitmq.username: rabbitmq.password: rabbitmq.resource.ownerId: srcDataSources: defaultDS: url: jdbc:mysql://127.0.0.1:3306/canal_manager?useUnicode=true&characterEncoding=UTF-8 username: canal password: canal canalAdapters: - instance: example # canal instance Name or mq topic name groups: - groupId: g1 outerAdapters: - name: logger # - name: rdb # key: mysql1 # properties: # jdbc.driverClassName: com.mysql.jdbc.Driver # jdbc.url: jdbc:mysql://127.0.0.1:3306/mytest2?useUnicode=true # jdbc.username: root # jdbc.password: 121212 # - name: rdb # key: oracle1 # properties: # jdbc.driverClassName: oracle.jdbc.OracleDriver # jdbc.url: jdbc:oracle:thin:@localhost:49161:XE # jdbc.username: mytest # jdbc.password: m121212 # - name: rdb # key: postgres1 # properties: # jdbc.driverClassName: org.postgresql.Driver # jdbc.url: jdbc:postgresql://localhost:5432/postgres # jdbc.username: postgres # jdbc.password: 121212 # threads: 1 # commitSize: 3000 # - name: hbase # properties: # hbase.zookeeper.quorum: 127.0.0.1 # hbase.zookeeper.property.clientPort: 2181 # zookeeper.znode.parent: /hbase - name: es6 hosts: http://127.0.0.1:9200 # 127.0.0.1:9200 for rest mode properties: mode: rest # or rest # security.auth: test:123456 # only used for rest mode cluster.name: elasticsearch
注意:使用rest时,要加http://,端口为9200.name要选择es6,集群的名称要与es中的一致。
(2)、修改配置文件bootstrap.yml
canal: manager: jdbc: url: jdbc:mysql://127.0.0.1:3306/canal_manager?useUnicode=true&characterEncoding=UTF-8 username: canal password: canal
(3)、用于配置MySQL中的表与Elasticsearch中索引的映射关系;
其他三个没用的yml文件可以删除
dataSourceKey: defaultDS destination: example groupId: g1 esMapping: _index: es_test _type: _doc _id: _id upsert: true # relations: # customer_order: # name: order # parent: customer_id sql: "select a.id as _id, a.id, a.count, a.name, a.color from es_test a" # skips: # - customer_id etlCondition: "where a.id>={}" commitBatch: 3000
(4)、启动服务
注意:如果下载的是canal.adapter-1.1.5-SNAPSHOT.tar.gz,则不会出现以下问题。
报错:class com.alibaba.druid.pool.DruidDataSource cannot be cast to class com.alibaba.druid.pool.DruidDataSource
原因是 druid 包冲突导致的,解决办法如下
下载源码包
wget https://github.com/alibaba/canal/archive/refs/tags/canal-1.1.5.tar.gz
解压后,使用IDEA打开,定位到 client-adapter.escore 模块的 pom.xml 的 druid 更新为
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<scope>provided</scope>
</dependency>
更新后,在项目根目录下执行
mvn clean package
然后到 canal-canal-1.1.5/client-adapter/es6x/target 下 将打包好的 client-adapter.es6x-1.1.5-jar-with-dependencies.jar 替换掉 canal-adapter/plugin 下原来的.
重启 Canal Adapter,发现日志不再报错,问题成功解决。
(5)、实时同步
向数据库中插入一条数据
insert into es_test(id,name,color,count) values(1,'钢铁是怎样炼成的','黄色','111');
canal 的 数据同步不是自动的,需要触发!!
触发:http://127.0.0.1:8081/etl/es6/canal_manager.yml?params=1
etl 固定的,es6 之前起的名字必须对应,后续配置文件也在这里面,canal_manager.yml 配置文件名称, "params=1" 同步数据的条件 1 入参
查看head插件
修改数据
update es_test set name = '简爱' where id = 1;
再次触发,查看head插件
删除数据
delete from es_test where id =1;
触发:
查看head插件
发现删除时,无法同步。
如果一个文档从 MySQL 中删除,该操作并不会同步到 ElasticSearch 中。
列举几个我们可以考虑的方案。
方案一:MySQL 中的记录可通过包含 is_deleted 字段以表明该条记录是否有效。一旦发生更新,is_deleted 也会同步更新到 ElasticSearch 中。如果通过这种方式,在执行 MySQL 或 ElasticSearch 查询时,重写查询语句来过滤掉 is_deleted 为 true 的记录。同时,可以通过一些后台进程将 MySQL 和 ElasticSearch 中的这些文档删除。
方案二:应用系统负责 MySQL 和 ElasticSearch 中数据的删除,即应用系统在删除 MySQL 中数据,同时负责也删除 ElasticSearch 中相应的文档。这个就会到程序层面的开发工作。
可以通过 mysql 的 binlog 实现数据库中的得事件(增加、更新、删除)同步,任何事件实时同步到 Elasticsearch。这个相对较复杂了。但好在已经有了开源组件可以用,即阿里的 canal。不过,我个人认为,项目不大,且实时性要求不高,还是用 logstash 比较简单省心。canal 在单表时,还比较简单,如果涉及多表,也不方便,根据需求会有不定量的的开发工作。
最后,要依据场景选择合适的方案。把它们进行适时地结合:实时性要求高,用 canal 实现;一般场景,logstash 即可;即使是有题主提的删除,也不建议使用 canal,而是用前面提到的几个思路。有能力的话,其实可以针对这两种方案设计一个数据同步框架,以后只要加个配置即可完成。