基於Canal+Kafka實現快取實時更新

ligkwww發表於2021-03-30

前言

相信對於大部分同學來說,快取應該是平時開發中會經常接觸的東西了。常規的邏輯一般是「查詢」->「檢查是否有快取」如果有,就直接返回;如果沒有,則從DB中讀取資料。後續對資料進行過更新之後,再刪除/更新快取。

所以從流程上來看讀快取很容易,難的是保證快取資料的有效性,大多數的做法是在業務程式碼中嵌入更新快取的邏輯。比如在修改某篇文章成功之後,刪除原有的 key。

正常情況下這種方式沒有問題,但有時我們的系統中不止一個地方會修改這些資料,那麼我們就不得不在每處業務程式碼中植入更新快取的邏輯,隨著時間的推移,我們的程式碼變得越來越臃腫,難以維護。

那麼有沒有什麼方法可以讓我們可以不用花費太多精力來關注業務之外的事情呢?當然有,這就是我們今天要介紹的工具 Canal。

Canal介紹

canal 是阿里巴巴旗下的一款開源專案,純 Java 開發。基於資料庫增量日誌解析,提供增量資料訂閱&消費,可以很方便地同步資料庫的增量資料到其他的儲存應用。

他的原理是把自己偽裝成 MySQL slave,模擬 MySQL slave 的互動協議向 MySQL Mater傳送 dump協議,MySQL master 收到 canal 傳送過來的 dump 請求,開始推送 binary log 給 canal,然後 canal 解析 binary log,再傳送到儲存目的地,比如 MySQL,Kafka,Elastic Search 等等。

那麼我們今天要分享的就是基於 Mysql+Canal+Kafka+Redis 來實現快取資料的實時更新。

環境準備

  • Canal
  • Mysql
  • Zookeeper
  • Kafka
  • Redis

    具體安裝流程就不在這裡說明了,網上都有,嫌麻煩的同學可以直接使用docker進行安裝

image

工作流程

image

說明:大體流程就是 canal 充當一個 mysql 的從伺服器,從 master 拉取 binlog 變化,將更新內容推送至 kafka 中,然後客戶端啟動消費者訂閱主題,根據資料變化執行對應的業務邏輯。

Mysql

首先資料庫需要開啟 binlog
my.cnf

[mysqld]
pid-file        = /var/run/mysqld/mysqld.pid
socket          = /var/run/mysqld/mysqld.sock
datadir         = /var/lib/mysql
log-bin         = /var/log/mysql/mysql-bin
binlog-format   = row
server-id       = 1

然後需要建立一個 canal 使用者,用於 canal 例項拉取 binlog,具體庫許可權根據自身業務匹配,我這裡直接給到所有。

create user canal@'%' identified by 'canal';
grant all privileges on *.* to 'canal'@'%';
flush privileges;

Canal配置

接下來就是 canal 的配置,主要分為兩部分:全域性配置(canal.properties) 和 例項配置(instance.properties),配置檔案在 canal-server/conf 下,這裡只列出一些修改項,其他的均採用預設配置。

canal.properties:

#canal引數
canal.register.ip = 172.17.0.4        # canal伺服器地址
canal.port = 11111                    # 埠

#zookeeper
canal.zkServers = 172.18.0.2:2181    #zookeeper的伺服器地址及埠
canal.serverMode = kafka            #服務模式 : tcp, kafka, rocketMQ, rabbitMQ

#destinations
canal.destinations = example        # 例項

#kafka
kafka.bootstrap.servers = kafka_kafka_1:9092    #kafka地址

instance.properties:

canal.instance.mysql.slaveId=999                # mysql->slaveId,其實就是mysql的server-id,不要和資料庫的server-id衝突

canal.instance.master.address=172.17.0.2:3306    # mysql連線地址
canal.instance.master.journal.name=mysql-bin.000005    #mysql-binlog日誌
canal.instance.master.position=462                # 日誌偏移位置

canal.instance.dbUsername=canal                    # mysql使用者名稱
canal.instance.dbPassword=canal                    # mysql密碼

canal.instance.filter.regex=.*\\..*                # 監聽庫表,當前配置是監聽所有庫所有表
#canal.instance.filter.regex=test\\..*            # 此配置是監聽test庫下所有表        

canal.mq.topic=canal                            # Kafka topic名稱

Kafka

Kafka相關內容可以參考「Kafka應用」系列文章

操作演示

  1. 啟動canal

    docker run --name canal --network kafka_default -v /Users/admin/docker/conf/canal:/home/admin/canal-server/conf -p 11111:11111 -p 11112:11112 -p 11110:11110 -d canal/canal-server
  2. 修改資料庫
    image

  3. MQ消費者(Client)

此時Kafka消費者會讀取到一條訊息,內容為本次更新內容。

image

  1. MQ消費者(PHP)
    使用 PHP 客戶端來消費 Kafka 資料。

    public function handle()
     {
         $this->line("開啟消費者...");
         $conf = new \RdKafka\Conf();
         $conf->set('group.id', 'test');
         $conf->set('metadata.broker.list', '192.168.65.2:32768');
         $conf->set('enable.auto.commit', 'false');
         $conf->set('auto.offset.reset', 'earliest');
    
         $consumer = new \RdKafka\KafkaConsumer($conf);
         $consumer->subscribe(['canal']);
         $this->line("訂閱主題...");
    
         while (true) {
             $message = $consumer->consume(120*10000);
             $this->line("接收訊息...");
             switch ($message->err) {
                 case RD_KAFKA_RESP_ERR_NO_ERROR:
                     $payload = json_decode($message->payload, true);
                     $this->line("產生的操作:".$payload['type']);
                     $this->line("變更的庫:".$payload['database']);
                     $this->line("變更的表:".$payload['table']);
                     echo "變更的資料:";
                     var_dump($payload['data']);
                     // 根據變更資料執行具體業務
                     // redis->del("xxx")    刪除快取等
    
                     $consumer->commit($message);
                     break;
    
                 case RD_KAFKA_RESP_ERR__PARTITION_EOF:
                     echo "No more messages; will wait for more\n";
                     break;
    
                 case RD_KAFKA_RESP_ERR__TIMED_OUT:
                     echo "Timed out\n";
                     break;
    
                 default:
                     throw new \Exception($message->errstr(), $message->err);
                     break;
             }
         }
     }

執行結果:
image
至此,我們在客戶端已經獲取到資料庫實時變更資料,對應的可以執行我們自身的業務邏輯,比如:傳送通知、快取更新等操作。

注意事項

  1. 連線不上mysql/zookeeper的問題

    檢查配置檔案中mysql或zookeeper的連線地址是否正確,canal所在伺服器是否可以ping通
    docker環境下需要提前配置網路環境,已達到容器可以互相訪問的目的。
    docker –link mysql:mysql # 建立和mysql容器的連線
    docker –network kafka_default # 將當前容器加入至指定網路中。
    注意:link和network同時使用時,network會將link覆蓋。需要使用docker network connect命令來連線link所在的網路。
    例如:
    docker inspect mysql # 檢視mysql的network-> bridge
    docker network connect bridge canal #將canal容器加入至bridge中。
    或者使用compose.yml的方式進行網路設定,docker run命令下只能連線一個網路。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章