1、为什么要用canal?
作用:同步mysql | 使用canal的理由 |
---|---|
做拉链表 | 某些情况无法从日志中获取信息,而又无法利用sqoop等EL工具对数据实时的监控 |
更新redis |
2、canal工作原理
如图所示:
·
canal的工作原理很简单,就是把自己伪装成slave,假装从master复制数据。
3、了解mysql的binlog
binlog开启 | binlog的分类 |
---|---|
在mysql的配置文件(Linux: /etc/my.cnf , Windows: \my.ini)下,修改配置 | mysql binlog的格式,那就是有三种,分别是STATEMENT,MIXED,ROW。 |
在[mysqld] 区块 | 在配置文件中可以选择配置 |
设置/添加 :log-bin=mysql-bin | binlog_format=row |
这个表示binlog日志的前缀是mysql-bin ,以后生成的日志文件就是 mysql-bin.123456 的文件后面的数字按顺序生成。 每次mysql重启或者到达单个文件大小的阈值时,新生一个文件,按顺序编号。 | 区别:statement 语句级,binlog会记录每次一执行写操作的语句。相对row模式节省空间,但是可能产生不一致性,比如update tt set create_date=now() 如果用binlog日志进行恢复,由于执行时间不同可能产生的数据就不同。 |
binlog的优点 | binlog缺点 |
---|---|
节省空间 | 有可能造成数据不一致。 |
保持数据的绝对一致性。因为不管sql是什么,引用了什么函数,他只记录执行后的效果。 | 占用较大空间。 |
行级, binlog会记录每次操作后每行记录的变化。 |
实际操作
(1)编辑/etc/my.cnf
#添加配置参数
#binlog数据中包含server_id,标识该数据是由那个server同步过来的
server-id = 1
##mysql向文件名前缀添加数字后缀来按顺序创建二进制日志文件如mysql-binlog.000006
log-bin = mysql-bin
##选择基于行的日志记录方式
binlog_format = ROW
(2)在mysql数据库执行sql脚本(gmall1205.sql)
注意:
自己使用的是sqlyog工具执行,navicat工具执行有点问题
(3) 虚拟机重新启动mysql服务,执行命令
[root@flink102 ~]# systemctl restart mysqld.service
[root@flink102 ~]#
//进入mysql目录
[root@flink102 etc]# cd /var/lib/mysql
//查看
[root@flink102 mysql]# ls -l
如图所示:
sqlyog执行语句
CALL init_data ('2019-06-04',10,2,TRUE)
注意,我们还需要给mysql赋权限
//赋canal权限
GRANT ALL PRIVILEGES ON *.* TO canal@'%' IDENTIFIED BY 'canal'
4、canal安装
(1)上传canal安装包到虚拟机上,并解压文件
//解压文件
[root@flink102 canal]# tar -zxvf canal.deployer-1.1.2.tar.gz -C module/
(2) 配置canal文件
[root@flink102 conf]# vim example/instance.properties
cannal.instance.mysql.slaveId=20
canal.instance.master.address=Flink102:3306
(3) 启动canal
[root@flink102 bin]# ll
total 16
-rwxr-xr-x 1 root root 39 Nov 26 2018 restart.sh
-rwxr-xr-x 1 root root 1145 Nov 26 2018 startup.bat
-rwxr-xr-x 1 root root 2956 Nov 26 2018 startup.sh
-rwxr-xr-x 1 root root 1356 Nov 26 2018 stop.sh
//启动canal
[root@flink102 bin]# ./startup.sh
//查看进程
[root@flink102 bin]# jps
6672 CanalLauncher //这个canal进来成功启动了
2131 Kafka
6697 Jps
[root@flink102 bin]#
5、canal客户端代码
(1)先创建一个子模块gmall0315-canal
子模块canal创建ok
(2)pom文件依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>dw-char</artifactId>
<groupId>com.study.gmall0315</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>gmall0315-canal</artifactId>
<dependencies>
<dependency>
<groupId>com.study.gmall0315</groupId>
<artifactId>gmall0315-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba.otter/canal.client -->
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka-clients -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>0.11.0.0</version>
</dependency>
</dependencies>
</project>
(3) 编写代码
CanalApp.java
package com.study.gmall0315.canal.app;
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.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.study.gmall0315.canal.handler.CanlHandler;
import java.net.InetSocketAddress;
import java.util.List;
public class CanalApp {
public static void main(String[] args) {
//初始化创建canal的连接器
CanalConnector canalConnector = CanalConnectors.newSingleConnector(new InetSocketAddress("Flink102", 11111), "example", " ", " ");
//不停往canal抓数据
while (true){
//连接、订阅、抓取数据
canalConnector.connect();
canalConnector.subscribe("gmall0315.order_info");
//抓数据不一定都有100条
Message message = canalConnector.get(100);
int size = message.getEntries().size();
//判断,如果抓数据为0,表示没抓到
if(size==0){
try {
System.out.println("没有数据,休息一会");
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
//遍历
for (CanalEntry.Entry entry : message.getEntries()) {
//判断事件类型,只处理行变化业务
if (entry.getEntryType().equals(CanalEntry.EntryType.ROWDATA)){
//把数据集进行反序列化
ByteString storeValue = entry.getStoreValue();
CanalEntry.RowChange rowChange=null;
try {
//ByteString类型无法直接使用,需要做类型转换
rowChange = CanalEntry.RowChange.parseFrom(storeValue);
//InvalidProtocolBufferException序列化格式,目前是市面上最快的
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
//获得行集
List<CanalEntry.RowData> rowDatasList = rowChange.getRowDatasList();
//操作类型
CanalEntry.EventType eventType = rowChange.getEventType();
//操作表名
String tableName = entry.getHeader().getTableName();
//传参数:名字、类型、数据
CanlHandler.handle(tableName,eventType,rowDatasList);
}
}
}
}
}
}
CanlHandler.java
package com.study.gmall0315.canal.handler;
import com.alibaba.otter.canal.protocol.CanalEntry;
import java.util.List;
public class CanlHandler {
//执行表名的方法
public static void handle(String tableName, CanalEntry.EventType eventType, List<CanalEntry.RowData> rowDatasList ){
//判断
if ("order_info".equals(tableName)&&CanalEntry.EventType.INSERT.equals(eventType)){
//下单操作遍历
for (CanalEntry.RowData rowData : rowDatasList) {
//行集展开
List<CanalEntry.Column> afterColumnsList = rowData.getAfterColumnsList();
//再次循环打印
///列集展开
for (CanalEntry.Column column : afterColumnsList) {
//打印
System.out.println(column.getName() + ":::" + column.getValue());
}
}
}
}
}
(4) 运行程序,控制台打印信息:
之后,在mysql客户端工具执行语句
CALL `init_data`('2019-06-04',10,2,FALSE)
再次,进入文件目录查看/var/lib/mysql
[root@flink102 ~]# cd /var/lib/mysql
[root@flink102 mysql]# ls -l
数据进来了
6、汇总canal的关系
CanalClient |
---|
message:一次cabal从日志中抓取的信息,一个message包括多个sql(even) |
entry:相当于一个sql命令,一个sql可能会对 多行记录造成影响,分为三当面:1、type–>用于区分是数据变化,还是事务变化;2、header.tableName表名;3、storevalue得到rowchage |
rowchage:entry经过反序列化得到对象,包括了多行记录的变化值,包括两方面:1、eventype–>数据的变化类型:insert、update、delete、create、alter、drop ; 2、rowdatalist |
RowDatas:一个rowchage里包括的数据变化集,其中每一个rowdata里面包含一行的多字段:afterColumnList、beforeColumnList |
Column:一个RowData里包含了多个column,每个column包含name和value,即:columnName、columnValue |
7、发送kafka
(1) 代码实现
CanlHandler类的kafka代码
//kafka操作
JSONObject jsonObject = new JSONObject();
//转成小写工具
String propertyName = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, column.getName());
jsonObject.put(propertyName,column.getValue());
}
MyKafkaSender.send(GmallConstant.KAFKA_TOPIC_ORDER,jsonObject.toJSONString());
MyKafkaSender.java
package com.study.gmall0315.canal.util;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import java.util.Properties;
public class MyKafkaSender {
public static KafkaProducer<String, String> kafkaProducer=null;
public static KafkaProducer<String, String> createKafkaProducer(){
Properties properties = new Properties();
properties.put("bootstrap.servers", "Flink102:9092");
properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
KafkaProducer<String, String> producer = null;
try {
producer = new KafkaProducer<String, String>(properties);
}catch (Exception e){
e.printStackTrace();
}
return producer;
}
public static void send(String topic,String msg){
if(kafkaProducer==null){
kafkaProducer=createKafkaProducer();
}
kafkaProducer.send(new ProducerRecord<String, String>(topic,msg));
}
}
(2)启动kafka和kafka的消费者
//启动kafka
[root@flink102 kafka-2.11]# bin/kafka-server-start.sh config/server.properties &
//启动kafka的消费者
[root@flink102 kafka-2.11]# bin/kafka-console-consumer.sh --bootstrap-server Flink102:9092 --topic GMALL_ORDER
(3)在mysql的客户端执行sql语句
CALL `init_data`('2019-06-04',10,2,FALSE);
kafka的消费者也将会打印相关的消费数据