Canal 核心概念及安装与使用

Canal 是阿里巴巴开源的一款基于 MySQL 数据库增量日志(binlog)的实时数据同步工具,主要用于数据库变更的订阅与消费。它可以模拟 MySQL Slave 的交互协议,解析 binlog 事件并推送变更数据,适用于数据同步、缓存更新、实时分析等场景。

Canal 核心概念

  1. 工作原理

    • Canal 伪装成 MySQL 从库(Slave),向主库(Master)发送 dump 请求。

    • MySQL 主库收到请求后,推送 binlog 事件给 Canal。

    • Canal 解析 binlog 并转发数据变更事件(INSERT/UPDATE/DELETE)到下游消费者(如你的 Spring Boot 应用)。

  2. 核心组件

    • Server:Canal 服务端,负责连接 MySQL、解析 binlog。

    • Client:客户端,订阅 Canal 服务端的数据变更事件。

    • Instance:Canal 实例,每个实例对应一个 MySQL 数据库的同步任务。


在 Spring Boot 中使用 Canal 的步骤

1. 准备工作
  • 开启 MySQL binlog

    • 确保 MySQL 的 my.cnf 配置中启用 binlog,格式为 ROW

      [mysqld]
      log-bin=mysql-bin
      binlog-format=ROW
      server-id=1
    • 创建 Canal 专用用户并授权:

      CREATE USER 'canal'@'%' IDENTIFIED BY 'canal';
      GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
      FLUSH PRIVILEGES;
  • 部署 Canal Server

    1. 下载 Canal 服务端(Canal Release)。

    2. 解压后修改 conf/example/instance.properties

      canal.instance.master.address=127.0.0.1:3306  # MySQL地址
      canal.instance.dbUsername=canal
      canal.instance.dbPassword=canal
      canal.instance.filter.regex=.*\\..*  # 监控所有库表
    3. 启动 Canal Server:

      sh bin/startup.sh

2. Spring Boot 集成 Canal 客户端
(1) 添加依赖

Canal 官方未提供官方客户端,可使用第三方库(如 com.alibaba.otter:canal.client):

<dependency>
    <groupId>com.alibaba.otter</groupId>
    <artifactId>canal.client</artifactId>
    <version>1.1.6</version>
</dependency>
(2) 配置 Canal 客户端
@Configuration
public class CanalConfig {

    @Value("${canal.server.host}")
    private String canalHost;

    @Value("${canal.server.port}")
    private int canalPort;

    @Value("${canal.destination}")
    private String destination;

    @Bean
    public CanalConnector canalConnector() {
        // 创建 Canal 连接器(支持集群模式)
        CanalConnector connector = CanalConnectors.newClusterConnector(
            Lists.newArrayList(new InetSocketAddress(canalHost, canalPort)),
            destination, "", "");
        connector.connect();
        connector.subscribe(".*\\..*");  // 订阅所有表
        return connector;
    }
}
(3) 监听并处理数据变更
@Component
public class CanalEventListener {

    @Autowired
    private CanalConnector canalConnector;

    @PostConstruct
    public void startListening() {
        new Thread(() -> {
            while (true) {
                try {
                    Message message = canalConnector.getWithoutAck(100);  // 批量获取数据
                    long batchId = message.getId();
                    if (batchId != -1 && !message.getEntries().isEmpty()) {
                        processEntries(message.getEntries());
                    }
                    canalConnector.ack(batchId);  // 确认消费
                } catch (Exception e) {
                    canalConnector.rollback();  // 处理失败,回滚
                }
            }
        }).start();
    }

    private void processEntries(List<CanalEntry.Entry> entries) {
        for (CanalEntry.Entry entry : entries) {
            if (entry.getEntryType() == CanalEntry.EntryType.ROWDATA) {
                CanalEntry.RowChange rowChange;
                try {
                    rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
                } catch (InvalidProtocolBufferException e) {
                    throw new RuntimeException("解析数据失败", e);
                }

                CanalEntry.EventType eventType = rowChange.getEventType();
                String tableName = entry.getHeader().getTableName();
                
                for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
                    // 处理变更数据
                    if (eventType == CanalEntry.EventType.INSERT) {
                        handleInsert(rowData, tableName);
                    } else if (eventType == CanalEntry.EventType.UPDATE) {
                        handleUpdate(rowData, tableName);
                    } else if (eventType == CanalEntry.EventType.DELETE) {
                        handleDelete(rowData, tableName);
                    }
                }
            }
        }
    }

    private void handleInsert(CanalEntry.RowData rowData, String tableName) {
        List<CanalEntry.Column> columns = rowData.getAfterColumnsList();
        // 提取字段并执行业务逻辑(如更新缓存、同步到ES等)
    }

    // 类似地实现 handleUpdate 和 handleDelete
}

3. 处理数据变更的业务逻辑

根据业务需求,将变更数据同步到其他系统(如 Redis、Elasticsearch 或发送消息队列):

private void handleUpdate(CanalEntry.RowData rowData, String tableName) {
    Map<String, String> before = rowData.getBeforeColumnsList().stream()
        .collect(Collectors.toMap(CanalEntry.Column::getName, CanalEntry.Column::getValue));
    Map<String, String> after = rowData.getAfterColumnsList().stream()
        .collect(Collectors.toMap(CanalEntry.Column::getName, CanalEntry.Column::getValue));

    if ("user".equals(tableName)) {
        String userId = after.get("id");
        // 更新缓存中的用户信息
        redisTemplate.opsForValue().set("user:" + userId, after);
    }
}

注意事项

  1. 确保 MySQL binlog 配置正确,格式必须为 ROW

  2. 处理网络中断:Canal 客户端需具备重连机制。

  3. 批量消费与ACK机制:合理设置批量大小,及时确认消息(ack)或回滚(rollback)。

  4. 监控 Canal 服务:通过 Canal 提供的管理端口(默认 11111)监控同步状态。

通过以上步骤,你可以在 Spring Boot 项目中实现基于 Canal 的实时数据同步。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Leaton Lee

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值