[分布式订单状态同步]:FlinkCDC+仅展示双写状态字段:保证MySQL与ElasticSearch双写一致性

197 阅读2分钟

通过 FlinkCDC 实现 MySQL 和 Elasticsearch 的双写一致性,并通过状态字段(如是否已同步到 ES 的标识)来保证数据的伪强实时性。


1. 方案概述

  1. 数据流

    • MySQL -> FlinkCDC -> Elasticsearch -> MQ -> 状态更新服务。
    • 新增数据默认标记为“未同步”,同步到 Elasticsearch 后通过 MQ 更新状态为“已同步”。
  2. 伪强实时性

    • 通过状态字段(如 is_synced)控制数据的展示逻辑,确保列表、详情和搜索功能只展示已同步的数据。
  3. 一致性保证

    • FlinkCDC 监听 MySQL 的变更,实时同步到 Elasticsearch。
    • 同步完成后,通过 MQ 触发状态更新服务,更新 MySQL 中的状态字段。

2. 实现步骤

  1. FlinkCDC 监听 MySQL 变更

    • 使用 FlinkCDC 监听 MySQL 的 Binlog,捕获数据的插入、更新和删除操作。
  2. 同步到 Elasticsearch

    • 将捕获的数据实时写入 Elasticsearch。
  3. 发送到 MQ

    • 数据写入 Elasticsearch 后,发送一条消息到 MQ,表示数据已同步。
  4. 状态更新服务

    • 消费 MQ 中的消息,更新 MySQL 中的状态字段(如 is_synced)。
  5. 数据展示

    • 列表、详情和搜索功能只展示 is_synced = 1 的数据。

3. 代码示例

(1)FlinkCDC 监听 MySQL 并写入 Elasticsearch

import com.ververica.cdc.connectors.mysql.MySQLSource;
import com.ververica.cdc.connectors.mysql.table.StartupOptions;
import com.ververica.cdc.debezium.JsonDebeziumDeserializationSchema;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.connectors.elasticsearch7.ElasticsearchSink;
import org.apache.flink.streaming.connectors.elasticsearch7.RestClientFactory;
import org.apache.http.HttpHost;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.Requests;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
​
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
​
public class FlinkCDCToES {
    public static void main(String[] args) throws Exception {
        // 创建 Flink 执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
​
        // 配置 MySQL CDC Source
        MySQLSource<String> mySQLSource = MySQLSource.<String>builder()
                .hostname("localhost")
                .port(3306)
                .databaseList("test_db")
                .tableList("test_db.test_table")
                .username("root")
                .password("password")
                .deserializer(new JsonDebeziumDeserializationSchema())
                .startupOptions(StartupOptions.initial())
                .build();
​
        // 添加 MySQL CDC Source
        DataStream<String> source = env.addSource(mySQLSource);
​
        // 配置 Elasticsearch Sink
        List<HttpHost> httpHosts = new ArrayList<>();
        httpHosts.add(new HttpHost("localhost", 9200, "http"));
​
        ElasticsearchSink.Builder<String> esSinkBuilder = new ElasticsearchSink.Builder<>(
                httpHosts,
                (element, ctx, indexer) -> {
                    Map<String, String> json = new HashMap<>();
                    json.put("data", element);
                    IndexRequest indexRequest = Requests.indexRequest()
                            .index("test_index")
                            .source(json);
                    indexer.add(indexRequest);
                }
        );
​
        // 写入 Elasticsearch
        source.addSink(esSinkBuilder.build());
​
        // 执行任务
        env.execute("Flink CDC to Elasticsearch");
    }
}

(2)发送到 MQ

在数据写入 Elasticsearch 后,发送一条消息到 MQ:

import org.apache.flink.streaming.api.functions.sink.SinkFunction;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
​
public class MQSink implements SinkFunction<String> {
    private static final String TOPIC = "sync_status_topic";
    private static final String BOOTSTRAP_SERVERS = "localhost:9092";
​
    private transient KafkaProducer<String, String> producer;
​
    @Override
    public void invoke(String value, Context context) {
        if (producer == null) {
            producer = new KafkaProducer<>(getKafkaProperties());
        }
        producer.send(new ProducerRecord<>(TOPIC, value));
    }
​
    private Properties getKafkaProperties() {
        Properties props = new Properties();
        props.put("bootstrap.servers", BOOTSTRAP_SERVERS);
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        return props;
    }
}

(3)状态更新服务

消费 MQ 中的消息,更新 MySQL 中的状态字段:

import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.consumer.ConsumerRecords;
​
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.time.Duration;
import java.util.Collections;
import java.util.Properties;
​
public class StatusUpdateService {
    private static final String TOPIC = "sync_status_topic";
    private static final String BOOTSTRAP_SERVERS = "localhost:9092";
​
    public static void main(String[] args) {
        Properties props = new Properties();
        props.put("bootstrap.servers", BOOTSTRAP_SERVERS);
        props.put("group.id", "status-update-group");
        props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
​
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
        consumer.subscribe(Collections.singletonList(TOPIC));
​
        while (true) {
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
            for (ConsumerRecord<String, String> record : records) {
                updateSyncStatus(record.value());
            }
        }
    }
​
    private static void updateSyncStatus(String data) {
        try (Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test_db", "root", "password")) {
            String sql = "UPDATE test_table SET is_synced = 1 WHERE id = ?";
            PreparedStatement statement = connection.prepareStatement(sql);
            statement.setString(1, extractId(data)); // 假设 data 包含记录的 ID
            statement.executeUpdate();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
​
    private static String extractId(String data) {
        // 解析 data 提取 ID
        return data; // 假设 data 就是 ID
    }
}

4. 总结

  • 通过 FlinkCDC 监听 MySQL 变更,实时同步到 Elasticsearch。
  • 同步完成后,通过 MQ 触发状态更新服务,更新 MySQL 中的状态字段。
  • 列表、详情和搜索功能只展示已同步的数据,实现伪强实时性。