MySQL与ES高效同步

在分布式架构中,MySQL凭借其强大的事务支持和数据一致性保障,成为核心业务数据存储的首选;而Elasticsearch(ES)则以卓越的全文检索能力和高并发查询性能,成为复杂检索场景的标配。两者的协同配合,能完美解决"数据存储可靠"与"查询高效灵活"的双重需求。但如何实现两者间的数据高效同步,是架构设计中绕不开的关键问题。本文将详细拆解6种主流同步方案,结合实战代码、适用场景与优化技巧,帮助开发者快速选型并避开常见陷阱。

一、同步双写:实时性优先的极简方案

同步双写是最直接的同步方式,在业务代码中同时完成MySQL写入和ES写入,确保数据实时一致。

核心原理

在同一个事务中,先将数据写入MySQL,成功后再写入ES,两者均成功则事务提交,任一失败则事务回滚。

实战代码

java 复制代码
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.common.xcontent.XContentType;
import com.alibaba.fastjson.JSON;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private RestHighLevelClient esClient;

    // 事务注解保证MySQL与ES写入原子性
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(Order order) {
        // 1. 写入MySQL主库
        orderMapper.insert(order);
        System.out.println("MySQL写入成功,订单ID:" + order.getId());

        // 2. 同步写入ES
        try {
            IndexRequest request = new IndexRequest("orders") // ES索引名
                    .id(order.getId().toString()) // 以订单ID作为ES文档ID,保证唯一性
                    .source(JSON.toJSONString(order), XContentType.JSON);
            esClient.index(request, RequestOptions.DEFAULT);
            System.out.println("ES同步写入成功,文档ID:" + order.getId());
        } catch (Exception e) {
            // 捕获ES写入异常,触发事务回滚
            throw new RuntimeException("ES同步写入失败,事务回滚", e);
        }
    }
}

适用场景

  • 对数据实时性要求极高(秒级同步)的场景,如金融交易记录、支付结果同步。
  • 业务逻辑简单、写操作频次较低的小型单体项目。

优缺点分析

优点 缺点
实现简单,无额外中间件依赖 代码侵入性强,所有写操作均需添加ES逻辑
数据一致性高,事务保证原子性 性能瓶颈明显,TPS下降30%以上
同步延迟极低(秒级) 容错性差,ES宕机将导致主业务中断

进阶优化:补偿机制实现

针对ES写入失败的问题,可引入"本地事务表+定时重试"的补偿方案:

java 复制代码
// 1. 定义事务补偿表实体
@Data
public class Sync补偿Table {
    private Long id;
    private String businessType; // 业务类型:ORDER、PRODUCT等
    private Long businessId; // 业务ID(如订单ID)
    private String data; // 待同步的JSON数据
    private Integer status; // 状态:0-待同步,1-已同步,2-同步失败
    private Date createTime;
    private Date updateTime;
}

// 2. 定时任务重试失败数据
@Scheduled(cron = "0 */1 * * * ?") // 每分钟执行一次
public void retrySyncEs() {
    // 查询30分钟内同步失败的数据
    List<Sync补偿Table> failList = sync补偿Mapper.selectByStatusAndTime(2, 30);
    for (Sync补偿Table item : failList) {
        try {
            IndexRequest request = new IndexRequest(item.getBusinessType().toLowerCase())
                    .id(item.getBusinessId().toString())
                    .source(item.getData(), XContentType.JSON);
            esClient.index(request, RequestOptions.DEFAULT);
            // 同步成功后更新状态
            sync补偿Mapper.updateStatus(item.getId(), 1);
        } catch (Exception e) {
            // 重试3次后标记为永久失败
            sync补偿Mapper.incrementRetryCount(item.getId());
        }
    }
}

二、异步双写:基于MQ的高可用方案

异步双写通过消息队列(MQ)解耦MySQL与ES的写入流程,MySQL写入成功后发送消息,由消费者异步处理ES同步,避免主业务受影响。

核心原理

  1. 生产者:业务系统完成MySQL写入后,向MQ发送消息(携带业务ID或完整数据)。
  2. 消费者:监听MQ主题,接收消息后查询MySQL获取完整数据(或直接使用消息体),写入ES。
  3. 依赖MQ的可靠性保证(持久化、重试)确保数据不丢失。

实战代码

