Canal
官网: https://github.com/alibaba/canal/
- canal是 阿里巴巴 的一个使用Java开发的开源项目
- 它是专门用来进行 数据库同步 的
- 目前支持 mysql 、以及(mariaDB)
1、Canal原理
-
Canal模拟mysql slave的交互协议,伪装自己为mysql slave
-
向mysql master发送dump协议
-
mysql master收到dump协议,发送binary log给slave(canal) 4. canal解析binary log字节流对象
2、Canal安装部署
2.1 MySQL开启binlog
binlog日志介绍:
-
用来记录mysql中的 增加 、 删除 、 修改 操作
-
select操作 不会 保存到binlog中
-
必须要 打开 mysql中的binlog功能,才会生成binlog日志
用来记录mysql中的 增加 、 删除 、 修改 操作select操作 不会 保存到binlog中
必须要 打开 mysql中的binlog功能,才会生成binlog日志
- binlog日志就是一系列的二进制文件
修改MySQL的配置文件: /etc/my.cnf
# 添加以下配置
log-bin=/var/lib/mysql/mysql-bin
binlog-format=ROW
server_id=1
# 配置说明
# 1、配置binlog日志的存放路径为/var/lib/mysql目录,文件以mysql-bin开头
# 2、配置mysql中每一行记录的变化都会详细记录下来
# 3、配置当前机器器的服务ID(如果是mysql集群,不能重复)
重启mysql
systemctl restart mysqld
# mysql -u root -p123456
登录mysql执行以下SQL命令查看配置是否生效
show variables like '%log_bin%';
mysql输出以下内容,表示binlog已经成功开启
+---------------------------------+--------------------------------+
| Variable_name | Value |
+---------------------------------+--------------------------------+
| log_bin | ON |
| log_bin_basename | /var/lib/mysql/mysql-bin |
| log_bin_index | /var/lib/mysql/mysql-bin.index |
| log_bin_trust_function_creators | OFF |
| log_bin_use_v1_row_events | OFF |
| sql_log_bin | ON |
+---------------------------------+--------------------------------+
6 rows in set (0.00 sec)
查看生产的binlog日志,为二进制格式
[root@node01 ~]# cd /var/lib/mysql
[root@node01 mysql]# ll -ah | grep mysql-bin.000001
-rw-r----- 1 mysql mysql 154 Jul 17 10:36 mysql-bin.000001
2.2 安装Canal
下载Canal
https://github.com/alibaba/canal/releases
选择canal.deployer-1.1.4.tar.gz这个版本
上传并解压Canal
# 创建canal的安装目录
mkdir -p /baicdt/servers/canal.deployer-1.1.4
# 上传canal安装包至/baicdt/softwares/
tar -zxvf /baicdt/softwares/canal.deployer-1.1.4.tar.gz -C /baicdt/servers/canal.deployer-1.1.4
修改 conf/example 目录中的 instance.properties 配置文件
# vim /baicdt/servers/canal.deployer-1.1.4/conf/example/instance.properties
## mysql serverId 这个ID是canal伪装的mysql从节点的id,所有不能与mysql实际配置的 service_id 重复
canal.instance.mysql.slaveId=1001
# position info
canal.instance.master.address=node01.hadoop.com:3306
canal.instance.dbUsername=root
canal.instance.dbPassword=123456
启动Canal
cd /baicdt/servers/canal.deployer-1.1.4
bin/startup.sh
注意:
Canal的远程连接端口号默认为 11111 ,当然如果需要,可以在 canal.properties 文件中修改
3、Canal Java API
功能描述:
使用java api创建一个canal客户端获取服务器binlog日志信息,解析结果并写入到Kafka中。
3.1 pom依赖
<repositories>
<repository>
<id>aliyun</id>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</repository>
<repository>
<id>cloudera</id>
<url>https://repository.cloudera.com/artifactory/cloudera-repos/</url>
</repository>
<repository>
<id>jboss</id>
<url>http://repository.jboss.com/nexus/content/groups/public</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<!--<version>1.0.24</version>-->
<version>1.1.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka_2.11</artifactId>
<version>0.9.0.1</version>
</dependency>
<!--对象和json 互相转换的-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.44</version>
</dependency>
</dependencies>
3.2 配置文件
application.properties文件
# canal配置
canal.host=node01.hadoop.com
canal.port=11111
canal.instance=example
mysql.username=root
mysql.password=123456
#kafka的配置
kafka.bootstrap.servers=node01.hadoop.com:9092
kafka.zookeeper.connect=node01.hadoop.com:2181
kafka.input.topic=canal
log4j.properties文件
log4j.rootLogger=error,stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p - %m%n
3.3 配置文件公共类
import java.util.ResourceBundle;
public class GlobalConfigUtil {
//读取application.properties文件
private static ResourceBundle resourceBundle = ResourceBundle.getBundle("application");
public static String canalHost= resourceBundle.getString("canal.host");
public static String canalPort = resourceBundle.getString("canal.port");
public static String canalInstance = resourceBundle.getString("canal.instance");
public static String mysqlUsername = resourceBundle.getString("mysql.username");
public static String mysqlPassword= resourceBundle.getString("mysql.password");
public static String kafkaBootstrap= resourceBundle.getString("kafka.bootstrap.servers");
public static String kafkaZookeeper= resourceBundle.getString("kafka.zookeeper.connect");
public static String kafkaInput = resourceBundle.getString("kafka.input.topic");
}
3.4 Kafka生产消息
import kafka.javaapi.producer.Producer;
import kafka.producer.KeyedMessage;
import kafka.producer.ProducerConfig;
import kafka.serializer.StringEncoder;
import java.util.Properties;
//Kafka生产消息工具类
public class KafkaSender {
private String topic;
public KafkaSender(String topic) {
super();
this.topic = topic;
}
//发送消息到Kafka指定topic
public static void sendMessage(String topic, String key, String data) {
Producer<String, String> producer = createProducer();
producer.send(new KeyedMessage<String, String>(topic, key, data));
}
// 创建生产者实
private static Producer<String, String> createProducer() {
Properties properties = new Properties();
properties.put("metadata.broker.list", GlobalConfigUtil.kafkaBootstrap);
properties.put("zookeeper.connect", GlobalConfigUtil.kafkaZookeeper);
properties.put("serializer.class", StringEncoder.class.getName());
return new Producer<String, String>(new ProducerConfig(properties));
}
}
3.5 canal客户端
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
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 com.baicdt.core.util.GlobalConfigUtil;
import com.baicdt.core.util.KafkaSender;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* Canal解析binlog日志工具类
*/
public class CanalClient {
/**
* 内部类: 解析列的信息
*/
static class ColumnValuePair {
private String columnName;
private String columnValue;
private Boolean isValid;
public ColumnValuePair(String columnName, String columnValue, Boolean isValid) {
this.columnName = columnName;
this.columnValue = columnValue;
this.isValid = isValid;
}
public String getColumnName() { return columnName; }
public void setColumnName(String columnName) { this.columnName = columnName; }
public String getColumnValue() { return columnValue; }
public void setColumnValue(String columnValue) { this.columnValue = columnValue; }
public Boolean getIsValid() { return isValid; }
public void setIsValid(Boolean isValid) { this.isValid = isValid; }
}
/**
* 获取Canal连接
*
* @param host 主机名
* @param port 端口号
* @param instance Canal实例名
* @param username 用户名
* @param password 密码
* @return Canal连接器
*/
public static CanalConnector getConn(String host, int port, String instance, String username, String password) {
CanalConnector canalConnector = CanalConnectors.newSingleConnector(new InetSocketAddress(host, port), instance, username, password);
return canalConnector;
}
/**
* 解析Binlog日志
*
* @param entries Binlog消息实体
* @param emptyCount 操作的序号
*/
public static void analysis(List<CanalEntry.Entry> entries, int emptyCount) {
for (CanalEntry.Entry entry : entries) {
// 只解析mysql事务的操作,其他的不解析
if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN ||
entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) {
continue;
}
// 那么解析binlog
CanalEntry.RowChange rowChange = null;
try {
rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
} catch (Exception e) {
e.printStackTrace();
}
// 获取操作类型字段(增加 删除 修改)
CanalEntry.EventType eventType = rowChange.getEventType();
// 获取binlog文件名称(当前数据来自那个文件)
String logfileName = entry.getHeader().getLogfileName();
// 读取当前操作在binlog文件的位置,偏移量
long logfileOffset = entry.getHeader().getLogfileOffset();
// 获取当前操作所属的数据库
String dbName = entry.getHeader().getSchemaName();
// 获取当前操作所属的表
String tableName = entry.getHeader().getTableName();//当前操作的是哪一张表
long timestamp = entry.getHeader().getExecuteTime();//执行时间
// 解析操作的行数据
for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
// 删除操作
if (eventType == CanalEntry.EventType.DELETE) {
// 获取删除之前的所有列数据
dataDetails(rowData.getBeforeColumnsList(), logfileName, logfileOffset, dbName, tableName, eventType, emptyCount,timestamp);
}
// 新增操作
else if (eventType == CanalEntry.EventType.INSERT) {
// 获取新增之后的所有列数据
dataDetails(rowData.getAfterColumnsList(), logfileName, logfileOffset, dbName, tableName, eventType, emptyCount,timestamp);
}
// 更新操作
else {
// 获取更新之后的所有列数据
dataDetails(rowData.getAfterColumnsList(), logfileName, logfileOffset, dbName, tableName, eventType, emptyCount,timestamp);
}
}
}
}
/**
* 解析具体一条Binlog消息的数据
*
* @param columns 当前行所有的列数据
* @param logFileName binlog文件名
* @param logFileOffset 当前操作在binlog中的位置
* @param dbName 当前操作所属数据库名称
* @param tableName 当前操作所属表名称
* @param eventType 当前操作类型(新增、修改、删除)
* @param emptyCount 操作的序号
*/
private static void dataDetails(List<CanalEntry.Column> columns,
String logFileName,
Long logFileOffset,
String dbName,
String tableName,
CanalEntry.EventType eventType,
int emptyCount,
long timestamp) {
// 找到当前那些列发生了改变 以及改变的值
List<ColumnValuePair> columnValueList = new ArrayList<ColumnValuePair>();
for (CanalEntry.Column column : columns) {
ColumnValuePair columnValuePair = new ColumnValuePair(column.getName(), column.getValue(), column.getUpdated());
columnValueList.add(columnValuePair);
}
String key = UUID.randomUUID().toString();
JSONObject jsonObject = new JSONObject();
jsonObject.put("logFileName", logFileName);
jsonObject.put("logFileOffset", logFileOffset);
jsonObject.put("dbName", dbName);
jsonObject.put("tableName", tableName);
jsonObject.put("eventType", eventType);
jsonObject.put("columnValueList", columnValueList);
jsonObject.put("emptyCount", emptyCount);
jsonObject.put("timestamp", timestamp);
// 拼接所有binlog解析的字段
String data = JSON.toJSONString(jsonObject);
System.out.println(data);
// 解析后的数据发送到kafka
KafkaSender.sendMessage(GlobalConfigUtil.kafkaInput, key, data);
}
/**
* 客户端入口方法
* @param args
*/
public static void main(String[] args) {
// 加载配置文件
String host = GlobalConfigUtil.canalHost;
int port = Integer.parseInt(GlobalConfigUtil.canalPort);
String instance = GlobalConfigUtil.canalInstance;
String username = GlobalConfigUtil.mysqlUsername;
String password = GlobalConfigUtil.mysqlPassword;
// 获取Canal连接
CanalConnector conn = getConn(host, port, instance, username, password);
// 从binlog中读取数据
int batchSize = 100;//每次请求的数据条数
int emptyCount = 1;
try {
conn.connect();
// canal提供数据库增量订阅和消费业务
conn.subscribe(".*\\..*"); //subscribe方法:通过正则表达式,指定订阅那个数据库
//维护偏移量
conn.rollback();
int totalCount = 120; //循环次数
while (totalCount > emptyCount) {
// 获取数据(100条)
Message message = conn.getWithoutAck(batchSize);
long id = message.getId();
int size = message.getEntries().size();
if (id == -1 || size == 0) {
//没有读取到任何数据
} else {
//有数据,那么解析binlog日志
analysis(message.getEntries(), emptyCount);
emptyCount++;
}
// 确认消息
conn.ack(message.getId());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
conn.disconnect();
}
}
}
3.6 SQL脚本
CREATE DATABASE IF NOT EXISTS pyg;
create table `pyg`.`commodity`(
commodityId int(10), -- 商品ID
commodityName varchar(20), -- 商品名称
commodityTypeId int(20), -- 商品类别ID
originalPrice double(16, 2), -- 原价
activityPrice double(16, 2) -- 活动价
);
insert into commodity values (1, '耐克', 1, 888.00, 820.00);
update commodity set originalPrice = 999.88 where commodityId = 1;
delete from commodity where commodityId = 1;
3.7 kafka脚本
# 启动kafka
nohup /baicdt/servers/kafka_2.11-1.0.0/bin/kafka-server-start.sh \
/baicdt/servers/kafka_2.11-1.0.0/config/server.properties 2>&1 &
# 查看Kafka当中已存在的主题
/baicdt/servers/kafka_2.11-1.0.0/bin/kafka-topics.sh \
--list \
--zookeeper node01.hadoop.com:2181
# 创建一个kafka主题 canal
# 单节点kafka服务器,副本数设置为1,分区可设置多个 3
/baicdt/servers/kafka_2.11-1.0.0/bin/kafka-topics.sh \
--create \
--zookeeper node01.hadoop.com:2181 \
--replication-factor 1 \
--partitions 3 \
--topic canal
# 创建一个kafka消费者,用来查看数据
/baicdt/servers/kafka_2.11-1.0.0/bin/kafka-console-consumer.sh \
--from-beginning \
--bootstrap-server node01.hadoop.com:9092 \
--topic canal
# 向topic生产数据
/baicdt/servers/kafka_2.11-1.0.0/bin/kafka-console-producer.sh \
--broker-list node01.hadoop.com:9092 \
--topic canal
# 删除topic
/baicdt/servers/kafka_2.11-1.0.0/bin/kafka-topics.sh \
--zookeeper node01.hadoop.com:2181 \
--delete \
--topic canal
3.8 binlog日志格式分析
客户端获取到的binlog日志json数据如下:
格式分析:字段以及说明
字段名称 | 说明 | 示例 |
---|---|---|
emptyCount | 操作序号(第几条记录) | 12 |
logFileName | binlog文件名 | mysql-bin.000001 |
dbName | 数据库名称 | pyg |
logFileOwset | binlog文件偏移位置 | 100 |
eventType | 操作类型 | INSERT或UPDATE或DELETE |
columnValueList | 列值列表 | { “columnName”: “列名”,“columnValue”: “列值”,“isValid”: “是否有效”} |
tableName | 表名 | commodity |
timestamp | 执行时间戳 | 1553701139000 |