数据同步之canal搭建和php操作

mysql数据库的变更,同步到es或者其它组件里,如果要监控的表字段较多,耦合到业务代码里 需要修改的地方会特别多,或者改不全

下面简单部署和操作canal

Canal 是阿里巴巴开源的一款基于 MySQL 数据库增量日志解析的技术框架,主要用于捕获数据库的 Binlog 数据变更(CDC,Change Data Capture)。以下是关于 Canal 的详细介绍:

核心功能

  • Binlog 解析:Canal 通过解析 MySQL 的 Binlog 文件,捕获数据库的增删改操作。
  • 实时数据同步:将捕获到的数据变更实时同步到其他系统(如搜索引擎、数据仓库等)。
  • 轻量级部署:支持单机部署和集群部署,适用于中小规模和大规模应用场景。

适用场景

  • 实时数据同步:例如将 MySQL 数据同步到 Elasticsearch 或 Hadoop。
  • 数据审计:记录数据库的历史变更。
  • 缓存更新:根据数据库变更动态更新缓存。
  • 数据分发:将数据库变更推送到消息队列(如 Kafka、RabbitMQ)。
  • 架构组成
    Canal Server:负责连接 MySQL 并解析 Binlog 数据。
    Canal Client:接收 Canal Server 发送的数据变更事件。
    Canal Admin:提供管理界面,用于监控和配置 Canal 实例。

docker-compose 部署 Mysql和canal
docker-compose.yml

version: '3'

services:
   mysql:
     image: registry.cn-beijing.aliyuncs.com/hkui_dev/mysql:5.7
     container_name: mysql
     environment:
      MYSQL_ROOT_PASSWORD: your_root_password
     ports:
      - "3306:3306"
     volumes:
      - ./mysql-data:/var/lib/mysql
     command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --log-bin=mysql-bin --binlog-format=ROW --server-id=1
     networks:
      - canal-net

   canal-server:
    image: registry.cn-beijing.aliyuncs.com/hkui_dev/canal-server:v1.1.7
    container_name: canal-server
    restart: unless-stopped
    depends_on:
      - mysql
    ports: 
      - 11111:11111
    environment:
      - canal.auto.scan=false
      - canal.instance.master.address=mysql:3306
      - canal.instance.dbUsername=canal
      - canal.instance.dbPassword=canal
      - canal.instance.filter.regex=.*\\..*
      - canal.destinations=test
      - canal.instance.connectionCharset=UTF-8
      - canal.instance.tsdb.enable=true
    volumes:
      - /root/canal/test/log/:/home/admin/canal-server/logs/
    networks:
      - canal-net
networks:
  canal-net:
    driver: bridge
docker-compose up -d
[root@master1 canal]# docker-compose ps
    Name                  Command               State                                      Ports
------------------------------------------------------------------------------------------------------------------------------------
canal-server   /alidata/bin/main.sh /home ...   Up      11110/tcp, 0.0.0.0:11111->11111/tcp,:::11111->11111/tcp, 11112/tcp, 9100/tcp
mysql          docker-entrypoint.sh --cha ...   Up      0.0.0.0:3306->3306/tcp,:::3306->3306/tcp, 33060/tcp

mysql配置

验证binlog的配置

mysql> show variables like 'binlog_format';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| binlog_format | ROW   |
+---------------+-------+
1 row in set (0.19 sec)
mysql>
mysql> show variables like 'log_bin';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_bin       | ON    |
+---------------+-------+
1 row in set (0.00 sec)
mysql>
mysql> show master status;
+------------------+----------+--------------+------------------+-------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+------------------+-------------------+
| mysql-bin.000003 |     4230 |              |                  |                   |
+------------------+----------+--------------+------------------+-------------------+
1 row in set (0.00 sec)

然后创建用户,并授权

CREATE USER canal IDENTIFIED BY 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;
show grants for 'canal'@'%'

php 操作canal

目录结构

[root@39b6932a18f7 canal]# tree -L 1
.
├── CanalDispose.php
├── composer.json
├── composer.lock
├── index.php
└── vendor

vendor 和composer.lock 为compser update 时产生
composer.json

{
    "require": {
        "xingwenge/canal_php": "^1.0"
    }
}

index.php

<?php

use xingwenge\canal_php\CanalClient;
use xingwenge\canal_php\CanalConnectorFactory;
use xingwenge\canal_php\Fmt;

ini_set('display_errors', 'On');
error_reporting(E_ALL);

require "./vendor/autoload.php";
require "./CanalDispose.php";

