當你的項目數據量上去了之後,通常會遇到兩種情況,第一種情況應是最大可能的使用cache來對抗上層的高併發,第二種情況同樣也是需要使用分庫
分表對抗上層的高併發。。。逼逼逼起來容易,做起來並不那麼樂觀,由此引入的問題,不見得你有好的解決方案,下面就具體分享下。
一:儘可能的使用Cache
比如在我們的千人千面系統中,會針對商品,訂單等維度爲某一個商家店鋪自動化建立大約400個數據模型,然後買家在淘寶下訂單之後,淘寶會將訂單推
送過來,訂單會在400個模型中兜一圈,從而推送更貼切符合該買家行爲習慣的短信和郵件,這是一個真實的業務場景,爲了應對高併發,這些模型自然都是緩
存在Cache中,模型都是從db中灌到redis的,那如果有新的模型進來了,我如何通知redis進行緩存更新呢???通常的做法就是在添加模型的時候,順便更新
redis。。。對吧,如下圖:
說的簡單,web開發的程序員會說,麻蛋的,我管你什麼業務,更新你妹啊。。。我把自己的手頭代碼寫好就可以了,我要高內聚,所以你必須碰一鼻子灰。
除了一鼻子灰之後,也許你還會遇到更新database成功,再更新redis的時候失敗,可人家不管,而且錯誤日誌還是別人的日誌系統裏面,所以你很難甚至
無法保證這個db和cache的緩存一致性,那這個時候能不能換個思路,我直接寫個程序訂閱database的binlog,從binlog中分析出模型數據的CURD操作,根
據這些CURD的實際情況更新Redis的緩存數據,第一個可以實現和web的解耦,第二個實現了高度的緩存一致性,所以新的架構是這樣的。
上面這張圖,相信大家都能看得懂,重點就是這個處理binlog程序,從binlog中分析出CURD從而更新Redis,其實這個binlog程序就是本篇所說的canal。。。
一個僞裝成mysql的slave,不斷的通過dump命令從mysql中盜出binlog日誌,從而完美的實現了這個需求。
二:數據異構
本篇開頭也說到了,數據量大了之後,必然會存在分庫分表,甚至database都要分散到多臺服務器上,現在的電商項目,都是業務趕着技術跑。。。
誰也不知道下一個業務會是一個怎樣的奇葩,所以必然會導致你要做一些跨服務器join查詢,你以爲自己很聰明,其實DBA早就把跨服務器查詢的函數給你
關掉了,求爹爹拜奶奶都不會給你開的,除非你殺一個DBA祭天,不過如果你的業務真的很重要,可能DBA會給你做數據異構,所謂的數據異構,那就是
將需要join查詢的多表按照某一個維度又聚合在一個DB中。讓你去查詢。。。。。
那如果用canal來訂閱binlog,就可以改造成下面這種架構。
三:搭建一覽
好了,canal的應用場景給大家也介紹到了,最主要是理解這種思想,人家搞不定的東西,你的價值就出來了。
1. 開啓mysql的binlog功能
開啓binlog,並且將binlog的格式改爲Row,這樣就可以獲取到CURD的二進制內容,windows上的路徑爲:C:\ProgramData\MySQL\MySQL Server 5.7\my.ini。
log-bin=mysql-bin #添加這一行就ok
binlog-format=ROW #選擇row模式
server_id=1
2. 驗證binlog是否開啓
使用命令驗證,並且開啓binlog的過期時間爲30天,默認情況下binlog是不過期的,這就導致你的磁盤可能會爆滿,直到掛掉。
show variables like 'log_%';
#設置binlog的過期時間爲30天
show variables like '%expire_logs_days%';
set global expire_logs_days=30;
3. 給canal服務器分配一個mysql的賬號權限,方便canal去偷binlog日誌。
CREATE USER canal IDENTIFIED BY 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;
show grants for 'canal'
4. 下載canal
github的地址: https://github.com/alibaba/canal/releases
5. 然後就是各種tar解壓 canal.deployer-1.0.24.tar.gz => canal
[root@localhost myapp]# ls
apache-maven-3.5.0-bin.tar.gz dubbo-monitor-simple-2.5.4-SNAPSHOT.jar nginx tengine-2.2.0.tar.gz
canal gearmand nginx-1.13.4.tar.gz tengine_st
canal.deployer-1.0.24.tar.gz gearmand-1.1.17 nginx_st tomcat
dubbo gearmand-1.1.17.tar.gz redis zookeeper
dubbo-monitor-simple-2.5.4-SNAPSHOT maven redis-4.0.1.tar.gz zookeeper-3.4.9.tar.gz
dubbo-monitor-simple-2.5.4-SNAPSHOT-assembly.tar.gz mysql-5.7.19-linux-glibc2.12-x86_64.tar.gz tengine
[root@localhost myapp]# cd canal
[root@localhost canal]# ls
bin conf lib logs
[root@localhost canal]# cd conf
[root@localhost conf]# ls
canal.properties example logback.xml spring
[root@localhost conf]# cd example
[root@localhost example]# ls
instance.properties meta.dat
[root@localhost example]#
6. canal 和 instance 配置文件
canal的模式是這樣的,一個canal裏面可能會有多個instance,也就說一個instance可以監控一個mysql實例,多個instance也就可以對應多臺服務器
的mysql實例。也就是一個canal就可以監控分庫分表下的多機器mysql。
《1》 canal.properties
它是全局性的canal服務器配置,具體如下,這裏面的參數涉及到方方面面。
#################################################
######### common argument #############
#################################################
canal.id= 1
canal.ip=
canal.port= 11111
canal.zkServers=
# flush data to zk
canal.zookeeper.flush.period = 1000
# flush meta cursor/parse position to file
canal.file.data.dir = ${canal.conf.dir}
canal.file.flush.period = 1000
## memory store RingBuffer size, should be Math.pow(2,n)
canal.instance.memory.buffer.size = 16384
## memory store RingBuffer used memory unit size , default 1kb
canal.instance.memory.buffer.memunit = 1024
## meory store gets mode used MEMSIZE or ITEMSIZE
canal.instance.memory.batch.mode = MEMSIZE
## detecing config
canal.instance.detecting.enable = false
#canal.instance.detecting.sql = insert into retl.xdual values(1,now()) on duplicate key update x=now()
canal.instance.detecting.sql = select 1
canal.instance.detecting.interval.time = 3
canal.instance.detecting.retry.threshold = 3
canal.instance.detecting.heartbeatHaEnable = false
# support maximum transaction size, more than the size of the transaction will be cut into multiple transactions delivery
canal.instance.transaction.size = 1024
# mysql fallback connected to new master should fallback times
canal.instance.fallbackIntervalInSeconds = 60
# network config
canal.instance.network.receiveBufferSize = 16384
canal.instance.network.sendBufferSize = 16384
canal.instance.network.soTimeout = 30
# binlog filter config
canal.instance.filter.query.dcl = false
canal.instance.filter.query.dml = false
canal.instance.filter.query.ddl = false
canal.instance.filter.table.error = false
canal.instance.filter.rows = false
# binlog format/image check
canal.instance.binlog.format = ROW,STATEMENT,MIXED
canal.instance.binlog.image = FULL,MINIMAL,NOBLOB
# binlog ddl isolation
canal.instance.get.ddl.isolation = false
#################################################
######### destinations #############
#################################################
canal.destinations= example
# conf root dir
canal.conf.dir = ../conf
# auto scan instance dir add/remove and start/stop instance
canal.auto.scan = true
canal.auto.scan.interval = 5
canal.instance.global.mode = spring
canal.instance.global.lazy = false
#canal.instance.global.manager.address = 127.0.0.1:1099
#canal.instance.global.spring.xml = classpath:spring/memory-instance.xml
canal.instance.global.spring.xml = classpath:spring/file-instance.xml
#canal.instance.global.spring.xml = classpath:spring/default-instance.xml
#################################################
## mysql serverId
canal.instance.mysql.slaveId = 1234
# position info,需要改成自己的數據庫信息
canal.instance.master.address = 127.0.0.1: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 = datamip
canal.instance.connectionCharset = UTF-8
# table regex
canal.instance.filter.regex = .*\\..*
#################################################
由於是全局性的配置,所以上面三處標紅的地方要注意一下:
canal.port= 11111 當前canal的服務器端口號
canal.destinations= example 當前默認開啓了一個名爲example的instance實例,如果想開多個instance,用","逗號隔開就可以了。。。
canal.instance.filter.regex = .*\\..* mysql實例下的所有db的所有表都在監控範圍內。
《2》 instance.properties
這個就是具體的某個instances實例的配置,未涉及到的配置都會從canal.properties上繼承。
#################################################
## mysql serverId
canal.instance.mysql.slaveId = 1234
# position info
canal.instance.master.address = 192.168.23.1: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 = canal
canal.instance.dbPassword = canal
canal.instance.defaultDatabaseName =datamip
canal.instance.connectionCharset = UTF-8
# table regex
canal.instance.filter.regex = .*\\..*
# table black regex
canal.instance.filter.black.regex =
#################################################
注意上面文件的以下配置項
canal.instance.master.address = 192.168.23.1:3306
canal.instance.dbUsername = canal
canal.instance.dbPassword = canal
canal.instance.defaultDatabaseName =datamip
canal.instance.filter.regex = .*\\..*
去偷binlog的時候,需要知道的mysql地址和用戶名,密碼。
7. 開啓canal
大家要記得把/canal/bin 目錄配置到 /etc/profile 的 Path中,方便快速開啓,通過下圖你會看到11111端口已經在centos上開啓了。
[root@localhost bin]# ls
canal.pid startup.bat startup.sh stop.sh
[root@localhost bin]# pwd
/usr/myapp/canal/bin
[root@localhost example]# startup.sh
cd to /usr/myapp/canal/bin for workaround relative path
LOG CONFIGURATION : /usr/myapp/canal/bin/../conf/logback.xml
canal conf : /usr/myapp/canal/bin/../conf/canal.properties
CLASSPATH :/usr/myapp/canal/bin/../conf:/usr/myapp/canal/bin/../lib/zookeeper-3.4.5.jar:/usr/myapp/canal/bin/../lib/zkclient-0.1.jar:/usr/myapp/canal/bin/../lib/spring-2.5.6.jar:/usr/myapp/canal/bin/../lib/slf4j-api-1.7.12.jar:/usr/myapp/canal/bin/../lib/protobuf-java-2.6.1.jar:/usr/myapp/canal/bin/../lib/oro-2.0.8.jar:/usr/myapp/canal/bin/../lib/netty-all-4.1.6.Final.jar:/usr/myapp/canal/bin/../lib/netty-3.2.5.Final.jar:/usr/myapp/canal/bin/../lib/logback-core-1.1.3.jar:/usr/myapp/canal/bin/../lib/logback-classic-1.1.3.jar:/usr/myapp/canal/bin/../lib/log4j-1.2.14.jar:/usr/myapp/canal/bin/../lib/jcl-over-slf4j-1.7.12.jar:/usr/myapp/canal/bin/../lib/guava-18.0.jar:/usr/myapp/canal/bin/../lib/fastjson-1.2.28.jar:/usr/myapp/canal/bin/../lib/commons-logging-1.1.1.jar:/usr/myapp/canal/bin/../lib/commons-lang-2.6.jar:/usr/myapp/canal/bin/../lib/commons-io-2.4.jar:/usr/myapp/canal/bin/../lib/commons-beanutils-1.8.2.jar:/usr/myapp/canal/bin/../lib/canal.store-1.0.24.jar:/usr/myapp/canal/bin/../lib/canal.sink-1.0.24.jar:/usr/myapp/canal/bin/../lib/canal.server-1.0.24.jar:/usr/myapp/canal/bin/../lib/canal.protocol-1.0.24.jar:/usr/myapp/canal/bin/../lib/canal.parse.driver-1.0.24.jar:/usr/myapp/canal/bin/../lib/canal.parse.dbsync-1.0.24.jar:/usr/myapp/canal/bin/../lib/canal.parse-1.0.24.jar:/usr/myapp/canal/bin/../lib/canal.meta-1.0.24.jar:/usr/myapp/canal/bin/../lib/canal.instance.spring-1.0.24.jar:/usr/myapp/canal/bin/../lib/canal.instance.manager-1.0.24.jar:/usr/myapp/canal/bin/../lib/canal.instance.core-1.0.24.jar:/usr/myapp/canal/bin/../lib/canal.filter-1.0.24.jar:/usr/myapp/canal/bin/../lib/canal.deployer-1.0.24.jar:/usr/myapp/canal/bin/../lib/canal.common-1.0.24.jar:/usr/myapp/canal/bin/../lib/aviator-2.2.1.jar:
cd to /usr/myapp/canal/conf/example for continue
[root@localhost example]# netstat -tln
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:11111 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN
tcp 0 0 192.168.122.1:53 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN
tcp6 0 0 :::111 :::* LISTEN
tcp6 0 0 :::22 :::* LISTEN
tcp6 0 0 ::1:631 :::* LISTEN
tcp6 0 0 ::1:25 :::* LISTEN
[root@localhost example]#
8. Java Client 代碼
canal driver 需要在maven倉庫中獲取一下:http://www.mvnrepository.com/artifact/com.alibaba.otter/canal.client/1.0.24,不過依賴還是蠻多的。
<!-- https://mvnrepository.com/artifact/com.alibaba.otter/canal.client -->
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.0.24</version>
</dependency>
9. 啓動java代碼進行驗證
下面的代碼對table的CURD都做了一個基本的判斷,看看是不是能夠智能感知,然後可以根據實際情況進行redis的更新操作。。。
package com.datamip.canal;
import java.awt.Event;
import java.net.InetSocketAddress;
import java.util.List;
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.Header;
import com.alibaba.otter.canal.protocol.CanalEntry.RowChange;
import com.alibaba.otter.canal.protocol.Message;
import com.google.protobuf.InvalidProtocolBufferException;
public class App {
public static void main(String[] args) throws InterruptedException {
// 第一步:與canal進行連接
CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("192.168.23.170", 11111),
"example", "", "");
connector.connect();
// 第二步:開啓訂閱
connector.subscribe();
// 第三步:循環訂閱
while (true) {
try {
// 每次讀取 1000 條
Message message = connector.getWithoutAck(1000);
long batchID = message.getId();
int size = message.getEntries().size();
if (batchID == -1 || size == 0) {
System.out.println("當前暫時沒有數據");
Thread.sleep(1000); // 沒有數據
} else {
System.out.println("-------------------------- 有數據啦 -----------------------");
PrintEntry(message.getEntries());
}
// position id ack (方便處理下一條)
connector.ack(batchID);
} catch (Exception e) {
// TODO: handle exception
} finally {
Thread.sleep(1000);
}
}
}
// 獲取每條打印的記錄
@SuppressWarnings("static-access")
public static void PrintEntry(List<Entry> entrys) {
for (Entry entry : entrys) {
// 第一步:拆解entry 實體
Header header = entry.getHeader();
EntryType entryType = entry.getEntryType();
// 第二步: 如果當前是RowData,那就是我需要的數據
if (entryType == EntryType.ROWDATA) {
String tableName = header.getTableName();
String schemaName = header.getSchemaName();
RowChange rowChange = null;
try {
rowChange = RowChange.parseFrom(entry.getStoreValue());
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
EventType eventType = rowChange.getEventType();
System.out.println(String.format("當前正在操作 %s.%s, Action= %s", schemaName, tableName, eventType));
// 如果是‘查詢’ 或者 是 ‘DDL’ 操作,那麼sql直接打出來
if (eventType == EventType.QUERY || rowChange.getIsDdl()) {
System.out.println("rowchange sql ----->" + rowChange.getSql());
return;
}
// 第三步:追蹤到 columns 級別
rowChange.getRowDatasList().forEach((rowData) -> {
// 獲取更新之前的column情況
List<Column> beforeColumns = rowData.getBeforeColumnsList();
// 獲取更新之後的 column 情況
List<Column> afterColumns = rowData.getAfterColumnsList();
// 當前執行的是 刪除操作
if (eventType == EventType.DELETE) {
PrintColumn(beforeColumns);
}
// 當前執行的是 插入操作
if (eventType == eventType.INSERT) {
PrintColumn(afterColumns);
}
// 當前執行的是 更新操作
if (eventType == eventType.UPDATE) {
PrintColumn(afterColumns);
}
});
}
}
}
// 每個row上面的每一個column 的更改情況
public static void PrintColumn(List<Column> columns) {
columns.forEach((column) -> {
String columnName = column.getName();
String columnValue = column.getValue();
String columnType = column.getMysqlType();
boolean isUpdated = column.getUpdated(); // 判斷 該字段是否更新
System.out.println(String.format("columnName=%s, columnValue=%s, columnType=%s, isUpdated=%s", columnName,
columnValue, columnType, isUpdated));
});
}
}
<1> Update操作
<2> Insert操作
<3> Delete 操作
從結果中看,沒毛病,有圖有真相,好了,本篇就說到這裏,對於開發的你,肯定是有幫助的~~~