使用Canal实现MySQL到Elasticsearch数据同步

一、Canal 核心原理详解

Canal 是阿里巴巴开源的数据同步工具,基于 MySQL 数据库的 binlog 增量订阅&消费 机制实现数据同步。其核心原理如下:

1. MySQL Binlog 机制

  • Binlog 作用:MySQL 的二进制日志,记录所有数据变更(INSERT/UPDATE/DELETE)
  • 三种格式
    • Statement:记录 SQL 语句
    • Row:记录行级变更(Canal 使用此格式
    • Mixed:混合模式
  • Row 格式优势:包含变更前后的完整数据行,无 SQL 解析歧义

2. Canal 工作原理

graph LR A[MySQL Master] -->|推送 binlog| B[Canal Server] B -->|解析 binlog| C[Canal Client] C --> D[Elasticsearch]
  1. 伪装 Slave
    • Canal 启动后伪装成 MySQL Slave
    • 向 Master 发送 dump 协议请求 binlog
  2. Binlog 接收
    • MySQL Master 收到请求后推送 binlog
  3. 协议解析
    • Canal 解析 binlog 原始字节流
    • 转换为结构化 Entry 对象(含 Table/Schema 信息)
  4. 事件转换
    • Entry 转换为可消费的 Message 对象
  5. 数据投递
    • 客户端订阅并处理消息(写入 ES)

3. 关键组件

组件 功能说明
EventParser 从数据源抓取并解析 binlog
EventSink 数据过滤&加工(如库表路由)
EventStore 数据存储(内存/PG)
MetaManager 元数据管理(同步点位持久化)

二、数据同步全流程实现

阶段 1:MySQL 准备(必须配置)

sql 复制代码
-- 检查 binlog 配置
SHOW VARIABLES LIKE 'log_bin';  -- 必须 ON
SHOW VARIABLES LIKE 'binlog_format'; -- 必须 ROW

-- 创建 Canal 专用账号
CREATE USER 'canal'@'%' IDENTIFIED BY 'canal_password';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;

阶段 2:Canal Server 部署

配置示例 (conf/example/instance.properties)

properties 复制代码
# MySQL 连接配置
canal.instance.master.address=127.0.0.1:3306
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal_password
canal.instance.connectionCharset=UTF-8

# 过滤规则(同步哪些数据)
canal.instance.filter.regex=.*\\..*  # 所有库表
# 或指定商城表:mall_db\\.product,mall_db\\.sku

# Binlog 位置存储策略(默认内存)
canal.instance.tsdb.enable=true  # 启用持久化存储

阶段 3:首次全量同步方案

sequenceDiagram participant A as 应用服务 participant C as Canal participant M as MySQL participant E as ES A->>C: 触发全量同步指令 C->>M: 记录当前 binlog 位置 POS-1 C->>M: SELECT * FROM products loop 分批读取 M-->>C: 批量数据 C->>E: Bulk 导入 ES end C->>C: 保存 POS-1 到元数据 Note right of C: 增量阶段从 POS-1 开始

关键实现要点

  1. 使用 DataSource 直连 MySQL

    java 复制代码
    try (Connection conn = dataSource.getConnection();
         Statement stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, 
                                              ResultSet.CONCUR_READ_ONLY)) {
         stmt.setFetchSize(5000); // 分批获取
         ResultSet rs = stmt.executeQuery("SELECT id,name,price FROM products");
         while (rs.next()) {
             // 构建 ES 文档
         }
    }
  2. ES 批量写入

    java 复制代码
    BulkRequest bulkRequest = new BulkRequest();
    for (Product product : productList) {
        IndexRequest request = new IndexRequest("products")
            .id(product.getId().toString())
            .source(JSON.toJSONString(product), XContentType.JSON);
        bulkRequest.add(request);
    }
    restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);

阶段 4:增量同步流程

graph TB S[Canal 捕获 binlog] --> P{事件类型} P -->|INSERT| I[生成 ES IndexRequest] P -->|UPDATE| U[生成 ES UpdateRequest] P -->|DELETE| D[生成 ES DeleteRequest] I --> B[ES Bulk 提交] U --> B D --> B

Canal Client 核心代码

java 复制代码
CanalConnector connector = CanalConnectors.newClusterConnector(
        "192.168.1.100:11111", "example", "", "");

connector.connect();
connector.subscribe("mall_db\\..*"); // 订阅商城库

while (running) {
    Message message = connector.getWithoutAck(100); // 批量获取
    List<CanalEntry.Entry> entries = message.getEntries();
    for (CanalEntry.Entry entry : entries) {
        if (entry.getEntryType() == EntryType.ROWDATA) {
            RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
            for (RowData rowData : rowChange.getRowDatasList()) {
                processData(rowData, entry.getHeader().getTableName());
            }
        }
    }
    connector.ack(message.getId()); // 确认消费
}

ES 文档处理逻辑