1. 生产者端(MySQL写入后发送消息)
java 复制代码
@Service
public class ProductService {

    @Autowired
    private ProductMapper productMapper;

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    public void updateProduct(Product product) {
        // 1. 写入MySQL
        productMapper.updateById(product);
        System.out.println("MySQL更新成功,商品ID:" + product.getId());

        // 2. 发送消息到Kafka(携带商品ID)
        String message = product.getId().toString();
        // 指定分区键为商品ID,保证同一商品的消息顺序消费
        kafkaTemplate.send("product-update-topic", product.getId().toString(), message);
        System.out.println("消息发送成功,商品ID:" + product.getId());
    }
}
2. 消费者端(监听消息同步ES)
java 复制代码
@Service
public class EsSyncConsumer {

    @Autowired
    private ProductMapper productMapper;

    @Autowired
    private RestHighLevelClient esClient;

    // 监听Kafka主题
    @KafkaListener(topics = "product-update-topic", groupId = "es-sync-group")
    public void syncToEs(String productId) {
        try {
            // 1. 查询MySQL获取最新数据
            Product product = productMapper.selectById(productId);
            if (product == null) {
                System.out.println("商品不存在,ID:" + productId);
                return;
            }

            // 2. 同步写入ES
            IndexRequest request = new IndexRequest("products")
                    .id(productId)
                    .source(JSON.toJSONString(product), XContentType.JSON);
            esClient.index(request, RequestOptions.DEFAULT);
            System.out.println("ES同步成功,商品ID:" + productId);
        } catch (Exception e) {
            // 抛出异常触发Kafka重试机制
            throw new RuntimeException("ES同步失败,商品ID:" + productId, e);
        }
    }
}
3. Kafka配置(保证顺序性与可靠性)
yaml 复制代码
spring:
  kafka:
    bootstrap-servers: 127.0.0.1:9092
    producer:
      retries: 3 # 生产者重试次数
      acks: all # 等待所有副本确认
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
    consumer:
      group-id: es-sync-group
      auto-offset-reset: earliest
      enable-auto-commit: false # 关闭自动提交偏移量
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
    listener:
      ack-mode: manual_immediate # 手动提交偏移量

适用场景

  • 电商订单状态更新、商品信息同步等中等并发场景。
  • 允许秒级延迟,要求主业务高可用的分布式系统。

优缺点分析

优点 缺点
解耦主业务与ES同步,故障隔离 存在消息堆积风险,突发流量可能导致延迟
吞吐量高,可承载万级QPS 需处理消息顺序性问题
主业务性能不受ES状态影响 数据一致性为最终一致,需处理重复消费

进阶优化:消息堆积监控

通过Kafka AdminClient监控消息Lag值(消费者未消费的消息数),及时预警:

java 复制代码
@Component
public class KafkaLagMonitor {

    @Autowired
    private KafkaAdmin kafkaAdmin;

