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

相关推荐
撩得Android一次心动2 分钟前
Android 四大组件——BroadcastReceiver(广播)
android·java·android 四大组件
canonical_entropy6 分钟前
Nop平台到底有什么独特之处,它能用在什么场景?
java·后端·领域驱动设计
chilavert3188 分钟前
技术演进中的开发沉思-174 java-EJB:分布式通信
java·分布式
不是株33 分钟前
JavaWeb(后端进阶)
java·开发语言·后端
IT_陈寒35 分钟前
5个Python 3.12新特性让你的代码效率提升50%,第3个太实用了!
前端·人工智能·后端
Victor3561 小时前
Redis(109)Redis的Pipeline如何使用?
后端
NPE~1 小时前
[手写系列]Go手写db — — 第七版(实现Disk存储引擎、Docker化支持)
数据库·后端·docker·golang·教程·手写数据库
Victor3561 小时前
Redis(108)Redis的事务机制如何实现?
后端
JaguarJack1 小时前
PHP 开发中 你可能不知道的非常好用 PhpStorm 插件
后端·php