java 复制代码
void processData(RowData rowData, String tableName) {
    if ("products".equals(tableName)) {
        if (eventType == INSERT || eventType == UPDATE) {
            Map<String, String> afterColumns = rowData.getAfterColumnsList().stream()
                .collect(Collectors.toMap(Column::getName, Column::getValue));
            
            IndexRequest request = new IndexRequest("products")
                .id(afterColumns.get("id"))
                .source(afterColumns);
            esClient.index(request); // 实际用 bulk
            
        } else if (eventType == DELETE) {
            Map<String, String> beforeColumns = rowData.getBeforeColumnsList().stream()
                .collect(Collectors.toMap(Column::getName, Column::getValue));
            
            DeleteRequest request = new DeleteRequest("products")
                .id(beforeColumns.get("id"));
            esClient.delete(request);
        }
    }
}

三、商品搜索系统整合

1. Elasticsearch 索引优化

json 复制代码
PUT /products
{
  "settings": {
    "analysis": {
      "analyzer": {
        "ik_smart_pinyin": { 
          "type": "custom",
          "tokenizer": "ik_smart",
          "filter": ["pinyin_filter"]
        }
      },
      "filter": {
        "pinyin_filter": {
          "type": "pinyin",
          "keep_first_letter": true
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "ik_smart_pinyin",  // 中文+拼音混合搜索
        "fields": {
          "keyword": {"type": "keyword"}
        }
      },
      "price": {"type": "double"},
      "category_id": {"type": "integer"},
      "create_time": {"type": "date"}
    }
  }
}

2. 搜索接口实现

java 复制代码
public SearchResponse searchProducts(String keyword, Double minPrice, Double maxPrice) {
    SearchRequest searchRequest = new SearchRequest("products");
    
    // 构建布尔查询
    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
    
    // 关键词查询(名称+分类+品牌)
    if (StringUtils.isNotBlank(keyword)) {
        MultiMatchQueryBuilder keywordQuery = QueryBuilders.multiMatchQuery(keyword)
            .field("name", 10)    // 权重提升
            .field("category_name")
            .field("brand_name");
        boolQuery.must(keywordQuery);
    }
    
    // 价格区间过滤
    if (minPrice != null || maxPrice != null) {
        RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("price");
        if (minPrice != null) rangeQuery.gte(minPrice);
        if (maxPrice != null) rangeQuery.lte(maxPrice);
        boolQuery.filter(rangeQuery);
    }
    
    // 分页排序
    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder()
        .query(boolQuery)
        .from(0).size(10)
        .sort("_score", SortOrder.DESC) // 相关性
        .sort("create_time", SortOrder.DESC); // 新品优先

    return restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
}

四、生产环境注意事项

  1. 性能优化

    • ES 批量写入:建议每批次 500-1000 个文档
    • Canal 内存管理:调整 canal.instance.memory.buffer.size(默认 16MB)
    • 网络压缩:启用 canal.instance.network.compression = SNAPPY
  2. 高可用方案

    graph LR M[MySQL Master] -->|binlog| C1[Canal Server 1] M -->|binlog| C2[Canal Server 2] C1 --> K[Kafka] C2 --> K K --> E[ES Consumer Group]
  3. 数据一致性保障

    • 全量同步时暂停增量(记录 binlog position)
    • 使用 ES 版本号冲突检测(version_type=external
    • 定期校验 MySQL 与 ES 数据差异
  4. 监控指标

    • Canal:解析延迟(canal_parse_time
    • ES:索引速率(indexing_pressure
    • MySQL:Binlog 堆积量(binlog_size

故障恢复流程

  1. 检查 Canal 元数据中的 binlog position
  2. 对比 MySQL SHOW MASTER STATUS
  3. 若存在差异,回退 position 重新同步
  4. 触发指定时间段的数据补偿

通过以上设计,可实现毫秒级搜索数据同步,支持每日千万级商品更新场景,查询响应时间控制在 50ms 内。

相关推荐
9523621 分钟前
SpringBoot统一功能处理
java·spring boot·后端
Lyyaoo.24 分钟前
优惠券秒杀业务分析
java·开发语言
消失的旧时光-194325 分钟前
统一并发模型:线程、Reactor、协程本质是一件事(从线程到协程 · 第6篇·终章)
java·python·算法
勿忘初心122128 分钟前
Java 国密 SM4 加密工具类实战(Hutool + BouncyCastle)|企业级数据加密 + 兼容 JDK8
java·数据安全·数据加密·后端开发·企业级开发·国密 sm4
庞轩px32 分钟前
第8篇:原子类与CAS底层原理——无锁并发的实现
java·cas·乐观锁·aba·无锁编程·自旋
rleS IONS42 分钟前
SpringBoot中自定义Starter
java·spring boot·后端
DevilSeagull1 小时前
MySQL(2) 客户端工具和建库
开发语言·数据库·后端·mysql·服务
苍煜1 小时前
慢SQL优化实战教学
java·数据库·sql
AI进化营-智能译站1 小时前
ROS2 C++开发系列16-智能指针管理传感器句柄|告别ROS2节点内存泄漏与野指针
java·c++·算法·ai
TeDi TIVE2 小时前
springboot和springframework版本依赖关系
java·spring boot·后端