Docker 中使用 PHP 通过 Canal 同步 Mysql 数据到 ElasticSearch

一、Mysql 的安装和配置

1.使用 docker 安装 mysql,并且映射端口和 root 账号的密码

php 复制代码
# 获取镜像
docker pull mysql:8.0.40-debian

# 查看镜像是否下载成功
docker images

# 运行msyql镜像
docker run -d -p 3388:3306 --name super-mysql -e MYSQL_ROOT_PASSWORD=123456 mysql:8.0.40-debian

2.连接 mysql,检查是否正确安装

使用 Navicat 连接

使用 linux 命令行连接,172.21.121.208 是我本地映射的ip地址,这里换成对应 ip 即可

3.修改 mysql 的配置文件

php 复制代码
# 进入刚刚安装的 mysql 容器
docker exec -it super-mysql /bin/bash

# 查找 mysql 的配置文件 my.cnf
find / -name my.cnf 2>/dev/null

#显示:
# /var/lib/dpkg/alternatives/my.cnf
# /etc/alternatives/my.cnf
# /etc/mysql/my.cnf


# 修改配置文件 my.cnf 
vim /etc/mysql/my.cnf

# 开启 Binlog 写入功能,配置 binlog-format 为 ROW 模式,my.cnf 中配置如下:
[mysqld]
log-bin=mysql-bin # 开启 binlog
binlog-format=ROW # 选择 ROW 模式
server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复


# 如果没有vim,则需 先安装vim,执行如下命令
# 更新源
apt update

# 安装vim
apt install vim

4.授权 canal 链接 mysql 账号具有作为 mysql slave 的权限, 如果已有账户可直接 grant

php 复制代码
CREATE USER canal IDENTIFIED BY 'canal';  
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
-- GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' ;
FLUSH PRIVILEGES;

运行截图如下

使用刚刚新建的账号: canal 密码: canal 连接 mysql,成功了才往下配置

二、Canal 的安装和配置

1.使用 docker 安装 canal

php 复制代码
# 获取镜像
docker pull canal/canal-server:latest

# 查看镜像是否下载成功
docker images

# 运行msyql镜像
docker run --name super-canal -p 3399:11111 -d canal/canal-server:latest

2.修改 canal 配置,并且重启 canal

php 复制代码
# 进入刚刚安装的 canal 容器
docker exec -it super-canal /bin/bash

# 查找 canal 的配置文件 instance.properties
find / -name 'instance.properties' 2>/dev/null 
# 显示:
# /home/admin/canal-server/conf/example/instance.properties

# 修改配置文件 instance.properties
vi /home/admin/canal-server/conf/example/instance.properties

canal.instance.master.address=连接mysql的ip:port
canal.instance.dbUsername=连接mysql的账号
canal.instance.dbPassword=连接mysql的密码


# 重启 canal 服务
find / -name 'stop.sh' 2>/dev/null # 查找停止 canal 命令
bash /home/admin/canal-server/bin/stop.sh # 停止 canal

find / -name 'startup.sh' 2>/dev/null # 查找重启 canal 命令
bash /home/admin/canal-server/bin/startup.sh # 重启 canal

主要修改连接 msyql 的 ip 和 port,使用的账号和密码,修改配置如下:

三、使用 PHP 测试 Canal 是否监听到了 Mysql 的变化

1.初始化项目

php 复制代码
# 创建项目文件夹 canal-elasticsearch,并且进入到项目文件夹

# composer 初始化项目
composer init 
# 一直按回车键 Enter

# 安装 cannal 依赖
composer require xingwenge/canal_php

2.在 src 目录下新建文件 index.php ,写入一下代码

php 复制代码
<?php
require __DIR__.'/../vendor/autoload.php';
use xingwenge\canal_php\CanalConnectorFactory;
use xingwenge\canal_php\CanalClient;
use xingwenge\canal_php\Fmt;

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

    $client->connect("172.21.121.208", 3399);
    $client->checkValid();
    $client->subscribe("1001", "example", ".*\\..*");
    # $client->subscribe("1001", "example", "db_name.tb_name"); # 设置过滤

    while (true) {
        $message = $client->get(100);
        if ($entries = $message->getEntries()) {
            foreach ($entries as $entry) {
                Fmt::println($entry);
            }
        }
        sleep(1);
    }

    $client->disConnect();
} catch (\Exception $e) {
    echo $e->getMessage(), PHP_EOL;
}

3.命令行中启动脚本 php ./src/index.php

