HBase里面配置的zk地址后面不能有端口号
node01:2181:2181
HBase的zk地址不需要自己手动添加端口号,有可能是配置文件引错了.
启动的时候提示Hive不存在,或者HCAT_HOME有问题.
先使用which hive
查看Hive的目录是不是/usr/bin/hive
,如果是这个目录,那么就直接删除,另外再看看有没有/usr/bin/hiveserver2
,有的话也顺带删除.
提示Hadoop目录不存在,或者HDFS上没有spark-libs.jar
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I48kaeE9-1591285741093)(assets/image-20200424091334833.png)]
学习Kylin
使用步骤:
Kylin的增量构建: 主要是为了解决全量构建速度慢/冗余数据的问题.
增量构建会造成碎片化问题:
Cube优化:
结合项目: 使用Kylin作为数仓的ADS层.
了解实时数仓的相关环境
学习Canal的使用.
学习ProtoBuf.
学习Canal原理.
MySQL数据实时采集: Canal.
序列化工具: Google ProtoBuf
消息队列(数仓分层): Kafka
使用Flink对数据进行实时ETL处理.
使用Redis保存维度数据.
关联查询Redis中的数据: 异步IO
使用HBase保存明细数据.便于后面进行一些查询.
使用Phoenix查询HBase中的数据.(即席查询)
实时OLAP分析引擎: Druid.
可视化BI分析工具: Superset.
使用FlinkCEP对订单超时数据进行监控.
整体业务分为3个部分:
公司内已经采用MR与spark之类的技术,做离线计算,为什么用实时计算?
目的:
要解决离线计算结果太慢,都是T+1的形式,我们需要掌握当前平台的指标数据/状态信息,只能等到第二天才可以获取.
问题:
既然实时数仓能及时的获取到各种指标数据,那么还要离线有什么用?
因为实时数仓存在不可控因素,比如网络/集群的稳定性,有可能会造成计算的误差.我们可以用离线数仓对实时数仓进行计算结果的校准.
比如: 昨天公司搞活动,当天实时部门给老板的数据是3000万的成交量,第二天,离线部门给老板的数据是3200万的成交量.
一般,实时数仓都会将计算的明细数据进行保存,就是为了作为备份,进行数据校准.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rDgqFZW9-1591285741097)(assets/image-20200424101443109.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EEMI8VyG-1591285741098)(assets/image-20200424103601268.png)]
使用Java技术栈进行实现,操作MySQL的数据进行计算,这种方式性能比较低,如果我们的数据量不大,可以使用,开发快/成本低.但是如果数据量很大,不推荐.
这种方式,适用于数据量很大的场景,同时还要求查询的效率比较高,比如1秒钟内返回数据.
但是这种方式也是有缺点的.
比如前面Flink阶段的电商指标分析系统,采用的就是Flink进行开发.
之前统计的PV/UV等指标数据,计算好之后存入HBase中,查询的时候通过RowKey获取数据.如果后期需求发生变更,那么我们就需要去修改代码,整体灵活性很差.这种方式也是18年以前会使用的方式,目前已经被淘汰了.
比如指标有按照小时/天月等时间段进行统计计算,但是如果实际使用中,时间分类满足不了需求怎么办?比如现在统计最近半个月的指标.
pv:2019090910这个是整点的数据,如果查询没半个小时的指标怎么办?要改代码吧.
我们可以将ETL相关的操作放在Flink中.而指标计算使用Druid来实现.后面我们需要什么指标,只需要通过编写SQL进行查询即可.而不需要修改Flink底层的代码逻辑.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q7UJj0Vi-1591285741100)(assets/01_实时数仓架构图.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IBwoHVLi-1591285741102)(assets/image-20200424142647905.png)]
canal [kə’næl],译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费
早期阿里巴巴因为杭州和美国双机房部署,存在跨机房同步的业务需求,实现方式主要是基于业务 trigger 获取增量变更。从 2010 年开始,业务逐步尝试数据库日志解析获取增量变更进行同步,由此衍生出了大量的数据库增量订阅和消费业务。
基于日志增量订阅和消费的业务包括
当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x
开启MySQL的binlog日志功能:vim /etc/my.conf
[mysqld]
log-bin=/var/lib/mysql/mysql-bin # 指定binlog存储的地址
binlog-format=ROW # 以行的形式进行记录(记录每一条发生变更的数据)
server_id=1 # Master的ID, 注意不要和slave的ID重复了.
重启MySQL: service mysqld restart
添加Canal的执行权限(可以不做):
CREATE USER root IDENTIFIED BY '123456';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' ;
FLUSH PRIVILEGES;
解压:
mkdir /export/servers/canal
tar -zxvf canal.deployer-1.0.24.tar.gz -C /export/servers/canal/
修改配置文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-POTeIex3-1591285741103)(assets/image-20200424144118242.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7jhkPJUJ-1591285741104)(assets/image-20200424144322824.png)]
#################################################
## mysql serverId
canal.instance.mysql.slaveId = 1234
# position info
canal.instance.master.address = node1:3306
canal.instance.master.journal.name =
canal.instance.master.position =
canal.instance.master.timestamp =
#canal.instance.standby.address =
#canal.instance.standby.journal.name =
#canal.instance.standby.position =
#canal.instance.standby.timestamp =
# username/password
canal.instance.dbUsername = root
canal.instance.dbPassword = 123456
canal.instance.defaultDatabaseName =
canal.instance.connectionCharset = UTF-8
# table regex
canal.instance.filter.regex = .*\\..*
# table black regex
canal.instance.filter.black.regex =
#################################################
启动Canal
bin/startup.sh
查询canal日志
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zNaLaLqi-1591285741105)(assets/image-20200424145412996.png)]
查询example实例的日志:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QqslwzkT-1591285741106)(assets/image-20200424145432521.png)]
如果Canal获取不到数据,可以尝试将索引重置.
先将canal删除,重新安装.
删除MySQL之前的binlog
rm -rf /var/lib/mysql/mysql-bin*
重启MySQL
package cn.itcast.canal;
import cn.itcast.canal.bean.CanalModel;
import com.alibaba.fastjson.JSON;
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 java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Canal客户端开发
*/
public class CanalClient {
public static void main(String[] args) throws Exception {
//2. 创建Canal连接对象
// CanalConnectors.newSingleConnector() // 单机版的Canal服务端
// CanalConnectors.newClusterConnector() // 高可用版本的Canal
CanalConnector connector = CanalConnectors.newSingleConnector(
new InetSocketAddress("node1", 11111),
"example", "root", "123456");
//3. 连接Canal服务端
connector.connect();
//4. 回滚到最后一次消费的位置
connector.rollback();
//5. 订阅我们要消费的数据.比如我们只消费itcast_shop数据库
connector.subscribe("itcast_shop.*");
// 获取数据不是只获取1次,而是应该源源不断的获取数据,只要数据发生了变更,我们就要进行数据的采集.
// 所以我们应该用一个死循环,源源不断的获取数据.
boolean flag = true;
while (flag) {
//6. 获取数据
// connector.get() 从服务器端获取数据,并发送回执
// connector.getWithoutAck() 从服务器获取数据,但是不发送回执.注意: 这种方式需要手动的发送回执.
Message message = connector.getWithoutAck(1000);
//7. 解析数据
long id = message.getId();//本批次数据的ID
List<CanalEntry.Entry> entries = message.getEntries();// 本批次的数据
//判断当前批次有没有获取到数据.
if (id == -1 || entries.size() == 0) {
//没有数据,不执行任何操作
} else {
//有数据,开始进行数据解析
// parseMessage(entries);
// 将数据转换为Json
// binlogToJson(message);
// 将binlog转换为protobuf格式
binlogToProtoBuf(message);
}
//8. 给服务器一个回执,告诉服务器本批次的数据已经消费过了.
connector.ack(id);
//每一批次都休息1秒钟
// Thread.sleep(1000);
}
//9. 关闭连接.
connector.disconnect();
}
// binlog解析为ProtoBuf
private static void binlogToProtoBuf(Message message) throws InvalidProtocolBufferException {
// 1. 构建CanalModel.RowData实体
CanalModel.RowData.Builder rowDataBuilder = CanalModel.RowData.newBuilder();
// 1. 遍历message中的所有binlog实体
for (CanalEntry.Entry entry : message.getEntries()) {
// 只处理事务型binlog
if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN ||
entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) {
continue;
}
// 获取binlog文件名
String logfileName = entry.getHeader().getLogfileName();
// 获取logfile的偏移量
long logfileOffset = entry.getHeader().getLogfileOffset();
// 获取sql语句执行时间戳
long executeTime = entry.getHeader().getExecuteTime();
// 获取数据库名
String schemaName = entry.getHeader().getSchemaName();
// 获取表名
String tableName = entry.getHeader().getTableName();
// 获取事件类型 insert/update/delete
String eventType = entry.getHeader().getEventType().toString().toLowerCase();
rowDataBuilder.setLogfileName(logfileName);
rowDataBuilder.setLogfileOffset(logfileOffset);
rowDataBuilder.setExecuteTime(executeTime);
rowDataBuilder.setDbName(schemaName);
rowDataBuilder.setTableName(tableName);
rowDataBuilder.setEventType(eventType);
// 获取所有行上的变更
CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
List<CanalEntry.RowData> columnDataList = rowChange.getRowDatasList();
for (CanalEntry.RowData rowData : columnDataList) {
if (eventType.equals("insert") || eventType.equals("update")) {
for (CanalEntry.Column column : rowData.getAfterColumnsList()) {
rowDataBuilder.putColumns(column.getName(), column.getValue().toString());
}
} else if (eventType.equals("delete")) {
for (CanalEntry.Column column : rowData.getBeforeColumnsList()) {
rowDataBuilder.putColumns(column.getName(), column.getValue().toString());
}
}
}
byte[] bytes = rowDataBuilder.build().toByteArray();
// 将bytes数组发送到Kafka中
for (byte b : bytes) {
//181610912111511310845981051104648484848525424-59-8938346117112100971161014211105116999711511695115104111112501110511699971151169510811110311556-24-91-12-37-1024666241021051001818495449534856505649564953485248485057662310311711410818161191191194610897110104117469911110946991106611107114101102101114101114180661110710710112111911111410018066910411612111210118151664410410311710510018365668495067566767455568485045525466514557654968455357506649525156505468706611107112971031019510510018066131091091111001171081019510510018066111071081051101079510510018066171013971161169799104101100951051101021111806646101011510111511510511111095105100183283688669546950665550768765738849499084806682895574546951658871556613109116114979910710111495117180661610121161149799107101114951161211121011806619102105112181349555346484649565746495157661510111161149799107101114951151149918066101069911111110710510118066141010111114100101114959911110010118066331010116114979910795116105109101181950484953454856455056324957584953584852662410111011101009511711510111495105100189504956555049504853661410101021051141151169510810511010718066191015115101115115105111110951181051011199511011118066141010112114111100117991169510510018066241015991171149510910111499104971101169510510018557495652506617101111211411111810511099101951051001824957661410799105116121951051001835050506671031021011011806616101210110010995979911610511810511612118066131091011001099510110997105108180661410101011001099510611198951051001806614101010510195118101114115105111110180661810811210897116102111114109186105801041111101016620101610511011610111411097108951071011211191111141001806614101011410111511710811695115117109180661610129911711411410111011695112971031011806617101310810511010795112111115105116105111110180661910159811711611611111095112111115105116105111110180
System.out.print(b);
}
System.out.println();
}
}
// binlog解析为json字符串
private static void binlogToJson(Message message) throws InvalidProtocolBufferException {
// 1. 创建Map结构保存最终解析的数据
Map rowDataMap = new HashMap<String, Object>();
// 2. 遍历message中的所有binlog实体
for (CanalEntry.Entry entry : message.getEntries()) {
// 只处理事务型binlog
if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN ||
entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) {
continue;
}
// 获取binlog文件名
String logfileName = entry.getHeader().getLogfileName();
// 获取logfile的偏移量
long logfileOffset = entry.getHeader().getLogfileOffset();
// 获取sql语句执行时间戳
long executeTime = entry.getHeader().getExecuteTime();
// 获取数据库名
String schemaName = entry.getHeader().getSchemaName();
// 获取表名
String tableName = entry.getHeader().getTableName();
// 获取事件类型 insert/update/delete
String eventType = entry.getHeader().getEventType().toString().toLowerCase();
rowDataMap.put("logfileName", logfileName);
rowDataMap.put("logfileOffset", logfileOffset);
rowDataMap.put("executeTime", executeTime);
rowDataMap.put("schemaName", schemaName);
rowDataMap.put("tableName", tableName);
rowDataMap.put("eventType", eventType);
// 封装列数据
Map columnDataMap = new HashMap<String, Object>();
// 获取所有行上的变更
CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
List<CanalEntry.RowData> columnDataList = rowChange.getRowDatasList();
for (CanalEntry.RowData rowData : columnDataList) {
if (eventType.equals("insert") || eventType.equals("update")) {
for (CanalEntry.Column column : rowData.getAfterColumnsList()) {
columnDataMap.put(column.getName(), column.getValue());
}
} else if (eventType.equals("delete")) {
for (CanalEntry.Column column : rowData.getBeforeColumnsList()) {
columnDataMap.put(column.getName(), column.getValue());
}
}
}
rowDataMap.put("columns", columnDataMap);
String json = JSON.toJSONString(rowDataMap);
System.out.println(json);
// 将这个json发送给Kafka,让Flink进行消费.
}
}
/**
* 解析BinLog日志数据
*
* @param entries
*/
private static void parseMessage(List<CanalEntry.Entry> entries) throws InvalidProtocolBufferException {
// 1. 遍历消息集合,本次遍历,相当于遍历每张表中的变化.
for (CanalEntry.Entry entry : entries) {
// 2. 判断消息的类型,如果是事务的开始,或者是事务的结束,那么这个数据我们不要.
if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN ||
entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) {
// 跳过本次循环,直接执行下一次循环
continue;
}
// 3. 开始获取我们需要的数据,比如日志文件名称/偏移量/变更类型/数据库名称/表名称/列信息....
CanalEntry.Header header = entry.getHeader();
//binlog日志文件名称
String logfileName = header.getLogfileName();
//偏移量
long logfileOffset = header.getLogfileOffset();
//变更类型
CanalEntry.EventType eventType = header.getEventType();
//数据库名称
String dbName = header.getSchemaName();
//表名称
String tableName = header.getTableName();
//SQL的执行时间
long executeTime = header.getExecuteTime();
//打印基本信息
System.out.println("logfileName: " + logfileName);
System.out.println("logfileOffset: " + logfileOffset);
System.out.println("eventType: " + eventType);
System.out.println("dbName: " + dbName);
System.out.println("tableName: " + tableName);
System.out.println("executeTime: " + executeTime);
// 4. 获取消息体中的当前行的所有记录 protobuf格式.
ByteString storeValue = entry.getStoreValue();
// ByteString中的数据都是字节,我们不能直接使用,需要转换为字符串或者是对象(能识别的)
CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(storeValue);
// rowChange中存放的是当前表中的所有变化的行信息.
List<CanalEntry.RowData> rowDatasList = rowChange.getRowDatasList();
// 遍历所有变化的行信息
for (CanalEntry.RowData rowData : rowDatasList) {
//RowData就是具体变化的某一行的数据
//获取变化之前的数据,比如删除/修改
// rowData.getBeforeColumnsList();
//获取变化之后的数据,比如新增/修改.
// rowData.getAfterColumnsList();
// 我们可以获取删除之前的数据
if ("DELETE".equals(eventType.toString())) {
//获取删除之前的数据
List<CanalEntry.Column> columnsList = rowData.getBeforeColumnsList();
printColumn(columnsList);
} else {
//对于新增和修改,获取之后的数据
List<CanalEntry.Column> columnsList = rowData.getAfterColumnsList();
printColumn(columnsList);
}
}
}
}
/**
* 打印变化的信息
*
* @param columnsList
*/
private static void printColumn(List<CanalEntry.Column> columnsList) {
//遍历列信息
for (CanalEntry.Column column : columnsList) {
//column就是每一列的封装对象, 列名/列值/变更状态
String columnName = column.getName();
String columnValue = column.getValue();
boolean updated = column.getUpdated();
System.out.println("列名: " + columnName + " 列值: " + columnValue + "状态: " + updated);
}
}
}
我们在进行数据传递的时候,会有很多额外的字段,比如字段名.
[{username: zhangsan, address:北京市}]
0: username
1: address
[{0: zhangsan, 1:北京市}]
使用Protobuf压缩后的数据是XML/JSON的三分之一左右.
可以在线安装或者本地安装: 实时数仓Day01\资料\软件\protobuf-jetbrains-plugin-0.13.0.zip
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r5bO3AIf-1591285741107)(assets/image-20200424163952820.png)]
<dependencies>
<dependency>
<groupId>com.google.protobufgroupId>
<artifactId>protobuf-javaartifactId>
<version>3.4.0version>
dependency>
dependencies>
<build>
<extensions>
<extension>
<groupId>kr.motd.mavengroupId>
<artifactId>os-maven-pluginartifactId>
<version>1.6.2version>
extension>
extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.pluginsgroupId>
<artifactId>protobuf-maven-pluginartifactId>
<version>0.5.0version>
<configuration>
<protoSourceRoot>${project.basedir}/src/main/protoprotoSourceRoot>
<protocArtifact>
com.google.protobuf:protoc:3.1.0:exe:${os.detected.classifier}
protocArtifact>
configuration>
<executions>
<execution>
<goals>
<goal>compilegoal>
goals>
execution>
executions>
plugin>
plugins>
build>
${os.detected.classifier} 如果爆红色,不影响使用,这个是获取本地一些环境的工具
${project.basedir}/src/main/proto 指定protobuf文件存放位置的配置,不要放错了.
//指定语法
syntax = "proto3";
//配置可选项,比如设置包名/类名
option java_package = "cn.itcast.proto";
option java_outer_classname = "DemoModel";
//设置我们要传递的消息的实体
message User {
// 指定当前字段的类型,还有识别码.
// 识别码,这个值是从1开始,官方建议这个值尽量在15以内,这样一个字节就可以搞定了.
int32 id = 1;
string name = 2;
string sex = 3;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-urRJDOzF-1591285741108)(assets/image-20200424165446349.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I8fRZwUy-1591285741108)(assets/image-20200424165458645.png)]
因为Protobuf已经将class放在了输入目录里面,所以我们可以直接使用,而不需要再src目录下创建对应的源码文件.
package cn.itcast.canal;
import cn.itcast.proto.DemoModel;
import com.google.protobuf.InvalidProtocolBufferException;
/**
* 测试ProtoBuf的使用
*/
public class ProtobufDemo {
public static void main(String[] args) throws InvalidProtocolBufferException {
//1. 将对象转换为字节
DemoModel.User.Builder builder = DemoModel.User.newBuilder();
// 使用构建器设置对象的属性信息
builder.setId(1);
builder.setName("张三");
builder.setSex("男");
//将对象转换为字节
DemoModel.User user = builder.build();
byte[] bytes = user.toByteArray();
for (byte b : bytes) {
//81186-27-68-96-28-72-119263-25-108-73
System.out.print(b);
}
System.out.println();
//2. 将字节转换为对象
DemoModel.User user2 = DemoModel.User.parseFrom(bytes);
System.out.println(user2.getId());
System.out.println(user2.getName());
System.out.println(user2.getSex());
}
}
syntax = "proto3";
option java_package = "cn.itcast.canal.bean";
option java_outer_classname = "CanalModel";
// 一行数据中包含的内容.
message RowData{
string logfileName = 2;
uint64 logfileOffset = 3;
string eventType = 4;
string dbName = 5;
string tableName = 6;
uint64 executeTime = 7;
//列信息
map columns = 8;
}
// binlog解析为ProtoBuf
private static void binlogToProtoBuf(Message message) throws InvalidProtocolBufferException {
// 1. 构建CanalModel.RowData实体
CanalModel.RowData.Builder rowDataBuilder = CanalModel.RowData.newBuilder();
// 1. 遍历message中的所有binlog实体
for (CanalEntry.Entry entry : message.getEntries()) {
// 只处理事务型binlog
if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN ||
entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) {
continue;
}
// 获取binlog文件名
String logfileName = entry.getHeader().getLogfileName();
// 获取logfile的偏移量
long logfileOffset = entry.getHeader().getLogfileOffset();
// 获取sql语句执行时间戳
long executeTime = entry.getHeader().getExecuteTime();
// 获取数据库名
String schemaName = entry.getHeader().getSchemaName();
// 获取表名
String tableName = entry.getHeader().getTableName();
// 获取事件类型 insert/update/delete
String eventType = entry.getHeader().getEventType().toString().toLowerCase();
rowDataBuilder.setLogfileName(logfileName);
rowDataBuilder.setLogfileOffset(logfileOffset);
rowDataBuilder.setExecuteTime(executeTime);
rowDataBuilder.setDbName(schemaName);
rowDataBuilder.setTableName(tableName);
rowDataBuilder.setEventType(eventType);
// 获取所有行上的变更
CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
List<CanalEntry.RowData> columnDataList = rowChange.getRowDatasList();
for (CanalEntry.RowData rowData : columnDataList) {
if (eventType.equals("insert") || eventType.equals("update")) {
for (CanalEntry.Column column : rowData.getAfterColumnsList()) {
rowDataBuilder.putColumns(column.getName(), column.getValue().toString());
}
} else if (eventType.equals("delete")) {
for (CanalEntry.Column column : rowData.getBeforeColumnsList()) {
rowDataBuilder.putColumns(column.getName(), column.getValue().toString());
}
}
}
byte[] bytes = rowDataBuilder.build().toByteArray();
// 将bytes数组发送到Kafka中
for (byte b : bytes) {
System.out.print(b);
}
System.out.println();
}
}
它记录了所有的DDL和DML(除了数据查询语句)语句,以事件形式记录,还包含语句所执行的消耗的时间。主要用来备份和数据同步。
binlog-format=ROW
binlog 有三种: STATEMENT、ROW、MIXED
STATEMENT 记录的是执行的sql语句
如果一个SQL对数据产生了变更,那么我只需要记录这条SQL就可以了.
update table set username = "zhangsan"
假如有1000万条数据,那么这一个SQL就产生了1000万条修改.
虽然这种方式能降低数据的记录量,但是本身不是特别稳定,有可能会造成数据不同步的问题.
ROW 记录的是真实的行数据记录 (Canal采用的是这种方式.)
如果发生一条改变,就记录一条,特别稳定.
MIXED 记录的是1+2,优先按照1的模式记录
mixed会自己选择数据的记录形式,会采用STATEMENT +ROW 2种方式进行记录.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cnhjAHcs-1591285741109)(assets/image-20200424175357387.png)]
server 代表一个 canal 运行实例,对应于一个 jvm
instance 对应于一个数据队列 (1个 canal server 对应 1…n 个 instance )
instance 下的子模块
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bmgRHZWB-1591285741110)(assets/image-20200424175452379.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O0SsIiDB-1591285741111)(assets/image-20200424180132812.png)]
EventStore负责存储解析后的Binlog事件,而解析动作负责拉取Binlog,它的流程比较复杂。需要和MetaManager进行交互。
比如要记录每次拉取的Position,这样下一次就可以从上一次的最后一个位置继续拉取。所以MetaManager应该是有状态的。
EventParser的流程如下: