使用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 内。

相关推荐
调试人生的显微镜19 分钟前
iOS App性能测试工具全解析:开发者必备的实战工具指南
后端
安思派Anspire20 分钟前
LangGraph + MCP + Ollama:构建强大代理 AI 的关键(二)
人工智能·后端·python
找不到、了28 分钟前
分布式理论:CAP、Base理论
java·分布式
天天摸鱼的java工程师31 分钟前
2025已过半,Java就业大环境究竟咋样了?
java·后端
人生在勤,不索何获-白大侠35 分钟前
day16——Java集合进阶(Collection、List、Set)
java·开发语言
货拉拉技术36 分钟前
OceanBase向量检索在货拉拉的探索和实践
后端
Zedthm42 分钟前
LeetCode1004. 最大连续1的个数 III
java·算法·leetcode
艺杯羹1 小时前
MyBatis之核心对象与工作流程及SqlSession操作
java·mybatis
转转技术团队1 小时前
多代理混战?用 PAC(Proxy Auto-Config) 优雅切换代理场景
前端·后端·面试
南囝coding1 小时前
这几个 Vibe Coding 经验,真的建议学!
前端·后端