使用场景
场景1: 更新缓存
场景2:抓取业务数据表的新增和变化数据,用于制作拉链表
场景3:抓取业务数据表的新增和变化数据,用于实时统计
工作原理
把自己伪装成slave 假装从master 复制数据
复制过程分为三步
1、master 主库 DDL DML 除了查询语句写进二进制文件(binary log)中。
2、slave 从库向master 发送dump协议,将master 的 binary log events 拷贝到自己的中继日志(relay log) 中。
3、slave 读取并重做 中继日志中的事件,将改变的数据同步到自己的数据库。
mysql的binlog
MySQL的二进制日志可以说是MySQL最重要的日志了,它记录了所有的DDL和DML(除了数据查询语句)语句,以事件形式记录,还包含语句所执行的消耗的时间,MySQL的二进制日志是事务安全型的。
一般来说开启二进制日志大概会有1%的性能损耗 。二进制有两个最重要的使用场景:
其一:MySQL Replication在Master端开启binlog,Mster把它的二进制日志传递给slaves来达到master-slave数据一致的目的。
其二:自然就是数据恢复了,通过使用mysqlbinlog工具来使恢复数据。
二进制日志包括两类文件:二进制日志索引文件(文件名后缀为.index)用于记录所有的二进制文件,二进制日志文件(文件名后缀为.00000*)记录数据库所有的DDL和DML(除了数据查询语句)语句事件
binlog的开启
编辑my.cnf文件 在[mysqld]区块添加
log-bin=mysql-bin
binlog的分类设置
mysql binlog的格式,那就是有三种,分别是STATEMENT,MIXED,ROW。
在配置文件中可以选择配置
binlog_format=row
区别:
1) statement
语句级,binlog会记录每次一执行写操作的语句。
相对row模式节省空间,但是可能产生不一致性,比如
update tt set create_date=now()
如果用binlog日志进行恢复,由于执行时间不同可能产生的数据就不同。
优点: 节省空间
缺点: 有可能造成数据不一致。
2) row
行级, binlog会记录每次操作后每行记录的变化。
优点:保持数据的绝对一致性。因为不管sql是什么,引用了什么函数,他只记录执行后的效果。
缺点:占用较大空间。
3) mixed
statement的升级版,一定程度上解决了,因为一些情况而造成的statement模式不一致问题
在某些情况下譬如:
当函数中包含 UUID() 时;
包含 AUTO_INCREMENT 字段的表被更新时;
执行 INSERT DELAYED 语句时;
用 UDF 时;
会按照 ROW的方式进行处理
优点:节省空间,同时兼顾了一定的一致性。
缺点:还有些极个别情况依旧会造成不一致,另外statement和mixed对于需要对binlog的监控的情况都不方便。
mysql准备
在mysql 中执行
创建用户赋予权限
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%' IDENTIFIED BY 'canal' ;
flush privileges;
修改my.cnf 文件
server-id= 1
log-bin=mysql-bin
binlog_format=row
binlog-do-db=你要监控的数据库名
如果改了mysql 的配置 一定要重启,配置才会生效
重启之后bin-log 文件会生成一个新的,我们验证一下是否开启成功
这是我新生成的
可以执行一些DDL 看文件大小是否变化
canal 安装
https://github.com/alibaba/canal/releases
把canal.deployer-1.1.2.tar.gz拷贝到linux
修改canal的配置
下面是canal 的配置结构
一个主服务
多个实例服务
一个实例服务对应一个mysql 服务器
也就是说一个canal 可以监控多台mysql服务器
mkdir canal
tar -zxvf canal.deployer-1.1.2.tar.gz -C canal
cd canal
vim conf/canal.properties
这个文件是canal的基本通用配置,主要关心一下端口号,不改的话默认就是11111
这里就是定义起多少个实例 多个的话用逗号分隔
canal.destinations = example,example1,example2,example3
这是目录结构,实例的实例的名称和文件夹的名称可以自定义,只要两个能对应上就行
vim conf/example/instance.properties
instance.properties是针对要追踪的mysql的实例配置
需要配置的项
canal.instance.mysql.slaveId=2
#mysql 的ip
canal.instance.master.address=localhost:3306
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
canal.instance.connectionCharset = UTF-8
#实例的名称
canal.mq.topic=example
启动canal
./bin/startup.sh
拿canal监控的数据
把canal 监控到的数据写进kafka
需要canal-client kafka-client 的依赖
maven 工程
版本不同自己适配
<dependencies>
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.1.0-cdh6.2.1</version>
</dependency>
</dependencies>
通用监视类 CanalClient
mysql 数据表准备
create database testcanal;
create table student(id int(11),name varchar(255));
create table teacher(id int(11),name varchar(255));
java代码
CanalClient
package canal;
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.InvalidProtocolBufferException;
import java.net.InetSocketAddress;
import java.util.List;
public class CanalClient {
public static void watch(String hostname, int port, String destination, String tables) {
//创建连接器
//这里的用户名密码是canal server 的用户名密码 可以才server的配置文件中配置 用作权限管理
CanalConnector canalConnector = CanalConnectors.newSingleConnector(new InetSocketAddress(hostname, port), destination,"","" );
//实时监控
while (true) {
//建立连接
canalConnector.connect();
//订阅表
canalConnector.subscribe(tables);
//抓取canal监控到的消息 一次多少个entry
//canal 相当于一个队列 mysql 往里放,canal client 往外拿
//有1000 每次拿100
//有10 拿10
Message message = canalConnector.get(100);
int size = message.getEntries().size();
if (size == 0) {
//没有数据休息5S
System.out.println("没有数据!!休息5秒");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
//有数据
for (CanalEntry.Entry entry : message.getEntries()) {
//每一个entry 对应一条sql 产生变化的行 如果一条sql造成50行的数据变化 这50行数据会放在一个entry里面
//mysql是有事务的 每条sql 语句之前都会有事务开启 语句之后有事务关闭,但是这些都会记录在bin-log 里面,我们这里要过滤一下
//如果数据是row 类型的 前面我们有设置
if (entry.getEntryType() == CanalEntry.EntryType.ROWDATA) {
CanalEntry.RowChange rowChange = null;
try {
//反序列化storeValue
rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
String tableName = entry.getHeader().getTableName();// 表名
CanalEntry.EventType eventType = rowChange.getEventType();//insert update delete drop alter ?
List<CanalEntry.RowData> rowDatasList = rowChange.getRowDatasList();//行集 数据
CanalHandler canalHandler = new CanalHandler(eventType, tableName, rowDatasList);
canalHandler.handle();
}
}
}
}
}
public static void main(String[] args) {
watch("localhost",11111,"example","testcanal.*");
}
}
CanalHandler
package canal;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.otter.canal.protocol.CanalEntry;
import java.util.List;
public class CanalHandler {
CanalEntry.EventType eventType;
String tableName;
List<CanalEntry.RowData> rowDataList;
public CanalHandler(CanalEntry.EventType eventType, String tableName, List<CanalEntry.RowData> rowDataList) {
this.eventType = eventType;
this.tableName = tableName;
this.rowDataList = rowDataList;
}
public void handle(){
//如果是student 表,并且是插入操作
if("student".equals(tableName)&& CanalEntry.EventType.INSERT==eventType){
rowDateList2Kafka( "canal_student_insert");
}else if ("teacher".equals(tableName)&& (CanalEntry.EventType.INSERT==eventType||CanalEntry.EventType.UPDATE==eventType)) {
rowDateList2Kafka( "canal_teacher_insert_update");
}
}
private void rowDateList2Kafka(String kafkaTopic){
for (CanalEntry.RowData rowData : rowDataList) {
List<CanalEntry.Column> columnsList = rowData.getAfterColumnsList();
JSONObject jsonObject = new JSONObject();
for (CanalEntry.Column column : columnsList) {
System.out.println(column.getName()+"::"+column.getValue());
jsonObject.put(column.getName(),column.getValue());
}
MykafkaSender.send(kafkaTopic,jsonObject.toJSONString());
}
}
}
MykafkaSender
package canal;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;
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(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hw-node1:9092");//kafka集群,broker-list
properties.put(ProducerConfig.ACKS_CONFIG, "all");
properties.put(ProducerConfig.RETRIES_CONFIG, 100);//重试次数
properties.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 1);//这个参数不设置,失败重试时会改变消息的顺序
properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG,536870912 );//重试次数
properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 10000);//批次大小
properties.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG,360000);
properties.put(ProducerConfig.MAX_BLOCK_MS_CONFIG,1800000);
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
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));
}
}
消费kafka topic
kafka-console-consumer --bootstrap-server hw-node3:9092 --topic canal_student_insert
往表中插入数据
insert into student values(1,'yang');
insert into student values(1,'yang');
kafka topic
监控数据的后续处理
1、上传到hdfs 用于制作拉链表
2、spark streaming 实时分析