在分布式架构中,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同步,避免主业务受影响。
核心原理
- 生产者:业务系统完成MySQL写入后,向MQ发送消息(携带业务ID或完整数据)。
- 消费者:监听MQ主题,接收消息后查询MySQL获取完整数据(或直接使用消息体),写入ES。
- 依赖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,无需修改业务代码。
核心原理
- 配置Logstash的JDBC输入插件,指定MySQL连接信息、查询SQL和执行周期。
- 利用
sql_last_value参数记录上次同步的时间戳,实现增量拉取。 - 通过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带来查询压力 |
进阶优化:避免全表扫描
- 为
update_time字段建立索引,优化增量查询性能:
sql
ALTER TABLE user_log ADD INDEX idx_update_time (update_time);
- 分时段拉取大表数据,避免单次查询数据量过大:
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,是高并发生产环境的首选方案。
核心原理
- MySQL开启binlog日志(row格式),Canal伪装成MySQL从库,订阅binlog日志。
- Canal解析binlog日志,提取数据变更(增删改),发送至消息队列(如RocketMQ/Kafka)。
- 消费者监听消息队列,将变更数据同步至ES。
- 整个流程无侵入,同步延迟可达毫秒级。
架构流程
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配置,需确保格式正确 |
避坑指南
- 数据漂移处理:通过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);
}
}
}
- 幂等消费:利用ES文档ID的唯一性,重复消费时会覆盖原有数据,避免重复写入。对于DELETE操作,可通过判断文档是否存在来避免误删。
五、DataX批量同步:大数据量迁移的首选方案
DataX是阿里巴巴开源的离线数据同步工具,支持多种数据源之间的批量数据迁移,尤其适合MySQL分库分表到ES的历史数据迁移场景。
核心原理
- DataX通过Reader插件读取MySQL数据(支持分库分表、并行读取)。
- 经过数据转换(Transformer)后,通过Writer插件批量写入ES。
- 支持流量控制、数据校验、故障重试等机制,保证大数据量迁移的稳定性。
实战配置
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插件参数 |
| 支持多种数据转换,适配不同字段类型 | 不支持增量同步(需结合定时任务实现) |
性能调优技巧
- 调整并发通道数 :
channel数建议与ES分片数一致,避免写入瓶颈。 - 优化批量大小 :
batchSize根据ES性能调整,一般设置为5000-10000条/批。 - 分片查询优化 :
splitPk选择主键或索引字段,确保并行查询均匀分布。 - 避免OOM :通过
fetchSize控制单次查询记录数,避免内存溢出。
六、Flink流处理:复杂ETL场景的实时同步方案
Flink是一款分布式流处理框架,支持高吞吐、低延迟的实时数据处理,适合需要复杂ETL(数据抽取、转换、加载)的MySQL-ES同步场景。
核心原理
- 通过CanalSource读取MySQL binlog日志,转换为Flink数据流。
- 利用Flink的状态管理、维表关联等功能,完成复杂数据处理(如关联用户画像、计算推荐评分)。
- 通过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运维 |
| 状态管理完善,故障恢复能力强 | 资源消耗大,需足够的集群资源 |
核心特性拓展
- Watermark机制:处理乱序事件,确保数据按时间顺序正确计算。
- Broadcast State:用于维表全量更新,避免频繁查询外部存储。
- 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流处理能满足多维度数据加工需求。
数据同步的核心是在"实时性、一致性、可用性"之间找到平衡,开发者需根据业务场景、团队技术栈与资源情况综合选择。实际落地时,还需关注数据一致性保障、监控告警、故障恢复等细节,确保同步链路稳定可靠。