SpringBoot整合 Canal

475 阅读5分钟

概述:

Canal 是由阿里巴巴开源的一个基于 MySQL 数据库增量日志解析的数据同步工具,主要用于将 MySQL 的数据变更实时同步到其他中间件、数据库或系统中。Canal 模拟了 MySQL Slave 的交互协议,伪装成一个 Slave 连接到 MySQL Master 服务器,从而可以读取到 MySQL binlog 的变更数据,并解析成自己的数据结构。

Canal 的工作原理:

  1. 模拟 Slave: Canal 伪装成 MySQL Slave,向 MySQL Master 发起连接请求。

  2. 获取 Binlog: Canal 通过建立的连接,请求并获取 MySQL Master 的 Binlog。

  3. 解析 Binlog: Canal 解析 Binlog 中的数据变更信息,转换为自己的数据格式(如 Java 对象)。

  4. 数据传输: Canal 将解析后的数据以一定的格式(如 Protobuf、JSON)提供给订阅者。

  5. 数据消费: 订阅者(可以是 Canal 客户端或其他系统)消费这些数据,进行相应的数据同步或业务逻辑处理。

Canal 的优点:

  1. 实时性: 可以实现近乎实时的数据同步。

  2. 低侵入性: 不需要修改 MySQL 源码,只需要开启 MySQL 的 Binlog 日志功能。

  3. 高可用性: 支持集群部署,通过 Zookeeper 管理多个 Canal 实例,提高系统的可用性。

  4. 灵活性: 提供了简单的客户端 API,可以灵活地接入自定义的数据处理逻辑。

  5. 兼容性: 支持多种数据消费方式,如 Kafka、RocketMQ 等。

  6. 数据不丢失: 提供了数据位置标记,确保数据同步的一致性和可靠性。

Canal 的缺点:

  1. 学习成本: 对于初学者来说,理解 Canal 的工作原理和配置可能需要一定的学习成本。

  2. 依赖 MySQL 特性: 依赖于 MySQL 的 Binlog 日志,如果 MySQL 实例未开启 Binlog 日志或配置不正确,Canal 将无法工作。

  3. 性能开销: 对于写入量非常大的数据库,Canal 监听和解析 Binlog 可能会带来额外的性能开销。

  4. 异常处理: 在网络抖动或 MySQL Master 异常情况下,需要合理处理异常和重连策略,以免影响数据同步。

  5. 数据一致性: 如果 Binlog 日志在某些特定情况下丢失,可能会导致数据不一致的问题。

  6. 维护成本: 需要对 Canal 进行适当的监控和维护,以确保其稳定运行。

流程图:

graph LR  
    A[MySQL Server] -->|Binlog| B[Canal Server]  
    B -->|Parse Binlog| C{Canal Connector}  
    C -->|Get Changes| D[Canal Client]  
    D -->|Data Synchronization| E[Data Consumers]  
    E --> F[Database/Cache/Message Queue]  
    E --> G[Search Engine]  
    E --> H[Other Systems]  

"MySQL Server" 产生 Binlog。
- "Canal Server" 连接到 MySQL Server 并读取 Binlog。
- "Canal Connector" 是 Canal Server 的一部分,解析 Binlog 并提供给客户端。
- "Canal Client" 从 Canal Connector 获取变更数据。
- "Data Consumers" 是数据消费者,可能是数据库、缓存、消息队列、搜索引擎或其他系统。
- 数据消费者处理同步的数据,更新自己的数据状态。

时序图:

sequenceDiagram  
    participant MySQL  
    participant Canal Server  
    participant Canal Client  
    participant Consumer   
    MySQL->>Canal Server: Write Binlog  
    Canal Server->>Canal Server: Read Binlog  
    Canal Server->>Canal Client: Send Parsed Data  
    Canal Client->>Consumer: Apply Data Changes  
    Consumer->>Consumer: Process Data  

- "MySQL" 代表 MySQL 服务器,它负责写入 Binlog。
- "Canal Server" 代表 Canal 服务器,它从 MySQL 读取 Binlog 并将其解析成数据变更。
- "Canal Client" 代表连接到 Canal Server 的客户端,它接收来自 Canal Server 的数据变更。 - "Consumer" 代表数据消费者,它从 Canal Client 接收数据变更,并根据业务逻辑进行处理。

时序图展示了从 MySQL 服务器写入 Binlog 开始,到数据消费者处理数据变更的整个流程。

总体来说,Canal 是一个成熟的工具,适用于需要实时数据同步的场景,尤其是在构建数据仓库、实现缓存更新、搜索引擎索引更新等场合。不过,使用 Canal 时也需要考虑其对系统的影响,并做好相应的异常处理和监控策略。