4.在 mysql 中新建表或者新增数据,就会在命令行中打印出来

php 复制代码
# 创建数据表
CREATE TABLE `user` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
  `age` int unsigned NOT NULL DEFAULT '0',
  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

注意:如果新增数据表或者其它情况,导致 canal 突然连接不上 mysql 了,建议停止 canal,且删除当前的 canal 容器,重新使用 docker 安装 canal,这样解决起来比较迅速(坏笑)

四、ElasticSearch 的安装和配置

1.使用 docker 安装 elasticsearch

php 复制代码
# 获取镜像
docker pull registry.cn-hangzhou.aliyuncs.com/xka/es:7.11.2-210328-1

# 查看镜像是否下载成功
docker images

# 创建数据文件夹
mkdir /mnt/d/Work/Code/docker/elasticsearch/data

# 运行 elasticsearch 镜像
docker run --name super-elasticsearch -d \
 -p 3320:9200 -p 3330:9300 \
 -e "ES_JAVA_OPTS=-Xms4g -Xmx16g" \
 -e "discovery.type=single-node" \
 -v /mnt/d/Work/Code/docker/elasticsearch/data:/usr/share/elasticsearch/data \
 registry.cn-hangzhou.aliyuncs.com/xka/es:7.11.2-210328-1

2.测试 elasticsearch 是否安装成功, 在浏览器地址栏输入:127.0.0.1:3320,出现如下画面表示安装成功了

五、使用 PHP 通过 Canal 同步 Mysql 数据到 ElasticSearch

1.在 php 中使用 composer 安装 elasticsearch 依赖包

php 复制代码
# 使用 composer 安装 elasticsearch 依赖包
composer require elasticsearch/elasticsearch

2.使用 php 在 elasticsearch 中创建 mapping,在 src 目录下创建文件 creatElasticSearchMapping.php

php 复制代码
<?php
require 'vendor/autoload.php';

use Elasticsearch\ClientBuilder;
use Elasticsearch\Common\Exceptions\BadRequest400Exception;

// 创建 Elasticsearch 客户端
$client = ClientBuilder::create()
    ->setHosts(['localhost:3320']) // 设置 Elasticsearch 主机和端口
    ->build();

// 索引名称
$indexName = 'canal_user_index';

// 索引设置和映射
$params = [
    'index' => $indexName,
    'body'  => [
        'settings' => [
            'number_of_shards' => 1, // 分片数量
            'number_of_replicas' => 0 // 副本数量
        ],
        'mappings' => [
            'properties' => [
                'name' => [
                    'type' => 'text'
                ],
                'age' => [
                    'type' => 'integer'
                ],
                'email' => [
                    'type' => 'keyword'
                ],
                'created_at' => ['type' => 'date', 'format' => 'yyyy-MM-dd HH:mm:ss'],
                'updated_at' => ['type' => 'date', 'format' => 'yyyy-MM-dd HH:mm:ss'],
            ]
        ]
    ]
];

try {
    // 创建索引
    $response = $client->indices()->create($params);
    echo "索引 '$indexName' 创建成功:\n";
    print_r($response);
} catch (BadRequest400Exception $e) {
    // 如果索引已经存在
    if (strpos($e->getMessage(), 'index_already_exists_exception') !== false) {
        echo "索引 '$indexName' 已经存在。\n";
    } else {
        echo "创建索引时发生错误: " . $e->getMessage() . "\n";
    }
} catch (\Exception $e) {
    echo "创建索引时发生错误: " . $e->getMessage() . "\n";
}

3.检查elasticsearch中的mapping是否创建成功

php 复制代码
curl -XGET 'http://localhost:3320/canal_user_index/_mapping'

提示如图

4.在 php 中通过 canal 获取 msyql 的数据变化,且更新到 elasticsearch 中,数据的增删改代码都在下面了,在 src 目录下创建文件 canalToElasticSearch.php

php 复制代码
<?php
require __DIR__.'/../vendor/autoload.php';
use Com\Alibaba\Otter\Canal\Protocol\EventType;
use Com\Alibaba\Otter\Canal\Protocol\RowChange;
use Com\Alibaba\Otter\Canal\Protocol\RowData;
use Elasticsearch\ClientBuilder;
use xingwenge\canal_php\CanalClient;
use xingwenge\canal_php\CanalConnectorFactory;
use xingwenge\canal_php\Fmt;

$clientES = ClientBuilder::create()
    ->setHosts(['localhost:3320'])
    ->setSSLVerification(false) // 禁用 SSL 验证(仅用于开发环境)
    ->setRetries(3) // 设置重试次数
    ->build();

