ES数据同步大乱斗:同步双写 vs MQ异步,谁才是王者?

你以为数据库到ES的数据同步只是简单的复制粘贴?Too young too simple!

前言:为什么需要数据同步?

各位程序员朋友们,想必大家都经历过这样的场景:数据库中存储了海量数据,但查询速度却让人抓狂。这时候Elasticsearch(ES)就闪亮登场了!它强大的全文搜索和分析能力让我们的查询飞起来。

但是问题来了:如何让数据库中的数据乖乖跑到ES里去呢? 这就是今天我们要讨论的重点!

方案一:同步双写 - 直来直去的"老实人"

实现思路

同步双写,顾名思义就是在写数据库的同时,直接同步写ES。简单粗暴,就像个老实人一样直来直去。

java 复制代码
@Service
public class UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private ElasticsearchTemplate elasticsearchTemplate;
    
    @Transactional
    public void addUser(User user) {
        // 1. 写入数据库
        userRepository.save(user);
        
        // 2. 同步写入ES
        IndexQuery indexQuery = new IndexQueryBuilder()
            .withObject(user)
            .build();
        elasticsearchTemplate.index(indexQuery);
    }
}

优缺点分析

优点:

  • 实现简单,代码直观
  • 强一致性,数据立即同步
  • 不需要引入额外中间件

缺点:

  • 性能瓶颈:每次写操作都要等两个系统都完成
  • 系统耦合度高:数据库和ES强耦合
  • 异常处理复杂:需要处理分布式事务问题

💡 适合场景:数据量不大,对一致性要求极高的场景

方案二:异步双写(MQ) - "聪明人的选择"

实现思路

异步双写通过消息队列(MQ)解耦,先写数据库,然后发送消息到MQ,最后由消费者异步写入ES。

graph LR A[应用] --> B[写入数据库] B --> C[发送消息到MQ] C --> D[消费者读取消息] D --> E[写入ES]

优缺点分析

优点:

  • 系统解耦,各司其职
  • 性能提升,异步处理不阻塞主流程
  • 容错性好,MQ有重试机制

缺点:

  • 最终一致性,数据同步有延迟
  • 需要维护MQ中间件
  • 系统复杂度增加

💡 适合场景:大部分业务场景,特别是对性能要求高的场景

实战:使用RabbitMQ实现数据同步

1. 准备消息转换器

首先配置RabbitMQ的消息转换器,确保消息能够正确序列化和反序列化。

java 复制代码
@Configuration
public class RabbitMQConfig {
    
    @Bean
    public MessageConverter jsonMessageConverter() {
        return new Jackson2JsonMessageConverter();
    }
    
    @Bean
    public Queue esQueue() {
        return new Queue("es.sync.queue", true);
    }
    
    @Bean
    public Exchange esExchange() {
        return new DirectExchange("es.sync.exchange");
    }
    
    @Bean
    public Binding binding(Queue esQueue, Exchange esExchange) {
        return BindingBuilder.bind(esQueue)
               .to(esExchange)
               .with("es.sync.routingKey")
               .noargs();
    }
}

2. 服务发送数据到MQ

在业务服务中,完成数据库操作后发送消息到MQ

java 复制代码
@Service
public class UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @Transactional
    public void addUser(User user) {
        // 1. 写入数据库
        User savedUser = userRepository.save(user);
        
        // 2. 发送消息到MQ
        rabbitTemplate.convertAndSend(
            "es.sync.exchange",
            "es.sync.routingKey",
            savedUser);
    }
}

3. 监听MQ将数据存储到ES

创建消费者监听MQ消息并写入ES

java 复制代码
@Component
public class EsDataSyncConsumer {
    
    @Autowired
    private ElasticsearchTemplate elasticsearchTemplate;
    
    @RabbitListener(queues = "es.sync.queue")
    public void handleMessage(User user) {
        try {
            IndexQuery indexQuery = new IndexQueryBuilder()
                .withObject(user)
                .build();
            
            elasticsearchTemplate.index(indexQuery);
            System.out.println("数据同步ES成功: " + user.getId());
        } catch (Exception e) {
            System.err.println("数据同步ES失败: " + e.getMessage());
            // 这里可以加入重试机制
            throw new AmqpRejectAndDontRequeueException(e);
        }
    }
}