try {
    $client = CanalConnectorFactory::createClient(CanalClient::TYPE_SOCKET_CLUE);

    $client->connect("192.168.52.110", 11111);
    $client->checkValid();
    #$client->subscribe("1001", "test",  ".*\\..*");
    $client->subscribe("1001", "test", "test\\.(user|user_1)");//监控test库下user和user_1表


    while (true) {
        $message = $client->get(100);
        if ($entries = $message->getEntries()) {
            foreach ($entries as $entry) {
                $data = CanalDispose::listen($entry);
                if (!empty($data)) {
                    echo "data:" . print_r($data, 1) . PHP_EOL;
                }
            }
        }
        sleep(1);
    }
    $client->disConnect();
} catch (\Exception $e) {
    echo $e->getMessage(), PHP_EOL;
    print_r($e->getTraceAsString());
}

CanalDispose.php

<?php

use Com\Alibaba\Otter\Canal\Protocol\Column;
use Com\Alibaba\Otter\Canal\Protocol\Entry;
use Com\Alibaba\Otter\Canal\Protocol\EntryType;
use Com\Alibaba\Otter\Canal\Protocol\EventType;
use Com\Alibaba\Otter\Canal\Protocol\RowChange;
use Com\Alibaba\Otter\Canal\Protocol\RowData;

class CanalDispose
{
    /**
     * @param Entry $entry
     * @throws \Exception
     */
    public static function println($entry)
    {
        switch ($entry->getEntryType()) {
            case EntryType::TRANSACTIONBEGIN:
            case EntryType::TRANSACTIONEND:
                return;
                break;
        }

        $rowChange = new RowChange();
        $rowChange->mergeFromString($entry->getStoreValue());
        $evenType = $rowChange->getEventType();
        $header = $entry->getHeader();

        echo sprintf("================> binlog[%s : %d],name[%s,%s], eventType: %s", $header->getLogfileName(), $header->getLogfileOffset(), $header->getSchemaName(), $header->getTableName(), $header->getEventType()), PHP_EOL;
        echo $rowChange->getSql(), PHP_EOL;

        /** @var RowData $rowData */
        foreach ($rowChange->getRowDatas() as $rowData) {
            switch ($evenType) {
                case EventType::DELETE:
                    self::ptColumn($rowData->getBeforeColumns());
                    break;
                case EventType::INSERT:
                    self::ptColumn($rowData->getAfterColumns());
                    break;
                default:
                    echo '-------> before', PHP_EOL;
                    self::ptColumn($rowData->getBeforeColumns());
                    echo '-------> after', PHP_EOL;
                    self::ptColumn($rowData->getAfterColumns());
                    break;
            }
        }
    }

    public static function getList($entry)
    {
        switch ($entry->getEntryType()) {
            case EntryType::TRANSACTIONBEGIN:
            case EntryType::TRANSACTIONEND:
                return [];
        }
        $rowChange = new RowChange();
        $rowChange->mergeFromString($entry->getStoreValue());
        $evenType = $rowChange->getEventType();
        $header = $entry->getHeader();
        $mysqlType = '';
        /** @var RowData $rowData */
        $data = [];
        foreach ($rowChange->getRowDatas() as $rowData) {
            switch ($evenType) {
                case EventType::DELETE:
                    $mysqlType = 'delete';
                    $data[] = self::getColumn($rowData->getBeforeColumns());
                    break;
                case EventType::INSERT:
                    $data[] = self::getColumn($rowData->getAfterColumns());
                    $mysqlType = 'insert';
                    break;
                default:
                    $mysqlType = 'update';
                    $data[] = self::getColumn($rowData->getAfterColumns());
                    break;
            }
        }
        return [
            'tableName' => $header->getTableName(),
            'data' => $data,
            'type' => $mysqlType
        ];
    }


    private static function getColumn($columns,$onlyChange=false)
    {
        /** @var Column $column */
        $data = [];
        foreach ($columns as $column) {
            if($onlyChange){
                if($column->getUpdated()){
                    $data[$column->getName()] = $column->getValue();
                }
            }else{
                $data[$column->getName()] = $column->getValue();
            }
        }
        return $data;
    }


    private static function ptColumn($columns)
    {
        /** @var Column $column */
        foreach ($columns as $column) {
            echo sprintf("%s : %s  update= %s", $column->getName(), $column->getValue(), var_export($column->getUpdated(), true)), PHP_EOL;
        }
    }

