面试的时候,最怕的不是问你不会的,而是你以为自己会的。
就比如下面这个场景,我敢打赌百分之八十的 Java 后端都掉进去过:
❝面试官:你项目里用了 Elasticsearch,是怎么同步数据的?
你:我们在写数据库的时候,同时也写了 ES。
面试官:那你怎么保证两个数据源的一致性?比如数据库写成功了,但 ES 写失败了呢?
你:……我还是回去等通知吧。
听起来是不是有点熟悉?别急着笑别人,也可能就是你。
其实,像这种多数据源同步的问题,在业务复杂一点的系统里几乎是标配,双写,是最容易上手的做法,也最容易出问题的做法。
今天咱就唠唠:为啥系统里不推荐双写?
双写,听起来很美,实际上很坑
先说说大家最常见的场景。
一开始,业务简单,只有一个数据库,CRUD 香得不行,部署个 MySQL,插个 Spring Boot,一切都风平浪静。
结果业务做大了——
- 数据库压力飙升,怎么办?加个 Redis 缓缓存吧。
- 搜索功能用户抱怨慢?加个 Elasticsearch 做全文检索吧。
- 老板说要 BI 报表?那搞个数据中台,用 Hadoop、Hive 跑个批处理分析吧。
于是,数据开始被写到多个地方。数据库、缓存、ES、数据仓库……就像你谈了四个对象,长得都不一样,但她们都说你是唯一。
你想了一招简单粗暴的同步方式:
public void saveProduct(Product product) {
database.save(product);
elasticsearch.save(product);
}
完美!数据库有了,ES也有了!
可问题也来了。
为什么双写是个坑?
因为它看起来同步,其实不同步。
比如你代码执行这两行:
database.save(product); // 成功了
elasticsearch.save(product); // 报错了
恭喜你,你的数据库和 ES 不一致了!
再比如两个请求并发执行:
两个请求都成功执行了数据库和 ES 的双写,但执行顺序不一致,就有可能一个数据源是 5,另一个是 10。
这不叫同步,这叫事故前兆。
问题主要集中在两块:
1. 一致性问题
数据要是不同步,光靠用户的血汗眼睛是看不出来的。比如用户查的是缓存,缓存没更新,就会看到旧数据;ES 没同步成功,搜索结果查不到最新商品。
这些 bug 特别隐蔽,因为它不会直接爆炸,而是偷偷摸摸地喂你一口屎。
2. 原子性问题
你不能保证两步操作是一个原子动作。
你总不能指望写入数据库和写入 ES 像手牵手一样成功,要么一起成功,要么一起失败。数据库事务确实能 roll back,但人家 ES 不吃你这套。
除非你能把所有系统放进一个分布式事务框架里,什么 XA 协议,什么 TCC 模式,听起来高级,真用了你就知道坑有多深。
而且,ES 和 Redis 这种系统,本身就不是为强一致设计的。
所以问题来了,那该怎么办?总不能放弃同步吧?
双写不好,单写也不行,那就来点巧的
真正靠谱的做法,是走异步解耦 + 顺序消费 + 最终一致性这条路。
说人话就是——你先把数据写到数据库,然后把“写了这条数据”的这个动作,发到一个消息队列里,其他系统(比如 ES、Redis、数据仓库)通过订阅消息队列来被动同步数据。
比如,Kafka、RocketMQ、RabbitMQ 就是干这活的。
架构图就变成了这样:
Client -> Database
-> Kafka -> ES
-> Redis
-> Hadoop
你可能会问:那我写数据库成功了,但消息发 Kafka 失败了呢?
可以这么处理:
- 或者写库成功后,把变更操作记录到一个“数据变更日志表”,异步定时推送到 Kafka
很多大厂用的就是这种套路。
比如你 insert 了一条产品数据,写入数据库成功后,插入一条变更记录到变更表,内容大概是:
{
"eventType": "INSERT",
"table": "product",
"data": {
"id": 123,
"name": "MacBook Air",
"price": 8999
},
"timestamp": "2025-04-18 10:00:00"
}
然后异步服务把这条变更推送到 Kafka,ES 消费这个消息,根据内容更新自己的索引。
这样子,所有数据源的更新都是按顺序来的,也就不会乱。
当然,为了保险起见,每个系统消费 Kafka 的时候还得记录消费位点(offset),如果消费失败了,就从上次失败的地方继续重试。
实战中,我们该怎么做?
别光说理论,咱实打实来点代码感受下。
数据库写入 + 记录变更日志
@Transactional
public void saveProduct(Product product) {
productRepository.save(product);
ChangeLog log = new ChangeLog();
log.setEventType("INSERT");
log.setTableName("product");
log.setData(JSON.toJSONString(product));
log.setTimestamp(LocalDateTime.now());
changeLogRepository.save(log);
}
异步任务:读取变更日志,发消息
@Scheduled(fixedDelay = 1000)
public void pushChangeLog() {
List logs = changeLogRepository.findUnsentLogs();
for (ChangeLog log : logs) {
kafkaTemplate.send("product-changes", log.getData());
log.setSent(true);
changeLogRepository.save(log);
}
}
消费端(ES服务):接收消息,同步数据
@KafkaListener(topics = "product-changes")
public void onMessage(String data) {
Product product = JSON.parseObject(data, Product.class);
elasticsearchService.save(product);
}
当然啦,实际系统中你还需要:
但核心逻辑就是这三个字:异步化。
用 Canal 做被动监听
如果你不想自己写日志表,还可以用成熟的中间件。
比如 MySQL + Canal,就是典型的 binlog 监听方式。
Canal 能监听数据库的 binlog 日志,把变更数据实时推送到 Kafka,你啥都不用管,ES、Redis 这些服务直接监听 Kafka 消息就能同步数据。
配个图感受一下:
MySQL -> Binlog -> Canal -> Kafka -> Redis/ES/Hadoop
这套东西就跟“监听朋友圈动态”的逻辑一样,别人发了,你自动看到,系统自动同步,省心又安心。
写在最后
面试这种事,说穿了就一个字:稳。
你要说自己系统用了双写,面试官一问原子性和一致性问题,你就露馅了;但如果你能聊到:
那这个点,立刻就从坑变成了加分项。
你看技术,不光是要能写,还得能解释出个所以然来。