4、Kafka深度实践:分区机制、副本机制、生产者与消费者API、Exactly-Once语义
各位同学,今天我们来啃一块硬骨头——Kafka。说实话,很多做风控的同学对Kafka的理解停留在“能发消息、能收消息”的层面。但真正到了高并发、高可用的风控场景,你会发现,不懂分区和副本,系统随时可能崩给你看。
我个人习惯把Kafka比作一个快递分拣中心。分区就是不同的分拣口,副本就是每个分拣口的备用人员。今天咱们就把这几个核心机制掰开揉碎了讲清楚。
核心要点:Kafka的吞吐量和可靠性,完全取决于你对分区和副本的理解深度。
4.1 分区机制:并行度的基石
分区是什么?说白了,就是一个Topic被切成了多个小队列。每个分区内部是有序的,但分区之间不保证顺序。
我在项目中遇到过一个问题:风控事件必须按用户ID顺序处理,否则会出大乱子。比如用户先下单、后支付,如果顺序乱了,风控规则就会误判。这时候,分区策略就至关重要。
4.1.1 分区策略
Kafka默认的分区策略是轮询(Round-Robin),消息均匀分布到各个分区。但风控场景下,我们通常需要按Key分区。
// 自定义分区器示例
public class RiskPartitioner implements Partitioner {
@Override
public int partition(String topic, Object key, byte[] keyBytes,
Object value, byte[] valueBytes, Cluster cluster) {
// 按用户ID哈希,保证同一用户的消息进入同一分区
String userId = (String) key;
int partitionCount = cluster.partitionCountForTopic(topic);
return Math.abs(userId.hashCode()) % partitionCount;
}
}
嗯,这里要注意:分区数一旦确定,只能增加,不能减少。我见过有人上线后想缩容分区,结果数据全乱了。所以,上线前一定要评估好流量峰值。
避坑指南:我曾经在一个日处理10亿条消息的项目中,把分区数设成了64。后来发现,分区太多导致文件句柄耗尽。建议分区数不超过Broker数量的10倍。
4.1.2 分区与消费者组的关系
消费者组里的每个消费者,会负责一个或多个分区。一个分区只能被组内的一个消费者消费。这就是Kafka实现并行消费的核心机制。
你想想看,如果消费者数量大于分区数,多余的消费者就会闲置。所以,消费者数量最好等于分区数,这样效率最高。
4.2 副本机制:高可用的保障
副本机制,说白了就是给数据做备份。每个分区可以有多个副本,分布在不同的Broker上。副本分为Leader和Follower。
- Leader副本:负责读写请求,是真正的“干活的人”
- Follower副本:只负责同步数据,是“备胎”
为什么这样设计?因为如果所有副本都处理读写,数据一致性就难以保证。Leader统一处理,Follower同步,简单又可靠。
4.2.1 ISR机制
ISR(In-Sync Replicas)是Kafka保证数据不丢的核心。只有与Leader保持同步的副本,才会被加入ISR列表。
我记得有一次线上故障:某个Broker磁盘IO飙升,导致Follower同步延迟。Leader等了一会儿,直接把那个Follower踢出了ISR。这时候,如果Leader挂了,只有ISR里的副本才能接替成为新Leader。
重要提醒:如果所有副本都挂了,Kafka会等待ISR中最后一个副本恢复。如果这个副本也丢了数据,那数据就永久丢失了。所以,副本数至少设为3,并且确保Broker分布在不同的物理机上。
4.2.2 ACK参数详解
生产者发送消息时,可以通过acks参数控制可靠性级别:
| acks值 | 含义 | 可靠性 | 性能 |
|---|---|---|---|
| 0 | 不等待确认 | 最低 | 最高 |
| 1 | Leader确认即可 | 中等 | 中等 |
| all | 所有ISR副本确认 | 最高 | 最低 |
风控场景下,我建议用acks=all。虽然性能会下降一些,但数据绝对不能丢。你想想看,如果风控规则判断“允许交易”,结果这条消息丢了,用户重复支付了,那责任谁来担?
4.3 生产者与消费者API实战
光说不练假把式。咱们直接上代码。
4.3.1 生产者API
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("acks", "all");
props.put("retries", 3);
props.put("batch.size", 16384);
props.put("linger.ms", 5);
Producer<String, String> producer = new KafkaProducer<>(props);
// 发送风控事件
producer.send(new ProducerRecord<>("risk-events", userId, eventJson),
(metadata, exception) -> {
if (exception != null) {
// 记录失败日志,后续补偿
log.error("发送失败: {}", exception.getMessage());
}
});
这里有个细节:batch.size和linger.ms要配合使用。batch.size是批次大小,linger.ms是等待时间。如果消息量不大,建议设小一点,避免延迟过高。
4.3.2 消费者API
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "risk-consumer-group");
props.put("enable.auto.commit", "false");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("risk-events"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
// 处理风控事件
processRiskEvent(record.value());
}
// 手动提交偏移量
consumer.commitSync();
}
嗯,这里要注意:一定要手动提交偏移量。自动提交虽然省事,但万一处理到一半挂了,重启后会重复消费或丢失数据。
个人经验:我曾经用自动提交,结果一次Full GC导致处理超时,偏移量自动提交了,但数据没处理完。重启后,那批数据就丢了。从那以后,我再也不敢用自动提交了。
4.4 Exactly-Once语义
这是Kafka最硬核的部分。Exactly-Once,说白了就是“每条消息只被处理一次,不多不少”。
为什么风控场景需要这个?因为重复处理可能导致重复扣款,漏处理可能导致风控失效。在金融场景下,这都不是小事。
4.4.1 幂等生产者
Kafka从0.11版本开始支持幂等生产者。开启后,Kafka会为每条消息生成一个唯一的序列号,即使生产者重试,Broker也能去重。
props.put("enable.idempotence", "true");
props.put("acks", "all");
props.put("max.in.retries", Integer.MAX_VALUE);
注意:开启幂等后,acks必须设为all,否则会报错。
4.4.2 事务性消息
如果生产者需要跨多个分区或Topic保证原子性,就需要用到事务。
producer.initTransactions();
try {
producer.beginTransaction();
producer.send(new ProducerRecord<>("topic1", "key1", "value1"));
producer.send(new ProducerRecord<>("topic2", "key2", "value2"));
producer.commitTransaction();
} catch (Exception e) {
producer.abortTransaction();
}
我建议在风控场景中,事务只用于关键链路。因为事务会带来额外的性能开销,如果所有消息都用事务,吞吐量会下降30%以上。
4.4.3 消费者端Exactly-Once
消费者端实现Exactly-Once,需要将消费结果和偏移量存储在同一个事务中。Kafka提供了读-处理-写模式:
consumer.subscribe(Arrays.asList("risk-events"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
producer.beginTransaction();
for (ConsumerRecord<String, String> record : records) {
// 处理结果写入输出Topic
producer.send(new ProducerRecord<>("risk-results", process(record.value())));
}
// 将偏移量发送到内部Topic
producer.sendOffsetsToTransaction(
getOffsets(records), consumer.groupMetadata().groupId());
producer.commitTransaction();
}
避坑指南:我曾经在生产环境中遇到一个问题:事务超时导致消费者卡死。后来发现是transaction.timeout.ms设得太短。建议设为60秒以上,给处理逻辑留足时间。
4.5 知识体系总览
下面这张图,是我自己总结的Kafka核心知识体系。建议你保存下来,每次遇到问题都对照着看一遍。
这张图把今天讲的内容串起来了。你仔细看,分区、副本、生产者、消费者这四个模块,最终都指向了Exactly-Once语义。这就是Kafka在风控场景下的核心价值——既要快,又要准,还要稳。
好了,今天的内容就到这里。记住一句话:Kafka不是万能的,但不懂Kafka的风控架构师是万万不能的。下次遇到性能问题,先看看分区数够不够;遇到数据丢失,先查查副本和ACK配置。这些基本功,比什么花哨的框架都管用。
公众号:蓝海资料掘金营,微信deep3321