消息队列为什么能扛住高并发
公司上线大促活动,订单系统瞬间被几千个请求砸中。这时候要是直接往数据库写,估计库先崩了。于是我们把订单丢进 Kafka,让下游慢慢消费。但你有没有想过,Kafka 是怎么做到每秒几十万条消息不掉链子的?光看文档说“高性能”“低延迟”,不如直接翻它的源码来得实在。
Producer 发送消息到底发生了什么
Kafka 的生产者不是一调 send() 就立马发出去。它先把消息缓存起来,攒够一批再发,这个叫 batch。源码里有个 RecordAccumulator 类,就是干这活的。它用一个双端队列 deque 存消息,每个分区对应一个 deque,满了就等 sender 线程来取。
Deque<ProducerBatch> dq = accumulator.getOrCreateDeque(topicPartition);这样做的好处是减少网络请求次数。就像快递员不会每收到一个包裹就跑一趟,而是等装满一车再出发。
Broker 如何高效落盘
Kafka 把消息写入磁盘,却比很多内存队列还快。秘密在于它不用 Java 堆内存做中转,而是用了操作系统的页缓存(Page Cache)。Producer 发来的数据直接通过 mmap 写入文件系统缓存,连序列化都尽量前置。
在 LogSegment 类里能看到,消息追加到文件时用的是 FileChannel.transferFrom,底层走零拷贝。这意味着数据从网卡到磁盘,中间几乎不复制,CPU 负担小多了。
fileChannel.transferFrom(transferableChannel, pos, size);Consumer 拉取消息的机制
消费者不是被动接收,而是主动向 Broker 发 fetch 请求。每次带上自己上次消费的 offset,Broker 找到对应位置返回一批数据。这种模式叫 pull,和 RabbitMQ 的 push 不一样。
在 Fetcher 类里,有一个 processCompletedFetch 方法,专门处理拉回来的数据。它会解析消息格式,更新本地 offset,再交给用户写的回调函数处理。如果网络断了,Consumer 只要重试,接着上次的位置继续拉,不会丢也不会重复。
元数据同步靠 ZooKeeper 吗
老版本 Kafka 用 ZooKeeper 管集群元数据,比如哪个 broker 挂了、leader 分区切换。但从 2.8 开始推 KRaft 模式,用内部协议替代 ZooKeeper。Controller 组件通过投票选出,管理分区状态变更。
在源码中可以看到 QuorumController 类,它实现了 Raft 协议的核心逻辑。这样一来,部署更轻量,启动更快,也减少了外部依赖出问题的风险。
从源码中学到的实用技巧
平时写业务代码,也可以借鉴这些思路。比如接口批量导入 Excel 数据,别一条条 insert,学 Kafka 攒 batch 走批量插入;日志写入别每次都刷磁盘,可以用缓冲+定时 flush;系统间通信别直连数据库,加个消息队列解耦。
看源码不是为了背代码,而是理解别人怎么解决实际问题。当你下次设计系统时,脑子里多几个方案,自然就能选最合适的那一个。