    /**
     * @param $entry Com\Alibaba\Otter\Canal\Protocol\Entry
     * @return array|false
     * @throws Exception
     */
    public static function listen($entry)
    {
        switch ($entry->getEntryType()) {
            case EntryType::TRANSACTIONBEGIN:
            case EntryType::TRANSACTIONEND:
                return [];
        }
        $rowChange = new RowChange();
        $rowChange->mergeFromString($entry->getStoreValue());
        $evenType = $rowChange->getEventType();
        $header = $entry->getHeader();
        $mysqlType = '';
        /** @var RowData $rowData */
        $data = [];
        //一次操作一千条数据返回 false;
        if (count($rowChange->getRowDatas()) > 2000) {
            return false;
        }
        foreach ($rowChange->getRowDatas() as $rowData) {
            
            switch ($evenType) {
                case EventType::DELETE:
                    $mysqlType = 'delete';
                    $data[] = self::getColumn($rowData->getBeforeColumns());
                    break;
                case EventType::INSERT:
                    $data[] = self::getColumn($rowData->getAfterColumns());
                    $mysqlType = 'insert';
                    break;
                default:
                    $mysqlType = 'update';
                    $after = self::getColumn($rowData->getAfterColumns(),true);
                    $data[] = [
                        'before'=>self::getColumn($rowData->getBeforeColumns()),
                        'after'=>$after,
                    ];
                    break;
            }
        }
        return [
            'tableName' => $header->getTableName(),
            'data' => $data,
            'type' => $mysqlType
        ];
    }
}

php index.php

curd表数据 ,看这个的变化

canal的一些参数解释

canal.auto.scan=false 是 Canal 配置中的一个参数,用于控制 Canal 是否自动扫描并加载实例配置文件

  • 参数含义
    canal.auto.scan:这是一个布尔类型的配置项。
    如果设置为 true,Canal 会自动扫描 conf/instance/ 目录下的实例配置文件(通常是 .properties 文件),并根据这些配置文件动态加载对应的 Canal 实例。
    如果设置为 false,Canal 不会自动扫描该目录,而是需要通过手动方式(例如 API 或命令行)来显式加载实例。
  • 默认值
    默认情况下,canal.auto.scan 的值为 true,即 Canal 启动时会自动扫描实例配置文件并加载所有实例。
  • 使用场景
    (1) 设置为 true 的场景
    适用场景:
    当您希望 Canal 在启动时自动加载所有配置好的实例时。
    适用于静态配置场景,即实例数量和配置在启动前已经确定,且不需要动态调整。
    优点:
    简化操作流程,无需额外手动加载实例。
    缺点:
    如果实例数量较多或配置复杂,可能会增加 Canal 的启动时间。
    (2) 设置为 false 的场景
    适用场景:
    当您需要动态管理 Canal 实例时,例如在运行时根据需求动态添加、删除或修改实例。
    适用于动态配置场景,例如使用 API 或其他工具动态管理 Canal 实例。
    优点:
    提供更高的灵活性,便于动态管理实例。
    减少 Canal 启动时的资源消耗(因为不会自动加载所有实例)。
    缺点:
    需要额外的操作来手动加载实例。

手动加载实例的方式

当 canal.auto.scan=false 时,可以通过以下方式手动加载 Canal 实例:

(1) 使用 API
Canal 提供了 RESTful API 接口,允许您通过 HTTP 请求动态加载或卸载实例。例如:

curl -X POST http://<canal-server-ip>:8089/canal/instance/start/<instanceName>

其中 是您需要加载的实例名称。

(2) 使用命令行工具
某些版本的 Canal 提供了命令行工具,可以直接通过命令行启动或停止实例。例如:

sh bin/startup.sh -i <instanceName>

配置文件路径

无论 canal.auto.scan 的值是什么,Canal 实例的配置文件通常位于 conf/instance/ 目录下。每个实例对应一个 .properties 文件,文件名通常为 -instance.properties。

例如:

test-instance.properties 表示名为 test 的 Canal 实例。

示例配置

假设您有以下配置:

environment:
  - canal.auto.scan=false
  - canal.destinations=test

解释:

canal.auto.scan=false:禁用自动扫描功能。
canal.destinations=test:指定需要加载的实例名称为 test。

在这种情况下,Canal 不会自动扫描 conf/instance/ 目录,但会根据 canal.destinations 的值手动加载名为 test 的实例。

canal.auto.scan=true:Canal 启动时自动扫描并加载所有实例配置文件。
canal.auto.scan=false:禁用自动扫描功能,需通过手动方式加载实例。
根据实际需求选择合适的配置方式:
静态配置场景适合启用自动扫描。
动态管理场景适合禁用自动扫描,并通过 API 或命令行动态加载实例

posted @   H&K  阅读(14)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示