canal主要用途是基於 MySQL 數據庫增量日誌解析,提供增量數據訂閱和消費。在大數據中廣泛用於實時數據的採集。
1.canal原理
mysql並沒有實現增量數據的查閱消費功能,先來說說mysql主備複製原理。
mysql主節點對數據庫做了任何寫操作,都會寫入Binary log文件。而slave備份節點會主動去master節點讀取Binary log文件,拷貝到自己的節點上,變爲Relay log。最後slave節點讀取Relay log看看它對數據庫做了哪些操作,按照順序執行日誌的操作,達到同步更新的結果。
而canal正是利用了mysql主備複製原理。canal將自己僞裝成一個mysql的備份節點,從master拉取binary log拷貝到自己節點上,然後根據用戶的自定義的程序執行操作,可以寫入mysql,kafka,es,hbase等。
插曲:
其實最開始canal並不是用於大數據的,而是用於緩存的。比如一些併發訪問量大的程序可能會做一些緩存處理,而不是直接從mysql讀取。比如客戶端查詢蘋果手機價格,先從redis查詢,沒有查詢到,繼續從mysql查詢。查詢到價格後是5000元,將蘋果手機價格5000保存在redis中,下次客戶端直接從redis緩存中讀取數據。過了一段時間,保存在mysql的蘋果手機降價了,價格是4500元,如果現在再從redis緩存讀取,那麼價格是錯誤的。可是客戶端發現緩存中已經有數據了,它不可能再從mysql讀取數據了。所以我們需要實時更新redis緩存的數據,因此canal就起到了至關重要的作用。
2.canal搭建
1.開啓mysql的binlog。
默認情況下mysql的binlog是沒有開啓的,所以我們首先要開啓binlog。
修改 /etc/my.cnf 文件,在文件末尾加入以下幾句話。
server-id相當於每一個mysql服務器的唯一標識,後面的canal也要設定,和mysql的不一樣即可。
log-bin是設置日誌文件的前綴名。設置完成後操作數據庫,在 /var/lib/mysql/ 目錄下就可以看到以mysql-bin開頭的文件。
binlog_format是設置以什麼樣的形式來存儲數據庫變更的數據。這裏詳細解釋下mysql的幾種記錄方式。
(1)binlog_format=statement
直接保存mysql語句。比如你執行了一句sql,update user_info set update_time='xxxx' where create_time='xxxx';日誌文件會直接保存這句sql,大大減少了日誌文件存儲的內容。但是有一個缺點。比如update user_info set age=rand() where update_time='xxxx';其中age的賦值是rand(),取值是隨機的。所以第二次執行sql的時候賦值會出問題。
(2)binlog_format=row
保存每行記錄的變化。也就是隻要有一行變化就記錄一行,這樣做的確規避了rand()類似的問題。但是也有一個缺點,就是update user_info set update_time='xxxx' where create_time='xxxx';這句sql如果有100萬的數據改變,那麼binlog就要記錄100萬行的數據,這顯然不是很方便。
(3)binlog_format=mixed
一般情況下mysql會用statement來記錄,遇到特殊情況就用row來記錄,相當於結合了statement和row。
介紹完3種情況,結合我們採集實時數據,是需要知道每行的數據變化的,所以這裏我們需要將binlog_format設置爲row。
binlog-do-db是設置要監控的數據庫名稱。
2.安裝canal
(1)下載解壓canal。
(2)修改 conf/canal.properties。
//設置canal端口號
canal.port=11111
//取消註釋
canal.instance.parser.parallelThreadSize = 16
(3)修改 conf/example/instance.properties
//slaveId和mysql的server-id要不一樣
canal.instance.mysql.slaveId=10
//設置要監控的mysql地址
canal.instance.master.address=hadoop102:3306
//由於canal要讀取mysql的內容,所以要給予canal一個權限。這裏在mysql裏面創建了一個canal賬戶。(在庫mysql執行的sql:GRANT ALL PRIVILEGES ON *.* TO canal@'%' IDENTIFIED BY 'canal' ,執行完可以在user表查詢是否添加成功)
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
canal.instance.connectionCharset = UTF-8
3.啓動canal
./bin/startup.sh
啓動完成後可以在/bigdata/canal/logs/example.log中查看日誌是否啓動成功。
3.Canal客戶端消費。
1.maven引入canal客戶端,kafka客戶端(我這裏直接將結果保存在kafka中)。
<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>0.11.0.2</version>
</dependency>
2.編寫客戶端代碼
類CanalApp
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.Column;
import com.alibaba.otter.canal.protocol.CanalEntry.Entry;
import com.alibaba.otter.canal.protocol.CanalEntry.EntryType;
import com.alibaba.otter.canal.protocol.CanalEntry.EventType;
import com.alibaba.otter.canal.protocol.CanalEntry.RowChange;
import com.alibaba.otter.canal.protocol.CanalEntry.RowData;
import com.alibaba.otter.canal.protocol.Message;
import com.google.common.base.CaseFormat;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import java.net.InetSocketAddress;
import java.util.List;
public class CanalApp {
public static void main(String[] args) {
//創建連接器
CanalConnector canalConnector = CanalConnectors.newSingleConnector(new InetSocketAddress("192.168.1.128", 11111), "example", "", "");
while(true){
//連接,訂閱,抓取
canalConnector.connect();
//監控數據庫中某張表
canalConnector.subscribe("gmall.order_info");
//message:一次canal從日誌中抓取的信息,一個message包含多個sql(event)
Message message = canalConnector.get(100);
int size = message.getEntries().size();
if(size == 0){
try {
System.out.println("沒有數據,暫停5秒");
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
//entry: 相當於一個sql命令,一個sql可能會對多行記錄造成影響
for (Entry entry : message.getEntries()){
//判斷事件類型,只處理行變化的數據,過濾掉事務變化的數據
if(entry.getEntryType().equals(EntryType.ROWDATA)){
ByteString storeValue = entry.getStoreValue();
RowChange rowChange = null;
try {
//rowchange : entry經過反序列化得到的對象,包含了多行記錄的變化值
rowChange = RowChange.parseFrom(storeValue);
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
//獲得行集
List<RowData> rowDataList = rowChange.getRowDatasList();
//eventtype 數據的變化類型 insert update delete create alter drop
EventType eventType = rowChange.getEventType();
String tableName = entry.getHeader().getTableName();
//這裏只監控order_info表新增的數據
if("order_info".equals(tableName) && EventType.INSERT.equals(eventType)){
for (RowData rowData : rowDataList){
List<Column> afterColumnsList = rowData.getAfterColumnsList();
JSONObject jsonObject = new JSONObject();
for (Column column : afterColumnsList){
System.out.println(column.getName() + ":" + column.getValue());
//駝峯命名處理
String propertyName = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, column.getName());
jsonObject.put(propertyName, column.getValue());
}
MyKafkaSender.send("GMALL_ORDER", jsonObject.toJSONString());
}
}
}
}
}
}
}
}
類MyKafkaSender
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 void send(String topic, String msg) {
if(kafkaProducer == null){
kafkaProducer = createKafkaProducer();
}
kafkaProducer.send(new ProducerRecord<String, String>(topic, msg));
}
private static KafkaProducer<String, String> createKafkaProducer() {
Properties properties = new Properties();
properties.put("bootstrap.servers", "hadoop102:9092,hadoop103:9092,hadoop104: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 = new KafkaProducer<String, String>(properties);
return producer;
}
}
3.運行程序查看結果。
運行zookeeper
運行kafka集羣
開啓kafka-console消費
啓動canal服務端
運行CanalApp
向表gmall.order_info插入數據後控制檯kafka消費如下: