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

相关推荐
huaweichenai12 分钟前
PHP实现华为OBS存储
开发语言·php
apgk122 分钟前
docker 安装mysql 5.7 详细保姆级教程
mysql·docker·容器
° 安如少年初如梦66225 分钟前
【数据结构】数据库索引(B树,B+树)简介
数据结构·数据库·mysql
二狗的编程之路32 分钟前
Python中的正则表达式
python·mysql·正则表达式
景未白1 小时前
如何保证数据库和缓存双写一致性?
redis·mysql
未来并未来2 小时前
深入解析MySQL事务隔离级别与锁机制在银行账户业务中的应用
数据库·mysql
sun_qqq4 小时前
Java从入门到工作4 - MySQL
java·开发语言·mysql
stormsha4 小时前
Docker 更换 MySQL 镜像:备份、迁移与恢复数据的详细流程
linux·mysql·docker·容器
是阿花y4 小时前
Centos7和9安装mysql5.7和mysql8.0详细教程(超详细)
linux·运维·mysql·云计算