数据同步之canal搭建和php操作
mysql数据库的变更,同步到es或者其它组件里,如果要监控的表字段较多,耦合到业务代码里 需要修改的地方会特别多,或者改不全
下面简单部署和操作canalCanal 是阿里巴巴开源的一款基于 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 文件,文件名通常为
例如:
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 或命令行动态加载实例
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)