Kafka + MongoDB 高并发数据插入:八年 Java 开发的实战优化秘籍
作为一名踩坑无数的八年 Java 老兵,最近刚搞定一个「日均千万级数据同步」的需求 —— 用 Kafka 承接上游高并发流量,再平稳写入 MongoDB。过程中踩过数据丢失、写入超时、消息堆积的大坑,也沉淀了一套可直接落地的优化方案。今天就把这套从生产环境淬炼出的实战经验,掰开揉碎了分享给大家,新手也能直接抄作业!
一、先搞懂核心矛盾:为什么高并发下直接插入会崩?
很多新手上来就写「Kafka 消费→单条插入 MongoDB」的代码,压测直接崩给你看。其实问题的核心矛盾就三个,八年经验告诉我,绕不开也躲不掉:
- IO 开销爆炸:单条插入时,每条数据都要走一次网络请求 + 数据库事务,百万级数据的网络往返直接把 MongoDB 压垮;
- 数据一致性风险:Kafka 默认自动提交 offset,一旦 MongoDB 写入失败但 offset 已提交,数据直接丢失,对账都没法对;
- 并发控制失衡:Kafka 分区数和消费线程不匹配,要么线程空闲浪费资源,要么分区不够导致消费堆积。
之前接手过一个项目,就是因为没处理好这些问题,导致 MongoDB 写入超时率高达 30%,消息堆积超百万条。后来按下面的方案优化后,TPS 直接从 3000 飙升到 5 万 +,超时率压到 0.1% 以下。
二、架构设计:高并发的基石是「解耦 + 批量」
高并发场景下,架构设计决定了 80% 的性能上限。推荐这套经过生产验证的架构:
上游系统 → Kafka(分区并行) → 消费者(批量拉取) → 本地缓存队列 → MongoDB(批量写入)
↓
死信队列(异常处理)
核心设计思路就两个:
- 用 Kafka 做流量削峰:不管上游流量波动多大,Kafka 都能稳稳接住,消费者按 MongoDB 的承载能力匀速消费;
- 批量处理减少 IO:把「单条拉取 + 单条插入」改成「批量拉取 + 批量插入」,MongoDB 批量写入性能比单条高 3~6 倍。
三、Kafka 消费者:调优到极致的「数据接收器」
Kafka 是高并发的入口,配置错一个参数,后面再怎么优化都白搭。这几个核心参数是八年经验总结的黄金配置,闭眼抄就行:
1. 核心配置优化(application.yml)
spring:
kafka:
consumer:
bootstrap-servers: 172.31.64.10:9092,172.31.64.11:9092,172.31.64.12:9092
group-id: ${spring.application.name}
auto-offset-reset: earliest # 首次启动消费历史数据,后续可改latest
enable-auto-commit: false # 关闭自动提交,避免数据丢失(关键!)
max-poll-interval-ms: 600000 # 10分钟,适配MongoDB批量写入耗时
max-poll-records: 500 # 单次拉取500条,平衡吞吐量和内存
session-timeout-ms: 45000 # 必须小于max-poll-interval-ms
heartbeat-interval-ms: 3000 # 3秒发一次心跳,避免被踢下线
listener:
ack-mode: manual_immediate # 手动提交offset,写入成功才确认
concurrency: 6 # 消费线程数=Topic分区数(关键匹配!)
poll-timeout: 3000 # 拉取超时时间,避免无限阻塞
2. 关键优化点解读
- 分区数与线程数匹配:Kafka 的核心并发机制是分区,一个分区只能被同组一个消费者消费。建议 Topic 分区数 = 消费线程数(concurrency),我这里设 6 个分区 + 6 个线程,并行消费效率拉满;
- 关闭自动提交 offset:这是避免数据丢失的核心!之前有个项目就是因为开了自动提交,MongoDB 写入超时后数据丢了几十万条,排查了三天才找到问题;
- 批量拉取参数:max-poll-records 设 500 是经过压测的黄金值 —— 太小会导致批量效果差,太大则会占用过多内存,甚至触发 max-poll-interval-ms 超时。
3. 消费者代码实现(批量拉取 + 手动提交)
@Slf4j
@Component
public class KafkaMongoConsumer {
@Autowired
private MongoTemplate mongoTemplate;
@Value("${spring.kafka.listener.concurrency}")
private int concurrency;
private static final String COLLECTION_NAME = "business_data";
// 本地缓存队列,承接批量拉取的数据
private final ConcurrentLinkedQueue<String> dataQueue = new ConcurrentLinkedQueue<>();
// 批量写入阈值,和max-poll-records保持一致
private static final int BATCH_SIZE = 500;
@KafkaListener(topics = "business-data-topic", groupId = "${spring.application.name}")
public void consume(ConsumerRecord<String, String> record, Acknowledgment ack) {
try {
String data = record.value();
if (StringUtils.isBlank(data)) {
log.warn("空消息,offset:{}", record.offset());
ack.acknowledge();
return;
}
// 加入本地缓存队列
dataQueue.offer(data);
// 达到批量阈值,执行写入
if (dataQueue.size() >= BATCH_SIZE) {
batchWriteToMongo();
// 手动提交offset,确认消费成功
ack.acknowledge();
log.info("批量写入成功,提交offset:{},本次写入{}条", record.offset(), BATCH_SIZE);
}
} catch (Exception e) {
log.error("消费失败,offset:{}", record.offset(), e);
// 失败不提交offset,Kafka会重新推送
}
}
// 定时刷盘,避免缓存数据积压(比如最后一批不足500条)
@Scheduled(fixedRate = 5000)
public void flushData() {
if (!dataQueue.isEmpty()) {
batchWriteToMongo();
log.info("定时刷盘,写入{}条数据", dataQueue.size());
}
}
}
四、MongoDB 写入:从「能写」到「快写」的优化
MongoDB 本身性能很强,但高并发下如果配置不当,很容易出现写入超时、连接耗尽的问题。这几个优化点,让 MongoDB 的写入性能直接翻倍:
1. 核心配置优化(application.yml)
spring:
data:
mongodb:
uri: mongodb://carsjoy:carsjoyMongo%40220@172.31.64.13:27021/carsjoy?authSource=carsjoy&maxPoolSize=200&minPoolSize=20&maxIdleTimeMS=300000&writeConcern=MAJORITY
2. 关键配置解读
- 连接池配置:maxPoolSize 设 200(默认 100),保证高并发下有足够连接;minPoolSize 设 20,避免频繁创建连接的开销;maxIdleTimeMS 设 5 分钟,自动回收闲置连接;
- 认证配置:必须指定 authSource(认证库),否则会默认用 admin 库认证,导致连接失败(踩过这个坑的举个手);
- 写入确认级别:writeConcern=MAJORITY,要求副本集多数节点确认写入成功,既保证数据安全,又不会过度影响性能。如果是非核心业务,也可以设为 ACKNOWLEDGED 提升速度。
3. 批量写入代码实现(核心优化!)
MongoDB 的批量写入 API 是性能关键,一定要用insertMany()或bulkWrite(),并设置ordered: false实现并行写入:
private void batchWriteToMongo() {
List<Document> documentList = new ArrayList<>(BATCH_SIZE);
String data;
// 从缓存队列中取出数据
while ((data = dataQueue.poll()) != null) {
Document document = Document.parse(data);
// 可以在这里添加业务字段,比如创建时间
document.put("createTime", new Date());
documentList.add(document);
}
if (documentList.isEmpty()) {
return;
}
try {
// 批量插入,ordered=false并行执行,失败不中断
mongoTemplate.getCollection(COLLECTION_NAME)
.insertMany(documentList, new InsertManyOptions().ordered(false));
} catch (MongoBulkWriteException e) {
// 处理部分写入失败的情况
int successfulCount = e.getWriteResult().getInsertedCount();
int failedCount = documentList.size() - successfulCount;
log.error("批量写入部分失败,成功{}条,失败{}条", successfulCount, failedCount, e);
// 失败的数据可以发送到死信队列
sendToDeadLetterQueue(documentList, e.getWriteErrors());
} catch (Exception e) {
log.error("批量写入失败", e);
// 全部失败,重新放回队列重试
documentList.forEach(doc -> dataQueue.offer(doc.toJson()));
}
}
4. 额外优化技巧(生产必备)
- 索引优化:高频写入的集合,尽量减少索引数量,只保留必要的查询索引。插入前可以先删除索引,插入后再重建,性能提升明显;
- 使用 SSD 存储:MongoDB 写入对 IO 要求高,SSD 的 IOPS 是 HDD 的 10 倍以上,能显著降低写入延迟;
- 分片集群:如果数据量超大(亿级以上),可以给 MongoDB 做分片,按业务键拆分数据,分散写入压力。
五、容错机制:高并发下的数据安全网
高并发场景下,异常是常态,必须做好容错设计,否则一次网络波动就可能导致数据丢失或系统雪崩:
1. 死信队列(DLQ)实现
消费失败的消息(比如数据格式错误、MongoDB 持续超时),不能一直重试,要发送到死信队列单独处理:
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
private static final String DEAD_LETTER_TOPIC = "business-data-dlq";
private void sendToDeadLetterQueue(List<Document> documentList, List<WriteError> errors) {
for (WriteError error : errors) {
int index = error.getIndex();
String failedData = documentList.get(index).toJson();
// 发送到死信队列,并添加错误信息
kafkaTemplate.send(DEAD_LETTER_TOPIC, failedData)
.addCallback(
result -> log.info("死信队列发送成功:{}", failedData),
ex -> log.error("死信队列发送失败", ex)
);
}
}
创建死信队列时,建议和原 Topic 保持相同的分区数,便于问题排查。
2. 幂等性处理
避免重复消费导致的数据重复,在 MongoDB 中给业务唯一键(比如消息 ID)创建唯一索引:
// 初始化时创建唯一索引
@PostConstruct
public void initIndex() {
mongoTemplate.getCollection(COLLECTION_NAME)
.createIndex(Indexes.ascending("messageId"), new IndexOptions().unique(true));
}
这样即使消息重复消费,MongoDB 也会拒绝重复插入,保证数据一致性。
3. 监控告警
一定要配置监控,实时掌握系统状态:
- Kafka:监控消费延迟、积压消息数、消费者在线状态;
- MongoDB:监控写入耗时、连接池使用率、磁盘空间;
- 可以用 Prometheus+Grafana 搭建监控面板,设置阈值告警(比如消费延迟超过 5 分钟就发短信通知)。
六、八年开发踩坑实录(避坑指南)
- 坑 1:Topic 分区数太少之前设了 2 个分区,就算开了 6 个消费线程,也只有 2 个线程在工作,消息堆积严重。后来把分区数改成 6,消费速度直接翻倍。记住:分区数决定了消费端的最大并行度。
- 坑 2:批量太大导致 OOM一开始把 max-poll-records 设为 2000,结果高并发下 JVM 内存飙升,频繁 GC。后来压测发现 500 是黄金值,既能保证批量效果,又不会占用过多内存。
- 坑 3:MongoDB 连接池耗尽没配置 maxPoolSize,默认 100 个连接,高并发下连接耗尽,写入超时。改成 200 后,连接池使用率稳定在 60% 左右,再也没出现超时问题。
- 坑 4:自动提交 offset这个是最致命的坑!一次 MongoDB 集群切换,导致写入失败,但 offset 已经自动提交,几十万条数据丢失。从那以后,所有 Kafka 消费都默认关闭自动提交。
七、最终效果与总结
按上面的方案优化后,系统表现如下:
- 吞吐量:从 3000 TPS 提升到 5 万 + TPS,支撑日均千万级数据插入;
- 稳定性:写入超时率从 30% 降到 0.1% 以下,无数据丢失;
- 可扩展性:通过增加 Kafka 分区和 MongoDB 分片,可轻松支撑更高流量。
总结一下,Kafka+MongoDB 高并发数据插入的核心就三点:
- 批量处理:Kafka 批量拉取 + MongoDB 批量写入,减少 IO 开销;
- 参数匹配:Topic 分区数 = 消费线程数,批量大小适配 MongoDB 承载能力;
- 容错兜底:手动提交 offset + 死信队列 + 幂等性处理,保证数据安全。
这套方案已经在多个生产环境验证过,不管是日志收集、数据同步还是高并发业务写入,都能直接复用。如果你的场景有特殊需求,也可以根据实际情况调整参数。