canal:阿里巴巴 MySQL binlog 增量订阅&消费组件
Home · alibaba/canal Wiki · GitHub
canal 是阿里巴巴 MySQL 数据库 Binlog 的增量订阅 & 消费组件。
名称:canal [kə'næl]
译意: 水道 / 管道 / 沟渠
语言: 纯 java 开发
定位: 基于数据库增量日志解析,提供增量数据订阅 & 消费,目前主要支持了 MySQL、Kafka、es、Hbase、mq等
早期,阿里巴巴 B2B 公司因为存在杭州和美国双机房部署,存在跨机房同步的业务需求。不过早期的数据库同步业务,主要是基于 trigger 的方式获取增量 变更,不过从 2010 年开始,阿里系公司开始逐步的尝试基于数据库的日志解析,获取增量变更进行同步,由此衍生出了增量订阅 & 消费的业务,从此开 启了一段新纪元。ps. 目前内部使用的同步,已经支持 MySQL 5.x 和 Oracle 部分版本的日志解析
基于日志增量订阅 & 消费支持的业务:
数据库镜像
数据库实时备份
多级索引 (卖家和买家各自分库索引)
search build
业务 cache 刷新
价格变化等重要业务消息
Canal 工作原理:
复制遵循三步过程:
主服务器将更改记录到binlog中(这些记录称为binlog事件,可以通过来查看show binary events)
从服务器将主服务器的二进制日志事件复制到其中继日志。
中继日志中的从服务器重做事件随后将更新其旧数据。
如何运作
原理很简单:
canal 模拟 mysql slave 的交互协议,伪装自己为 mysql slave,向 mysql master 发送 dump 协议
mysql master 收到 dump 请求,开始推送 binary log 给 slave (也就是 canal)
canal 解析 binary log 对象 (原始为 byte 流)
只需要在工具下运行 SHOW VARIABLES LIKE 'log_%';
以下是我以Centos7服务器安装MySql 5.7为例,自行去官网下载
在my.conf文件中的 [mysqld] 下添加以下三行内容
或者复制我这个my.conf的全部内容只需要修改对应的目录就行
[client]
port = 3306
socket = /var/lib/mysql/mysql.sock
default-character-set = utf8mb4
[mysqld]
port = 3306
socket = /var/lib/mysql/mysql.sock
basedir = /usr/local/mysql
datadir = /data/mysql
pid-file = /var/run/mysqld/mysqld.pid
user = mysql
server-id = 1
log-bin = mysql-bin
log_slave_updates = on
auto_increment_increment=1
auto_increment_offset=1
lower_case_table_names=1
init-connect = 'SET NAMES utf8mb4'
character-set-server = utf8mb4
performance_schema_max_table_instances = 200
table_definition_cache=200
table_open_cache=128
log_error = /var/log/mysqld.log
character_set_server=utf8mb4
collation_server=utf8mb4_unicode_ci
init_connect='SET NAMES utf8mb4'
bind-address = 0.0.0.0
skip-name-resolve
back_log = 600
max_connections = 1000
max_connect_errors = 6000
open_files_limit = 65535
max_allowed_packet = 512M
binlog_cache_size = 1M
log_bin = mysql-bin
binlog_format = row
expire_logs_days = 7
slow_query_log = 1
long_query_time = 1
slow_query_log_file = /var/log/mysql-slow.log
Releases · alibaba/canal · GitHub
wget https://github.com/alibaba/canal/releases/download/canal-1.1.6/canal.deployer-1.1.6.tar.gz
mv canal.deployer-1.1.6.tar.gz /data/canal/
cd /data/canal
tar -zxvf canal.deployer-1.1.6.tar.gz
vi conf/example/instance.properties
# position info
canal.instance.master.address=127.0.0.1:3306# username/password
canal.instance.dbUsername=改成数据库用户
canal.instance.dbPassword=改成数据库密码
cd /data/canal/bin
sh start.sh
org.springframework.boot spring-boot-starter-web com.alibaba.otter canal.client 1.1.3
package com.toms.canal.utils; import com.alibaba.otter.canal.client.CanalConnector; import com.alibaba.otter.canal.client.CanalConnectors; import com.alibaba.otter.canal.protocol.CanalEntry; import com.alibaba.otter.canal.protocol.Message; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; import java.net.InetSocketAddress; import java.util.Date; import java.util.List; /** * @author 重庆阿汤哥 * @Description: 基于canal实现mysql binLog监听 * @date 2022/6/7 18:23 */ @Slf4j @Component public class CanalCommandLineRunner implements CommandLineRunner { @Override public void run(String... args) throws Exception { //在canal部署的conf/canal.properties ip和端口信息 CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("canal 部署的ip", canal 的端口), "example", "", ""); try { //打开连接 connector.connect(); //订阅数据库表,全部表q // connector.subscribe(".*\\..*"); // 监听jes库中的字典表 connector.subscribe("jes.jes_dictionary"); //回滚到未进行ack的地方,下次fetch的时候,可以从最后一个没有ack的地方开始拿 connector.rollback(); while (true) { // 获取指定数量的数据 Message message = connector.getWithoutAck(1); long batchId = message.getId(); int size = message.getEntries().size(); if (batchId > 0 && size != 0) { handleDATAChange(message.getEntries()); } // 提交确认 connector.ack(batchId); } } catch (Exception e) { e.printStackTrace(); } finally { connector.disconnect(); //防止频繁访问数据库链接: 线程睡眠 10秒 try { Thread.sleep(10 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } } } private void handleDATAChange(Listentrys) { for (CanalEntry.Entry entry : entrys) { // 只解析mysql事务的操作,其他的不解析 if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN || entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) { continue; } //RowChange对象,包含了一行数据变化的所有特征 CanalEntry.RowChange rowChange; try { rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue()); } catch (Exception e) { throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(), e); } CanalEntry.EventType eventType = rowChange.getEventType(); // 获取当前操作所属的数据库 String dbName = entry.getHeader().getSchemaName(); // 获取当前操作所属的表 String tableName = entry.getHeader().getTableName(); // 事务提交时间 long timestamp = entry.getHeader().getExecuteTime(); log.info("Canal监测到更新:【{}】库的【{}】表", dbName, tableName); for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) { dataDetails(rowData.getBeforeColumnsList(), rowData.getAfterColumnsList(), dbName, tableName, eventType, timestamp); log.info("-------------------------------------------------------------"); } } } /** * 解析具体一条Binlog消息的数据 * * @param dbName 当前操作所属数据库名称 * @param tableName 当前操作所属表名称 * @param eventType 当前操作类型(新增、修改、删除) */ private static void dataDetails(List beforeColumns, List afterColumns, String dbName, String tableName, CanalEntry.EventType eventType, long timestamp) { log.info("数据库:{},表名:{},操作类型:{}", dbName, tableName, eventType); if (CanalEntry.EventType.INSERT.equals(eventType)) { log.info("新增数据:"); printColumn(afterColumns); } else if (CanalEntry.EventType.DELETE.equals(eventType)) { log.info("删除数据:"); printColumn(beforeColumns); } else { log.info("更新数据:更新前数据--"); printColumn(beforeColumns); log.info("更新数据:更新后数据--"); printColumn(afterColumns); } log.info("操作时间:{}", new Date(timestamp)); } private static void printColumn(List columns) { for (CanalEntry.Column column : columns) { log.info(column.getName() + " : " + column.getValue() + " update=" + column.getUpdated()); } } }