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

相关推荐
杨充4 分钟前
03.接口vs抽象类比较
前端·后端
一只叫煤球的猫6 分钟前
基于Redisson的高性能延迟队列架构设计与实现
java·redis·后端
卡尓7 分钟前
使用 Layui 替换 Yii 基础模板的默认 Bootstrap 样式并尝试重写导航栏组件
后端
WhyWhatHow11 分钟前
JEnv:新一代Java环境管理器,让多版本Java管理变得简单高效
java·后端
保加利亚的风23 分钟前
【Java】使用FreeMarker来实现Word自定义导出
java·word
SteveCode.27 分钟前
SpringBoot 2.x 升 3.x 避坑指南:企业级项目的实战问题与解决方案
java·spring boot
Rust语言中文社区31 分钟前
Rust 训练营二期来袭: Rust + AI 智能硬件
开发语言·后端·rust
Yang-Never37 分钟前
Kotlin -> object声明和object表达式
android·java·开发语言·kotlin·android studio
风萧萧199938 分钟前
Java 实现poi方式读取word文件内容
java·开发语言·word
喵手1 小时前
如何实现一个简单的基于Spring Boot的用户权限管理系统?
java·spring boot·后端