通过 FlinkCDC 实现 MySQL 和 Elasticsearch 的双写一致性,并通过状态字段(如是否已同步到 ES 的标识)来保证数据的伪强实时性。
1. 方案概述
-
数据流:
- MySQL -> FlinkCDC -> Elasticsearch -> MQ -> 状态更新服务。
- 新增数据默认标记为“未同步”,同步到 Elasticsearch 后通过 MQ 更新状态为“已同步”。
-
伪强实时性:
- 通过状态字段(如
is_synced)控制数据的展示逻辑,确保列表、详情和搜索功能只展示已同步的数据。
- 通过状态字段(如
-
一致性保证:
- FlinkCDC 监听 MySQL 的变更,实时同步到 Elasticsearch。
- 同步完成后,通过 MQ 触发状态更新服务,更新 MySQL 中的状态字段。
2. 实现步骤
-
FlinkCDC 监听 MySQL 变更:
- 使用 FlinkCDC 监听 MySQL 的 Binlog,捕获数据的插入、更新和删除操作。
-
同步到 Elasticsearch:
- 将捕获的数据实时写入 Elasticsearch。
-
发送到 MQ:
- 数据写入 Elasticsearch 后,发送一条消息到 MQ,表示数据已同步。
-
状态更新服务:
- 消费 MQ 中的消息,更新 MySQL 中的状态字段(如
is_synced)。
- 消费 MQ 中的消息,更新 MySQL 中的状态字段(如
-
数据展示:
- 列表、详情和搜索功能只展示
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 中的状态字段。
- 列表、详情和搜索功能只展示已同步的数据,实现伪强实时性。