// 索引名称
$indexName = 'canal_user_index';

try {
    $client = CanalConnectorFactory::createClient(CanalClient::TYPE_SOCKET_CLUE);
    $client->connect("172.21.121.208", 3399);
    $client->checkValid();
    $client->subscribe("1001", "example", ".*\\..*");
    echo "script start success!";

    while (true) {
        $message = $client->get(100);
        if ($entries = $message->getEntries()) {
            foreach ($entries as $entry) {
                Fmt::println($entry);

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

                /** @var RowData $rowData */
                foreach ($rowChange->getRowDatas() as $rowData) {
                    switch ($evenType) {
                        /** 删除数据 */
                        case EventType::DELETE:
                            if ($rowData->getAfterColumns()) {
                                foreach ($rowData->getAfterColumns() as $column) {
                                    if ($column->getName() === 'id') $id = $column->getValue();
                                }
                                if (!empty($id) && $clientES->exists(['index' => $indexName, 'id' => $id])) {
                                    $response = $clientES->delete(['index' => $indexName, 'type' => '_doc', 'id' => $id]);
                                }
                            }
                            break;
                        /** 新增数据 */
                        case EventType::INSERT:
                            $insertData = [];
                            if ($rowData->getAfterColumns()) {
                                foreach ($rowData->getAfterColumns() as $column) {
                                    $insertData = array_merge($insertData, [$column->getName() => $column->getValue()]);
                                    if ($column->getName() === 'id') $id = $column->getValue();
                                }
                                if (!empty($insertData)) {
                                    $params = ['index' => $indexName, 'body' => $insertData];
                                    if (!empty($id)) $params['id'] = $id;
                                    $response = $clientES->index($params);
                                }
                            }
                            break;
                        default:
                            /** 更新数据 */
                            if ($rowData->getAfterColumns()) {
                                $updateData = [];
                                foreach ($rowData->getAfterColumns() as $column) {
                                    $updateData = array_merge($updateData, [$column->getName() => $column->getValue()]);
                                    if ($column->getName() === 'id') $id = $column->getValue();
                                }

                                if (!empty($id) && !empty($updateData)) {
                                    $params = [
                                        'index' => $indexName,
                                        'id' => $id,
                                        'body' => [
                                            'doc' => $updateData
                                        ],
                                    ];
                                    if ($clientES->exists(['index' => $indexName, 'id' => $id])) {
                                        $response = $clientES->update($params);
                                    } else {
                                        $updateData['id'] = $id;
                                        $params['body'] = $updateData;
                                        $response = $clientES->index($params);
                                    }
                                }
                            }
                            break;
                    }
                }
            }
        }
        sleep(1);
    }
} catch (\Exception $e) {
    echo $e->getMessage(), PHP_EOL;
}

5.执行php脚本,监听数据变化

我这里在 mysql 中添加了一些数据,都同步到 elasticsearch 里面了

msyql 中的截图:

elasticsearch 中的截图

看到这里,辛苦了。

感觉自己今天又又又变得比昨天更强了

参考文档链接:

1.Mysql 数据库 主从数据库 (主从)(主主)-CSDN博客

2.https://github.com/xingwenge/canal-php

相关推荐
小白银子1 小时前
零基础从头教学Linux(Day 60)
linux·数据库·mysql·oracle
憋问我,我也不会2 小时前
MYSQL 命令
数据库·mysql
苗壮.2 小时前
「个人 Gitee 仓库」与「企业 Gitee 仓库」同步的几种常见方式
大数据·elasticsearch·gitee
计算机小手3 小时前
使用 llama.cpp 在本地高效运行大语言模型,支持 Docker 一键启动,兼容CPU与GPU
人工智能·经验分享·docker·语言模型·开源软件
无泡汽水3 小时前
MySQL入门练习50题
数据库·mysql
laocaibulao3 小时前
mac电脑composer命令如何指定PHP版本
macos·php·composer
岚天start3 小时前
KubeSphere在线安装单节点K8S集群
docker·容器·kubernetes·k8s·kubesphere·kubekey
栗子~~3 小时前
shell-基于k8s/docker管理容器、监控模型训练所消耗的最大CPU与最大内存脚本
docker·容器·kubernetes
半梦半醒*3 小时前
k8s——pod详解2
linux·运维·docker·容器·kubernetes·负载均衡
zz-zjx3 小时前
云原生LVS+Keepalived高可用方案(二)
开发语言·php·lvs