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

相关推荐
Haooog7 小时前
98.验证二叉搜索树(二叉树算法题)
java·数据结构·算法·leetcode·二叉树
武子康7 小时前
Java-143 深入浅出 MongoDB NoSQL:MongoDB、Redis、HBase、Neo4j应用场景与对比
java·数据库·redis·mongodb·性能优化·nosql·hbase
njsgcs7 小时前
sse mcp flask 开放mcp服务到内网
后端·python·flask·sse·mcp
jackaroo20208 小时前
后端_基于注解实现的请求限流
java
间彧8 小时前
Java单例模式:饿汉式与懒汉式实现详解
后端
道可到8 小时前
百度面试真题 Java 面试通关笔记 04 |JMM 与 Happens-Before并发正确性的基石(面试可复述版)
java·后端·面试
Ray668 小时前
guide-rpc-framework笔记
后端
37手游后端团队8 小时前
Claude Code Review:让AI审核更懂你的代码
人工智能·后端·ai编程
飞快的蜗牛8 小时前
利用linux系统自带的cron 定时备份数据库,不需要写代码了
java·docker
长安不见8 小时前
解锁网络性能优化利器HTTP/2C
后端