    @Scheduled(cron = "0 */5 * * * ?") // 每5分钟监控一次
    public void monitorLag() {
        AdminClient adminClient = AdminClient.create(kafkaAdmin.getConfigurationProperties());
        String topic = "product-update-topic";
        String groupId = "es-sync-group";

        try {
            // 获取消费组偏移量
            ListConsumerGroupOffsetsResult result = adminClient.listConsumerGroupOffsets(groupId);
            Map<TopicPartition, OffsetAndMetadata> offsets = result.partitionsToOffsetAndMetadata().get();

            // 获取主题分区的最新偏移量
            Map<TopicPartition, Long> endOffsets = adminClient.endOffsets(offsets.keySet()).get();

            // 计算每个分区的Lag值
            for (Map.Entry<TopicPartition, OffsetAndMetadata> entry : offsets.entrySet()) {
                TopicPartition tp = entry.getKey();
                long consumerOffset = entry.getValue().offset();
                long endOffset = endOffsets.get(tp);
                long lag = endOffset - consumerOffset;

                System.out.println("分区:" + tp.partition() + ",Lag值:" + lag);
                if (lag > 1000) { // 阈值可根据业务调整
                    // 发送预警通知(如钉钉、邮件)
                    sendAlert("Kafka消息堆积预警", "主题:" + topic + ",分区:" + tp.partition() + ",Lag值:" + lag);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            adminClient.close();
        }
    }

    private void sendAlert(String title, String content) {
        // 实现预警通知逻辑
    }
}

三、Logstash定时拉取:零侵入的离线同步方案

Logstash是ELK栈的核心组件,通过JDBC插件定时从MySQL拉取数据并写入ES,无需修改业务代码。

核心原理

  1. 配置Logstash的JDBC输入插件,指定MySQL连接信息、查询SQL和执行周期。
  2. 利用sql_last_value参数记录上次同步的时间戳,实现增量拉取。
  3. 通过Elasticsearch输出插件将数据写入ES。

实战配置

1. 完整配置文件(mysql-es-sync.conf)
ruby 复制代码
# 输入插件:从MySQL拉取数据
input {
  jdbc {
    # MySQL驱动路径(需提前下载mysql-connector-java.jar放置到指定目录)
    jdbc_driver_library => "/usr/local/logstash/lib/mysql-connector-java-8.0.30.jar"
    jdbc_driver_class => "com.mysql.cj.jdbc.Driver"
    # MySQL连接地址(替换为实际地址)
    jdbc_connection_string => "jdbc:mysql://127.0.0.1:3306/user_db?useSSL=false&serverTimezone=UTC"
    jdbc_user => "root"
    jdbc_password => "123456"
    # 连接池配置
    jdbc_pool_timeout => 5000
    # 定时执行周期(Cron表达式,每5分钟执行一次)
    schedule => "*/5 * * * *"
    # 增量查询SQL(sql_last_value为内置变量,记录上次同步的update_time)
    statement => "SELECT id, username, action, create_time, update_time FROM user_log WHERE update_time > :sql_last_value ORDER BY update_time ASC"
    # 记录上次同步位置的文件路径
    last_run_metadata_path => "/usr/local/logstash/last_run/user_log_sync"
    # 强制转换字段类型(可选)
    type_conversion => "true"
    # 启用分页查询(避免单次拉取数据过多)
    jdbc_paging_enabled => "true"
    jdbc_page_size => "1000"
  }
}

# 过滤插件:数据清洗(可选)
filter {
  mutate {
    # 移除无用字段
    remove_field => ["@version", "@timestamp"]
    # 重命名字段(如需)
    rename => {"create_time" => "log_create_time"}
  }
}

# 输出插件:写入ES
output {
  elasticsearch {
    # ES集群地址(多个地址用逗号分隔)
    hosts => ["127.0.0.1:9200"]
    # ES索引名(可按日期分区,如user_logs-%{+YYYY.MM.dd})
    index => "user_logs"
    # 文档ID(使用MySQL的id字段)
    document_id => "%{id}"
    # ES认证信息(如需)
    user => "elastic"
    password => "changeme"
  }

  # 日志输出到控制台(调试用)
  stdout {
    codec => rubydebug
  }
}
2. 启动命令
bash 复制代码
# 进入Logstash安装目录
cd /usr/local/logstash
# 指定配置文件启动
bin/logstash -f config/mysql-es-sync.conf

适用场景

  • 离线分析场景,如用户行为日志T+1统计、报表生成。
  • 历史数据迁移,无需改造业务代码。
  • 对同步延迟要求不高(分钟级)的场景。

优缺点分析

优点 缺点
零代码侵入,无需修改业务系统 同步延迟高(分钟级),无法满足实时需求
配置简单,维护成本低 增量查询依赖update_time字段,需确保索引优化
支持批量拉取,适合大数据量迁移 高并发场景下可能给MySQL带来查询压力

进阶优化:避免全表扫描

  1. update_time字段建立索引,优化增量查询性能:
sql 复制代码
ALTER TABLE user_log ADD INDEX idx_update_time (update_time);
  1. 分时段拉取大表数据,避免单次查询数据量过大:
ruby 复制代码
statement => "SELECT id, username, action, create_time, update_time FROM user_log WHERE update_time > :sql_last_value AND update_time < DATE_ADD(:sql_last_value, INTERVAL 5 MINUTE) ORDER BY update_time ASC"

四、Canal监听Binlog:高实时低侵入的生产级方案

Canal是阿里巴巴开源的数据库binlog解析工具,可模拟MySQL从库接收binlog日志,解析后同步至ES,是高并发生产环境的首选方案。

核心原理

  1. MySQL开启binlog日志(row格式),Canal伪装成MySQL从库,订阅binlog日志。
  2. Canal解析binlog日志,提取数据变更(增删改),发送至消息队列(如RocketMQ/Kafka)。
  3. 消费者监听消息队列,将变更数据同步至ES。
  4. 整个流程无侵入,同步延迟可达毫秒级。

架构流程

复制代码
MySQL(主库)→ Binlog日志 → Canal(解析)→ RocketMQ/Kafka → 消费者 → ES

实战配置与代码

1. MySQL配置(开启binlog)

编辑MySQL配置文件my.cnf(或my.ini):

ini 复制代码
[mysqld]
# 开启binlog
log-bin=mysql-bin
# 选择row模式(Canal仅支持row模式)
binlog-format=ROW
# 服务器ID(唯一,不能与Canal重复)
server-id=1
# 记录所有数据库的binlog(或指定数据库)
binlog-do-db=business_db

重启MySQL使配置生效,并创建Canal专用账号:

sql 复制代码
-- 创建账号
CREATE USER 'canal'@'%' IDENTIFIED BY 'canal123';
-- 授权
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
-- 刷新权限
FLUSH PRIVILEGES;
2. Canal配置
(1)canal.properties(全局配置)
properties 复制代码
#  Canal服务端口
canal.port = 11111
#  Canal实例配置目录(每个实例对应一个MySQL数据库)
canal.destinations = example
#  消息队列类型(这里使用RocketMQ)
canal.serverMode = rocketmq
#  RocketMQ地址
rocketmq.namesrv.addr = 127.0.0.1:9876
#  RocketMQ主题
rocketmq.topic = canal_es_sync
#  分区数(与ES分片数对齐)
rocketmq.partitionsNum = 3
(2)instance.properties(实例配置,路径:conf/example/instance.properties)
properties 复制代码
# MySQL主库地址
canal.instance.master.address = 127.0.0.1:3306
# MySQL用户名密码
canal.instance.dbUsername = canal
canal.instance.dbPassword = canal123
# 订阅的数据库和表(格式:dbName.tableName,多个用逗号分隔)
canal.instance.filter.regex = business_db\\..*
# binlog起始位置(初次启动可留空,自动从最新位置开始)
canal.instance.master.journal.name = 
canal.instance.master.position = 
# 解析模式(row模式)
canal.instance.parser.parallel = true
3. 启动Canal
bash 复制代码
# 进入Canal安装目录
cd /usr/local/canal
# 启动服务
bin/startup.sh
4. 消费者代码(RocketMQ消费并同步ES)
java 复制代码
@Service
public class CanalEsSyncConsumer {

    @Autowired
    private RestHighLevelClient esClient;

    @Autowired
    private SchemaRegistry schemaRegistry; // 自定义Schema管理组件

    @RocketMQMessageListener(
            topic = "canal_es_sync",
            consumerGroup = "canal_es_sync_group",
            messageModel = MessageModel.CLUSTERING
    )
    public class SyncConsumer implements RocketMQListener<CanalMessage> {

        @Override
        public void onMessage(CanalMessage message) {
            List<CanalData> dataList = message.getData();
            String tableName = message.getTable();
            String eventType = message.getEventType(); // INSERT/UPDATE/DELETE

            // 获取表结构映射(表名→ES索引名、字段映射)
            TableSchema schema = schemaRegistry.getSchema(tableName);
            String indexName = schema.getIndexName();

            for (CanalData data : dataList) {
                Map<String, Object> dataMap = data.getAfter(); // 变更后的数据
                String id = dataMap.get("id").toString(); // 主键ID

                try {
                    switch (eventType) {
                        case "INSERT":
                        case "UPDATE":
                            // 写入或更新ES
                            IndexRequest request = new IndexRequest(indexName)
                                    .id(id)
                                    .source(dataMap);
                            esClient.index(request, RequestOptions.DEFAULT);
                            break;
                        case "DELETE":
                            // 删除ES文档
                            DeleteRequest request = new DeleteRequest(indexName, id);
                            esClient.delete(request, RequestOptions.DEFAULT);
                            break;
                    }
                    System.out.println("ES同步成功,表名:" + tableName + ",ID:" + id + ",操作类型:" + eventType);
                } catch (Exception e) {
                    throw new RuntimeException("ES同步失败,表名:" + tableName + ",ID:" + id, e);
                }
            }
        }
    }
}

适用场景

  • 高并发生产环境,如社交平台动态实时搜索、电商商品实时检索。
  • 对同步实时性要求高(毫秒级)、无代码侵入需求的场景。

优缺点分析

优点 缺点
同步延迟低(毫秒级),实时性强 部署维护复杂,需运维中间件(Canal+MQ)
零代码侵入,不影响主业务 需处理DDL变更、数据漂移等问题
基于binlog增量同步,性能优异 依赖MySQL binlog配置,需确保格式正确

避坑指南

  1. 数据漂移处理:通过Schema Registry管理表结构与ES索引的映射关系,当MySQL表发生DDL变更(如新增字段)时,同步更新ES索引映射:
java 复制代码
// SchemaRegistry核心逻辑
public class SchemaRegistry {
    // 缓存表结构映射
    private Map<String, TableSchema> schemaCache = new ConcurrentHashMap<>();

    public TableSchema getSchema(String tableName) {
        if (schemaCache.containsKey(tableName)) {
            return schemaCache.get(tableName);
        }
        // 从数据库查询表结构(或从配置中心获取)
        TableSchema schema = loadSchemaFromDB(tableName);
        // 同步更新ES索引映射
        syncEsMapping(schema);
        schemaCache.put(tableName, schema);
        return schema;
    }

    private void syncEsMapping(TableSchema schema) {
        // 构建ES索引映射
        Map<String, Object> mapping = new HashMap<>();
        Map<String, Object> properties = new HashMap<>();
        for (Field field : schema.getFields()) {
            Map<String, Object> fieldProps = new HashMap<>();
            fieldProps.put("type", getEsFieldType(field.getDbType()));
            properties.put(field.getFieldName(), fieldProps);
        }
        mapping.put("properties", properties);

        // 创建或更新ES索引映射
        PutMappingRequest request = new PutMappingRequest(schema.getIndexName());
        request.source(mapping);
        try {
            esClient.indices().putMapping(request, RequestOptions.DEFAULT);
        } catch (Exception e) {
            throw new RuntimeException("同步ES映射失败,索引名:" + schema.getIndexName(), e);
        }
    }
}
  1. 幂等消费:利用ES文档ID的唯一性,重复消费时会覆盖原有数据,避免重复写入。对于DELETE操作,可通过判断文档是否存在来避免误删。

五、DataX批量同步:大数据量迁移的首选方案

DataX是阿里巴巴开源的离线数据同步工具,支持多种数据源之间的批量数据迁移,尤其适合MySQL分库分表到ES的历史数据迁移场景。

核心原理

  1. DataX通过Reader插件读取MySQL数据(支持分库分表、并行读取)。
  2. 经过数据转换(Transformer)后,通过Writer插件批量写入ES。
  3. 支持流量控制、数据校验、故障重试等机制,保证大数据量迁移的稳定性。

实战配置

1. 完整配置文件(mysql2es.json)
json 复制代码
{
  "job": {
    "setting": {
      "speed": {
        "channel": 3, // 并发通道数(建议与ES分片数一致)
        "byte": 1048576 // 每秒流量限制(1MB)
      },
      "errorLimit": {
        "record": 0, // 允许错误记录数(0表示不允许错误)
        "percentage": 0.02 // 允许错误比例
      }
    },
    "content": [
      {
        "reader": {
          "name": "mysqlreader", // MySQL读取插件
          "parameter": {
            "username": "root",
            "password": "123456",
            "connection": [
              {
                "querySql": [
                  "SELECT id, order_no, user_id, amount, status, create_time FROM orders WHERE create_time >= '2023-01-01' AND create_time < '2024-01-01'"
                ],
                "jdbcUrl": [
                  "jdbc:mysql://127.0.0.1:3306/order_db_1?useSSL=false",
                  "jdbc:mysql://127.0.0.1:3306/order_db_2?useSSL=false" // 分库分表支持
                ]
              }
            ],
            "splitPk": "id", // 分片字段(用于并行读取)
            "fetchSize": 1000 // 每次查询获取的记录数
          }
        },
        "writer": {
          "name": "elasticsearchwriter", // ES写入插件
          "parameter": {
            "endpoint": "http://127.0.0.1:9200",
            "index": "orders_historical", // ES索引名
            "documentType": "_doc", // ES7+已废弃,填写_doc即可
            "batchSize": 5000, // 批量写入大小
            "username": "elastic",
            "password": "changeme",
            "column": [
              {
                "name": "id",
                "type": "keyword" // ES字段类型
              },
              {
                "name": "order_no",
                "type": "keyword"
              },
              {
                "name": "user_id",
                "type": "long"
              },
              {
                "name": "amount",
                "type": "double"
              },
              {
                "name": "status",
                "type": "integer"
              },
              {
                "name": "create_time",
                "type": "date",
                "format": "yyyy-MM-dd HH:mm:ss" // 日期格式
              }
            ],
            "writeMode": "upsert", // 写入模式:insert/upsert/update
            "cleanup": true, // 任务结束后清理临时文件
            "timeout": 30000 // 超时时间(30秒)
          }
        }
      }
    ]
  }
}
2. 执行命令
bash 复制代码
# 进入DataX安装目录
cd /usr/local/datax
# 执行同步任务
bin/datax.py job/mysql2es.json

适用场景

  • 历史数据迁移,如分库分表MySQL数据迁移至ES。
  • 大数据量批量同步(百万级以上数据)。
  • 离线数据同步,对实时性无要求(小时级)。

优缺点分析

优点 缺点
支持分库分表、并行读取,迁移效率高 同步延迟高(小时级),不支持实时同步
数据校验、故障重试机制完善,稳定性强 配置复杂,需熟悉DataX插件参数
支持多种数据转换,适配不同字段类型 不支持增量同步(需结合定时任务实现)

性能调优技巧

  1. 调整并发通道数channel数建议与ES分片数一致,避免写入瓶颈。
  2. 优化批量大小batchSize根据ES性能调整,一般设置为5000-10000条/批。
  3. 分片查询优化splitPk选择主键或索引字段,确保并行查询均匀分布。
  4. 避免OOM :通过fetchSize控制单次查询记录数,避免内存溢出。

六、Flink流处理:复杂ETL场景的实时同步方案

Flink是一款分布式流处理框架,支持高吞吐、低延迟的实时数据处理,适合需要复杂ETL(数据抽取、转换、加载)的MySQL-ES同步场景。

核心原理

  1. 通过CanalSource读取MySQL binlog日志,转换为Flink数据流。
  2. 利用Flink的状态管理、维表关联等功能,完成复杂数据处理(如关联用户画像、计算推荐评分)。
  3. 通过ElasticsearchSink将处理后的数据写入ES。

实战代码

java 复制代码
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.common.state.BroadcastState;
import org.apache.flink.api.common.state.MapStateDescriptor;
import org.apache.flink.api.common.state.ReadOnlyBroadcastState;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.BroadcastConnectedStream;
import org.apache.flink.streaming.api.datastream.BroadcastStream;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.co.BroadcastProcessFunction;
import org.apache.flink.util.Collector;
import org.apache.flink.connector.elasticsearch7.Elasticsearch7Sink;
import org.apache.flink.connector.elasticsearch7.RestClientFactory;
import org.elasticsearch.client.RestClientBuilder;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

public class MysqlEsFlinkSync {

    // 定义用户画像状态描述符(广播流)
    private static final MapStateDescriptor<String, UserProfile> USER_PROFILE_DESCRIPTOR =
            new MapStateDescriptor<>("userProfile", String.class, UserProfile.class);

    public static void main(String[] args) throws Exception {
        // 1. 创建Flink执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(3); // 并行度设置

        // 2. 读取MySQL binlog数据流(通过CanalSource)
        DataStream<BinlogEvent> binlogStream = env.addSource(new CanalSource(
                "127.0.0.1:11111", // Canal服务地址
                "example", // Canal实例名
                "business_db.product", // 订阅的表
                Duration.ofSeconds(5) // 水位线延迟
        ))
                // 设置水位线,处理乱序事件
                .assignTimestampsAndWatermarks(
                        WatermarkStrategy.<BinlogEvent>forBoundedOutOfOrderness(Duration.ofSeconds(3))
                                .withTimestampAssigner((event, timestamp) -> event.getEventTime())
                );

        // 3. 转换为商品价格事件流
        DataStream<PriceEvent> priceEventStream = binlogStream
                .filter(event -> "UPDATE".equals(event.getEventType())) // 只处理更新事件
                .map((MapFunction<BinlogEvent, PriceEvent>) event -> {
                    Map<String, Object> data = event.getData();
                    return new PriceEvent(
                            data.get("id").toString(),
                            data.get("product_id").toString(),
                            Double.parseDouble(data.get("price").toString()),
                            event.getEventTime()
                    );
                });

        // 4. 读取用户画像维表(广播流,全量更新)
        DataStream<UserProfile> userProfileStream = env.addSource(new UserProfileSource());
        BroadcastStream<UserProfile> broadcastStream = userProfileStream
                .broadcast(USER_PROFILE_DESCRIPTOR);

        // 5. 关联商品价格流与用户画像广播流
        BroadcastConnectedStream<PriceEvent, UserProfile> connectedStream =
                priceEventStream.connect(broadcastStream);

        // 6. 处理关联后的数据,计算推荐评分
        DataStream<ProductRecommendation> recommendationStream = connectedStream.process(
                new BroadcastProcessFunction<PriceEvent, UserProfile, ProductRecommendation>() {

                    @Override
                    public void processElement(PriceEvent priceEvent, ReadOnlyContext ctx, Collector<ProductRecommendation> out) throws Exception {
                        // 读取广播状态中的用户画像
                        ReadOnlyBroadcastState<String, UserProfile> broadcastState = ctx.getBroadcastState(USER_PROFILE_DESCRIPTOR);
                        UserProfile userProfile = broadcastState.get(priceEvent.getProductId());

                        if (userProfile != null) {
                            // 计算推荐评分(示例逻辑:价格匹配用户消费能力)
                            double score = calculateRecommendationScore(priceEvent.getPrice(), userProfile.getConsumptionLevel());
                            out.collect(new ProductRecommendation(
                                    priceEvent.getProductId(),
                                    userProfile.getUserId(),
                                    priceEvent.getPrice(),
                                    score,
                                    System.currentTimeMillis()
                            ));
                        }
                    }

                    @Override
                    public void processBroadcastElement(UserProfile userProfile, Context ctx, Collector<ProductRecommendation> out) throws Exception {
                        // 更新广播状态
                        BroadcastState<String, UserProfile> broadcastState = ctx.getBroadcastState(USER_PROFILE_DESCRIPTOR);
                        broadcastState.put(userProfile.getProductId(), userProfile);
                    }

                    // 推荐评分计算逻辑
                    private double calculateRecommendationScore(double price, int consumptionLevel) {
                        if (consumptionLevel == 1) {
                            return price < 100 ? 9.0 : (price < 300 ? 7.0 : 5.0);
                        } else if (consumptionLevel == 2) {
                            return price < 300 ? 9.0 : (price < 500 ? 7.0 : 5.0);
                        } else {
                            return price < 500 ? 9.0 : (price < 1000 ? 7.0 : 5.0);
                        }
                    }
                }
        );

        // 7. 写入ES
        recommendationStream.addSink(Elasticsearch7Sink.<ProductRecommendation>builder(
                (element, context, indexer) -> {
                    Map<String, Object> data = new HashMap<>();
                    data.put("product_id", element.getProductId());
                    data.put("user_id", element.getUserId());
                    data.put("price", element.getPrice());
                    data.put("recommendation_score", element.getScore());
                    data.put("create_time", element.getCreateTime());

                    indexer.add(new IndexRequest("product_recommendations")
                            .id(element.getProductId() + "_" + element.getUserId())
                            .source(data));
                }
        )
                .setRestClientFactory((RestClientFactory) restClientBuilder -> {
                    restClientBuilder.setHttpClientConfigCallback(httpClientBuilder -> {
                        httpClientBuilder.setMaxConnTotal(100);
                        httpClientBuilder.setMaxConnPerRoute(50);
                        return httpClientBuilder;
                    });
                })
                .build()
        );

        // 8. 执行任务
        env.execute("MySQL-ES Product Recommendation Sync");
    }

    // 商品价格事件实体
    @Data
    public static class PriceEvent {
        private String id;
        private String productId;
        private double price;
        private long eventTime;

        // 构造函数、getter、setter省略
    }

    // 用户画像实体
    @Data
    public static class UserProfile {
        private String userId;
        private String productId;
        private int consumptionLevel; // 消费等级:1-低,2-中,3-高

        // 构造函数、getter、setter省略
    }

    // 商品推荐结果实体
    @Data
    public static class ProductRecommendation {
        private String productId;
        private String userId;
        private double price;
        private double score;
        private long createTime;

        // 构造函数、getter、setter省略
    }
}

适用场景

  • 复杂ETL场景,如商品价格变更后关联用户画像计算实时推荐评分。
  • 需处理乱序事件、维表关联的实时数据同步。
  • 实时数仓建设,需对数据进行多维度加工处理。

优缺点分析

优点 缺点
支持复杂数据处理、维表关联、乱序事件处理 技术栈复杂,学习成本高
高吞吐、低延迟,适合实时计算场景 部署维护成本高,需专业Flink运维
状态管理完善,故障恢复能力强 资源消耗大,需足够的集群资源

核心特性拓展

  1. Watermark机制:处理乱序事件,确保数据按时间顺序正确计算。
  2. Broadcast State:用于维表全量更新,避免频繁查询外部存储。
  3. Checkpoint机制:定期保存作业状态,故障后可恢复至最近检查点,保证数据一致性。

七、方案选型指南与总结

1. 核心指标对比表

方案 实时性 侵入性 复杂度 适用场景 推荐优先级
同步双写 秒级 小型单体项目、高实时要求 ★★★☆☆
异步双写(MQ) 秒级 中型分布式系统、中等并发 ★★★★☆
Logstash定时拉取 分钟级 离线分析、历史数据迁移 ★★★☆☆
Canal监听Binlog 毫秒级 高并发生产环境、实时检索 ★★★★★
DataX批量同步 小时级 大数据量迁移、分库分表同步 ★★★★☆
Flink流处理 毫秒级 极高 复杂ETL、实时数仓 ★★★☆☆

2. 选型决策树

复制代码
1. 需求实时性?
   - 毫秒级 → 2
   - 秒级 → 3
   - 分钟/小时级 → 4
2. 是否需要复杂数据处理?
   - 是 → Flink流处理
   - 否 → Canal监听Binlog
3. 是否允许代码侵入?
   - 是 → 同步双写
   - 否 → 异步双写(MQ)
4. 数据量大小?
   - 百万级以上 → DataX批量同步
   - 少量数据 → Logstash定时拉取

3. 关键建议

  • 小型项目、快速验证场景:优先选择同步双写或Logstash,开发成本低。
  • 中型分布式系统:推荐异步双写(MQ),平衡可用性与开发成本。
  • 大型生产环境、高并发实时场景:首选Canal+MQ方案,无侵入且高可靠。
  • 历史数据迁移、大数据量同步:DataX是最优选择,支持分库分表与批量处理。
  • 复杂ETL、实时计算场景:Flink流处理能满足多维度数据加工需求。

数据同步的核心是在"实时性、一致性、可用性"之间找到平衡,开发者需根据业务场景、团队技术栈与资源情况综合选择。实际落地时,还需关注数据一致性保障、监控告警、故障恢复等细节,确保同步链路稳定可靠。

相关推荐
yuguo.im3 小时前
Elasticsearch vs MySQL:查询语法与设计哲学对比
mysql·elasticsearch
edjxj4 小时前
解决QT可执行文件在不同缩放大小的电脑上显示差异
服务器·数据库·qt
Mr.Pascal11 小时前
Redis:主动更新,读时更新,定时任务。三种的优劣势对比
数据库·redis·缓存
思成不止于此12 小时前
【MySQL 零基础入门】DQL 核心语法(二):表条件查询与分组查询篇
android·数据库·笔记·学习·mysql
骥龙12 小时前
3.10、构建网络防线:防火墙、WAF 与蜜罐实战
服务器·网络·数据库·网络安全
帝吃藕和13 小时前
MySQL 知识点复习- 4. update/delete/like
mysql
gugugu.14 小时前
Redis 字符串类型完全指南:从原理到实战应用
数据库·redis·缓存
杨云龙UP14 小时前
MySQL 自动备份与覆盖恢复实战:一套脚本搞定全库/按库备份恢复
linux·运维·数据库·sql·mysql
workflower15 小时前
PostgreSQL 数据库优化
数据库·团队开发·数据库开发·时序数据库·数据库架构