前阵子排查一个消费者 rebalance 的问题,翻到 Kafka 源码里去了。本来只想看看协调器怎么工作的,结果发现 Kafka 的设计模式用得相当精妙——不是教科书式的"定义接口→实现类→调用",而是把多个模式揉在一起解决真实问题。
很多人学设计模式停留在"观察者就是 EventBus"这个层面,但 Kafka 的观察者模式跟教科书完全不是一个物种。今天拆几个 Kafka 源码里最有意思的设计模式,看完你可能得重新审视自己写的代码。
观察者模式:Consumer Group 的协调机制
教科书里的观察者模式:一个 Subject,N 个 Observer,状态变了通知一下。完事。
Kafka 的消费者组协调机制也是观察者,但它解决的问题复杂得多:
- 不是简单的"状态变了通知一下",而是"状态变了之后,所有人要协商出一个新的一致状态"
- 观察者(消费者)可以随时加入和离开
- 通知不是一次性的,而是一个多轮的 rebalance 协议
// Kafka 源码:ConsumerCoordinator 的协调逻辑
private void onJoinComplete(int generation, String memberId, String assignmentStrategy, ByteBuffer assignment) {
// rebalance 完成后的回调——每个消费者拿到自己的分区分配
// 这不是简单的 notify,而是一个分布式协商的终点
}
关键点:Kafka 的协调器(GroupCoordinator)不是简单广播状态,而是通过 JoinGroupRequest → SyncGroupRequest 两轮交互完成一次 rebalance。第一轮选 leader,第二轮由 leader 做分区分配,再把结果分发下去。
你写的观察者模式是"通知一下",Kafka 的观察者模式是"通知→协商→确认→生效"。这个差距不是代码量的差距,是问题域的差距。
策略模式:分区分配策略的可插拔设计
Kafka 的分区分配策略是个教科书级的策略模式,但它有一个很多人忽略的设计决策:默认不是 RoundRobin。
// PartitionAssignor 接口——策略模式的抽象
public interface PartitionAssignor {
// 每个策略实现自己的分配算法
Map<String, Assignment> assign(Cluster metadata, Map<String, Subscription> subscriptions);
}
// 三种内置策略
class RangeAssignor implements PartitionAssignor { ... } // 默认策略
class RoundRobinAssignor implements PartitionAssignor { ... }
class StickyAssignor implements PartitionAssignor { ... }
踩过一个坑:用 RoundRobin 分配策略时,消费者数变化会导致几乎所有分区重新分配。换成 StickyAssignor 后,只有必要的分区会移动,消费者缓存命中率直接上去。
这个案例说明一个策略模式的选型问题:策略不能只看"对不对",要看"切换代价大不大"。RoundRobin 逻辑没错,但在消费者频繁上下线的场景下,每次 rebalance 的代价太高。
责任链模式:Producer 的 Interceptor 链
很多人不知道 Kafka Producer 有拦截器机制,因为它默认是空的。但这个设计非常典型:
// Producer 拦截器接口
public interface ProducerInterceptor<K, V> extends Configurable {
ProducerRecord<K, V> onSend(ProducerRecord<K, V> record); // 发送前
void onAcknowledgement(RecordMetadata metadata, Exception e); // 收到 ack 后
void close();
}
// KafkaProducer 中的调用
private RecordAccumulator recordAccumulator;
private List<ProducerInterceptor<K, V>> interceptors;
// 发送时经过拦截器链
private Future<RecordMetadata> doSend(ProducerRecord<K, V> record) {
// 拦截器链处理
ProducerRecord<K, V> interceptedRecord = interceptors == null ?
record : this.interceptors.onSend(record);
// ... 后续发送逻辑
}
跟 Servlet Filter 的区别在于:Kafka 的拦截器只在两个钩子点生效(onSend 和 onAcknowledgement),不像 Filter 可以拦截整个请求链。这个取舍是有意的——Kafka 追求吞吐量,拦截器链越短越好。
实际用法:我们用拦截器做了两件事:
- 在 onSend 里给消息打时间戳和链路追踪 ID
- 在 onAcknowledgement 里统计发送成功率和延迟
这两个功能加起来只增加了不到 1% 的延迟,但监控粒度提升了几个量级。
模板方法模式:KafkaProducer 的发送流程
KafkaProducer.send() 方法是模板方法模式的经典实现,只是它没用抽象类,而是用一个固定流程 + 多个可替换组件:
public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
// 1. 拦截器处理(可选)
ProducerRecord<K, V> interceptedRecord = interceptors.onSend(record);
// 2. 序列化(Serializer 可替换)
byte[] serializedKey = keySerializer.serialize(topic, interceptedRecord.headers(), interceptedRecord.key());
// 3. 分区选择(Partitioner 可替换)
int partition = partition(record, serializedKey, serializedValue, cluster);
// 4. 追加到 RecordAccumulator
RecordAccumulator.RecordAppendResult result = accumulator.append(...);
// 5. 唤醒 Sender 线程
if (result.batchIsFull || result.newBatchCreated) {
this.sender.wakeup();
}
return result.future;
}
整个流程是固定的(序列化→分区→攒批→发送),但每一步的具体实现都可以替换:Serializer、Partitioner、Interceptor 都是可插拔的。这比你在代码里写个 abstract class 再搞几个子类要灵活得多。
工厂模式 + 门面模式:Kafka 的客户端创建
创建一个 KafkaProducer 要做的事很多:配 NetworkClient、配 Serializer、配 RecordAccumulator、配 Sender 线程……如果让用户自己组装,90% 的人会配错。
// 用户只需要这样
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
// KafkaProducer 构造方法里做了大量组装工作
KafkaProducer(Map<String, Object> configs, Serializer<K> keySerializer, Serializer<V> valueSerializer) {
this.producerConfig = new ProducerConfig(configs);
// ... 组装几十个组件
this.metrics = new Metrics();
this.serializer = ...;
this.partitioner = ...;
this.recordAccumulator = new RecordAccumulator(...);
this.sender = new Sender(...);
this.ioThread = new KafkaThread("kafka-producer-network-thread", this.sender, true);
this.ioThread.start();
}
这就是门面模式:用户只需要传一个 Properties,Kafka 在内部组装好所有组件。同时,Serializer、Partitioner 这些组件通过配置动态创建,这就是工厂模式的变体——配置驱动创建。
总结一下关键洞察
-
观察者模式不只是通知,还可以是协商。Kafka 的 rebalance 协议比简单的 notify 复杂一个量级,但也解决了一个量级更难的问题。
-
策略模式的选型要看切换代价。RoundRobin vs Sticky 不是一个"对不对"的问题,而是"改一次的代价大不大"的问题。
-
责任链不是越灵活越好。Kafka 的拦截器只有两个钩子,刻意限制了灵活性,换取了低延迟。设计模式的运用要跟性能目标对齐。
-
模板方法不用抽象类也能做。KafkaProducer 的 send() 是固定流程 + 可插拔组件,比继承体系更灵活。
-
工厂 + 门面是复杂对象创建的标准解法。当你有几十个组件要组装时,别让用户自己 new。
这些模式在 GoF 书里都有,但 Kafka 的实现方式跟教科书不一样——教科书教你的是结构,Kafka 展示的是取舍。结构容易学,取舍需要踩坑。
我在做一个用卡皮巴拉讲设计模式的微信小程序「爪爪代码冒险记」,23 个设计模式用漫画 + 答题的方式讲,感兴趣可以搜一下,后续每篇文章我也会同步对应的小程序内容。