Spring Boot 中整合 Canal

Spring Boot 中整合 Canal 来同步 MySQL 数据,需要按照以下步骤操作:

1. 添加 Canal 客户端依赖:

在项目的 pom.xmlbuild.gradle 文件中添加 Canal 客户端依赖。

Maven 依赖示例:

<dependency>
    <groupId>com.alibaba.otter</groupId>
    <artifactId>canal.client</artifactId>
    <version>1.1.5</version> 
</dependency>

Gradle 依赖示例:

implementation 'com.alibaba.otter:canal.client:1.1.5' // 使用最新的版本

2. 配置 Canal 连接属性:

application.propertiesapplication.yml 中添加 Canal 的配置。

# application.properties
canal.host=127.0.0.1
canal.port=11111
canal.destination=example
canal.username=
canal.password=

3. 创建 Canal 客户端配置:

创建一个配置类来配置 Canal 客户端。

@Configuration
public class CanalConfig {

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

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

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

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

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

    @Bean
    public CanalConnector canalConnector() {
        // 创建连接器
        CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress(host, port),
                destination, username, password);
        return connector;
    }
}

4. 创建 Canal 客户端监听器:

创建一个服务类来监听 Canal 服务器的变化,并处理同步逻辑。

@Component
public class CanalClient {

    @Autowired
    private CanalConnector canalConnector;

    @PostConstruct
    public void listen() {
        Executors.newSingleThreadExecutor().submit(() -> {
            canalConnector.connect();
            canalConnector.subscribe(".*\\..*");
            canalConnector.rollback();
            try {
                while (true) {
                    Message message = canalConnector.getWithoutAck(1000);
                    long batchId = message.getId();
                    int size = message.getEntries().size();
                    if (batchId != -1 && size > 0) {
                        handleEntries(message.getEntries());
                    }
                    canalConnector.ack(batchId); // 提交确认
                }
            } finally {
                canalConnector.disconnect();
            }
        });
    }

    private void handleEntries(List<CanalEntry.Entry> entries) {
        for (CanalEntry.Entry entry : entries) {
            // 根据实际业务处理逻辑
        }
    }
}

针对以上代码可以进一步优化一下:

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CanalConfig {
 // ... 同上,配置类代码
}

@Service
public class CanalClientService implements DisposableBean {

 private final CanalConnector connector;
 private final ExecutorService executorService = Executors.newSingleThreadExecutor();

 public CanalClientService(CanalConnector connector) {
     this.connector = connector;
 }

 @PostConstruct
 public void init() {
     // 使用线程池启动监听
     executorService.submit(this::process);
 }

 @Async
 public void process() {
     try {
         connector.connect();
         connector.subscribe(".*\\..*");
         while (true) {
             // 获取数据并处理
             // ... 同上,数据获取和处理代码
         }
     } catch (Exception e) {
         // 日志记录和重连逻辑
     } finally {
         connector.disconnect();
     }
 }

 @Override
 public void destroy() {
     // 优雅关闭
     connector.disconnect();
 	  executorService.shutdown();
 }

 private void handleEntry(List<CanalEntry.Entry> entries) {
     // 数据处理逻辑解耦到单独的服务或组件
     // ... 同上,数据处理代码
 }
}

使用了 ExecutorService 来异步执行 Canal 客户端监听工作,并在 destroy 方法中进行了资源的清理。同时,将配置信息外部化,并预留了异常处理和重连的位置。

5. 处理 Canal 监听到的数据变更:

handleEntries 方法中,需要解析来自 Canal 的 Entry 对象,并根据业务需求同步数据到应用程序或另一个数据库。

private void handleEntries(List<CanalEntry.Entry> entries) {
    for (CanalEntry.Entry entry : entries) {
        if (entry.getEntryType() == CanalEntry.EntryType.ROWDATA) {
            CanalEntry.RowChange rowChange = null;
            try {
                rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
            } catch (Exception e) {
                throw new RuntimeException("ERROR ## parser of eromanga-event has an error, data:" + entry.toString(),
                        e);
            }

            for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
                if (rowChange.getEventType() == CanalEntry.EventType.DELETE) {
                    // 处理删除逻辑
                } else if (rowChange.getEventType() == CanalEntry.EventType.INSERT) {
 			        // 处理插入逻辑
                } else {
                    // 处理更新逻辑
                }
            }
        }
    }
}

确保 MySQL 服务器和 Canal 服务器已经正确配置并且运行。在 MySQL 服务器上,需要开启 binlog,并设置 binlog_format=ROW,Canal 才能捕获到数据变更。

此外,需要根据具体业务需求来实现数据同步的逻辑。这可能包括将数据写入另一个数据库、发送到消息队列、触发其他服务的操作等。