棘手问题:消息顺序消费

问题描述

在某些场景下,消息的顺序很重要。比如:

  1. 先新增用户,再更新用户信息
  2. 先创建订单,再取消订单

如果消息乱序消费,会导致ES中的数据状态错误!

解决方案

方案1:单一消费者模式

最简单的方案是只使用一个消费者,确保消息按顺序处理。

java 复制代码
@Bean
public SimpleMessageListenerContainer messageListenerContainer(
    ConnectionFactory connectionFactory) {
    SimpleMessageListenerContainer container = 
        new SimpleMessageListenerContainer(connectionFactory);
    container.setQueues(esQueue());
    container.setConcurrentConsumers(1); // 关键:只设置一个消费者
    container.setMaxConcurrentConsumers(1);
    return container;
}

优缺点:实现简单,但性能受限

方案2:按业务ID分片(推荐)

使用RabbitMQ的consistent hash exchange或者按照业务ID进行路由,确保同一业务ID的消息发送到同一个队列。

java 复制代码
// 发送消息时,根据业务ID决定routing key
public void sendUserMessage(User user) {
    String routingKey = "user." + (user.getId() % 10); // 分成10个队列
    rabbitTemplate.convertAndSend("user.sync.exchange", routingKey, user);
}

// 配置多个队列,每个队列一个消费者
@Bean
public Queue userQueue0() { return new Queue("user.queue.0", true); }
@Bean
public Queue userQueue1() { return new Queue("user.queue.1", true); }
// ... 配置更多队列

方案3:版本号控制

在消息体中增加版本号,消费者处理时检查版本号,只处理最新版本的消息。

java 复制代码
@Data
public class UserMessage {
    private User user;
    private Long version;
    private String operationType; // CREATE, UPDATE, DELETE
}

// 消费者处理逻辑
public void handleMessage(UserMessage message) {
    Long currentVersion = getCurrentVersionFromES(message.getUser().getId());
    if (message.getVersion() > currentVersion) {
        // 处理消息
        updateES(message.getUser());
    }
}

总结对比

方案 一致性 性能 复杂度 适用场景
同步双写 强一致性 数据量小,要求强一致
MQ异步 最终一致性 大多数业务场景

写在最后

选择合适的数据同步方案就像选择女朋友一样,没有最好的,只有最合适的!

  • 如果你业务简单,数据量小,同步双写是你的"贤惠型女友"
  • 如果你追求高性能,能接受短暂延迟,MQ异步就是你的"时尚型女友"

无论选择哪种方案,都要记得:没有银弹!根据业务需求做出最适合的选择才是王道。


互动环节:大家在项目中用过哪种数据同步方案?遇到了哪些坑?欢迎在评论区分享你的"血泪史"!

点赞关注不迷路,下一篇我们将深入探讨「ES数据同步之数据一致性保障」,敬请期待!

相关推荐
Yvonne爱编码3 小时前
后端编程开发路径:从入门到精通的系统性探索
java·前端·后端·python·sql·go
程序消消乐3 小时前
ZooKeeper Multi-op+乐观锁实战优化:提升分布式Worker节点状态一致性
分布式·zookeeper·云原生
猫林老师3 小时前
HarmonyOS 5分布式数据管理初探:实现跨设备数据同步
分布式·harmonyos
失散133 小时前
分布式专题——10.3 ShardingSphere实现原理以及内核解析
java·分布式·架构·shardingsphere·分库分表
虫小宝3 小时前
京东返利app的多数据源整合策略:分布式数据同步与一致性保障
分布式
Cloud Traveler3 小时前
第3天-Jenkins详解-3
运维·分布式·jenkins
u0104058363 小时前
电商导购平台的搜索引擎优化:基于Elasticsearch的商品精准推荐系统
elasticsearch·搜索引擎·jenkins
在未来等你4 小时前
Elasticsearch面试精讲 Day 16:索引性能优化策略
大数据·分布式·elasticsearch·搜索引擎·面试
bobz9654 小时前
ovn 厂商使用的规模
后端