一、从一个烦恼说起
很多做后端开发的朋友,第一次接触消息队列的时候,脑子里都是问号:我明明能用接口直接调用,为什么非要在中间塞一个Kafka?我当年也觉得这玩意是自找麻烦。后来遇到一个场景:用户下单后,要扣库存、发短信、送积分、更新报表,这些操作如果全部在同一个请求里同步做完,接口动不动就卡几秒。用户体验差不说,只要有一个环节出问题,整个订单都得回滚。后来我把这些操作改成发消息,让订单服务只专心写订单,其他事情让下游服务自己去订阅处理,接口瞬间就快多了。这就是消息队列最直观的价值:把耦合拆开,把速度提上去。而Kafka,正是这类场景里最常用的选手之一。
二、先认识一下Kafka
Kafka是一个分布式的消息流平台。你可以把它想象成一个超级大的邮局。你往邮局里投信,邮局把信分门别类放好,收信的人随时去取。Kafka里,投信的人叫“生产者”,收信的人叫“消费者”。存放信件的“信箱”叫主题,一个主题可以拆成多个分区,分区越多,同一封邮件被不同的人同时取的能力就越强。消费者还可以组成“消费者组”,组里的每个人分工取不同分区的信,谁也不会取重。
2.1 为什么它能扛住高吞吐
Kafka的高吞吐来源于两点:第一是顺序写磁盘,硬盘顺序写比随机写快得多;第二是批量处理,生产者攒一批消息再发送,消费者拉一批消息再处理。就像快递小哥不会一趟只送一个包裹,而是攒一车再出发。正因为这样,它能轻松做到每秒处理几十万条消息,这在很多业务系统里完全够用了。
三、Spring Boot项目怎么接上Kafka
下面我们用Java技术栈,基于Spring Boot 2.7和Spring Kafka来演示。如果你用的是Spring Boot 3.x,依赖和配置也差不多。
3.1 引入依赖
在pom.xml里加上Spring Kafka的依赖即可:
<!-- pom.xml 中引入 Spring Kafka 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
3.2 配置连接信息
在配置文件里告诉Spring Boot Kafka服务在哪,以及用什么序列化器处理消息的键和值:
spring:
kafka:
bootstrap-servers: 127.0.0.1:9092 # Kafka服务器地址
producer:
acks: all # 所有副本确认后才算发送成功
retries: 3 # 发送失败重试3次
batch-size: 16384 # 批量发送的字节数
linger-ms: 5 # 最多等5毫秒,凑一批再发
buffer-memory: 33554432 # 发送缓冲内存
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
consumer:
group-id: order-group # 默认消费者组
enable-auto-commit: false # 关闭自动提交偏移量,改手动
auto-offset-reset: latest # 没有偏移量时从最新位置开始读
max-poll-records: 500 # 一次拉取最多500条
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
listener:
ack-mode: manual_immediate # 手动确认模式
3.3 写一个最简单的生产者
用Spring Kafka提供的发送模板来发消息,代码很简单:
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
@Service
public class OrderProducer {
private final KafkaTemplate<String, String> kafkaTemplate;
public OrderProducer(KafkaTemplate<String, String> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
/**
* 发送订单消息
*
* @param orderId 订单号,也作为消息的key
*/
public void sendOrder(String orderId) {
// 构造消息内容,此处用简单字符串,实际业务中通常放JSON
String message = "{\"orderId\": \"" + orderId + "\", \"amount\": 99.5}";
// 发送到order-topic,key设为订单号,方便后续按订单号分区
kafkaTemplate.send("order-topic", orderId, message);
System.out.println("订单消息已发送:" + message);
}
}
3.4 写一个最简单的消费者
用监听器注解来订阅主题,并手动确认消息已经处理完:
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Component;
@Component
public class OrderConsumer {
/**
* 监听order-topic,手动确认消息
*
* @param record 原始消息
* @param ack 确认对象,调用acknowledge()后才会提交偏移量
*/
@KafkaListener(topics = "order-topic", groupId = "order-group")
public void onOrder(ConsumerRecord<String, String> record, Acknowledgment ack) {
try {
System.out.println("收到订单消息:" + record.value());
// 这里模拟业务处理:比如更新库存、发短信、送积分
System.out.println("订单处理中:" + record.key());
// 业务处理成功后,手动确认消息已消费
ack.acknowledge();
System.out.println("订单处理完成:" + record.key());
} catch (Exception e) {
// 处理失败时不要提交确认,消息还会再次拉到
System.err.println("订单处理失败:" + record.key());
System.err.println("失败原因:" + e.getMessage());
}
}
}
这样一套走下来,一个消息从生产者发到Kafka,再由消费者拉出来处理,就全通了。
四、把吞吐量调上去的关键配置
很多人在Spring Boot里接上Kafka后,发几百条消息看不出问题,但一旦压到每秒几千条,性能就崩了。这时候就要做配置调优。
4.1 生产者端调优
生产者最影响吞吐的有三个配置:批量发送的字节数、最多等待时间、发送缓冲区大小。你可以把它们调大,比如一次攒到三万多字节再发,最多等十毫秒凑一批,这样每条消息不用立刻发,而是攒一批再发,网络请求次数大大减少。但注意,等待时间太长,消息延迟会增加;批次太大,内存占用也会高。要根据业务情况取舍。
4.2 消费者端调优
消费者端有两点关键:并发度和批量拉取量。监听器注解可以指定并发数,比如并发数为3,表示用3个线程一起消费这个主题,处理速度能上三倍。同时一次拉取的最大条数也可以调大,比如一千条,减少网络往返。不过要小心,如果单条消息处理很慢,拉太多也处理不完,还可能在超时前没确认,导致重复消费。
4.3 分区与消费者组
消费者组里每个人负责一个或多个分区。假设一个主题有6个分区,消费者组里有3个消费者,那每个消费者可以分到2个分区。如果你组的消费者数量超过分区数,多出来的消费者就闲着,浪费了资源。所以调优时要先想清楚分区数。分区数在创建主题时确定,太少会限制并发,太多则会有额外的文件开销。一般建议分区数是消费者数的整数倍,这样能均匀分配。
五、可靠性保障要怎么做
高吞吐和可靠,有时候是矛盾的。比如为了快,可以不等确认就发下一条,但可能丢消息。可靠性的本质,是找到平衡点。
5.1 消息别丢
生产端要把确认模式设置为全部副本都写成功,并且打开重试开关。配置里我们已经做了。消费者端要关闭自动提交偏移量,改成手动确认。就像签收快递一样,你确认处理好了,再告诉Kafka可以删掉记录了。如果没确认,Kafka会认为你没收到,下次还会发给你。
5.2 消息别重
手动确认有个麻烦:你处理完业务,还没来得及确认,程序就挂了。恢复后Kafka会再发一次这条消息,造成重复。解决重复的关键是“幂等”。比如用订单号作为唯一键,在数据库里建一张处理记录表。消费时先查一下这个订单号有没有处理过,处理过就跳过。下面是个简单的幂等检查示例:
import org.springframework.stereotype.Service;
@Service
public class OrderProcessedService {
/**
* 模拟检查某个订单是否已处理。
* 实际项目中这里会查数据库或Redis。
*/
public boolean isProcessed(String orderId) {
// 比如查Redis:return redisTemplate.hasKey("processed:" + orderId);
return false;
}
/**
* 把订单标记为已处理
*/
public void markProcessed(String orderId) {
// 比如写Redis:redisTemplate.opsForValue().set("processed:" + orderId, "1");
System.out.println("标记订单已处理:" + orderId);
}
}
然后在消费者里这样用:
// 伪代码形式演示,展示幂等逻辑
if (orderProcessedService.isProcessed(record.key())) {
// 已经处理过了,直接确认,不再重复执行业务
ack.acknowledge();
return;
}
// 真正的业务逻辑
orderProcessedService.markProcessed(record.key());
// 处理完再确认
ack.acknowledge();
这样一来,即使同一个订单被重复投递,第二次进来也能被拦住。
5.3 消息别乱
Kafka只能保证同一个分区内的消息有序。所以如果你要求订单消息严格按顺序处理,那就要用同一个键,比如订单号作为键,这样同一个订单的多个操作会进同一个分区。跨分区的全局顺序是做不到的,但大多数业务只需要分区有序就够了。比如同一个用户的操作,用用户ID当键,这个用户的事件就都在一个分区里了。
5.4 关联技术:重试机制
如果消费失败,除了放弃,最好做重试。Spring Retry是个好帮手,可以配置重试次数和退避策略。集成方式:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.retry.support.RetryTemplate;
@Configuration
@EnableRetry
public class RetryConfig {
@Bean
public RetryTemplate retryTemplate() {
// 创建一个重试模板:最多重试3次,每次间隔1秒后翻倍
RetryTemplate template = RetryTemplate.builder()
.maxAttempts(3)
.exponentialBackoff(1000, 2.0, 5000)
.build();
return template;
}
}
注意,这里只是示例,实际使用时需要引入相关依赖,并在消费逻辑里调用重试模板的execute方法。如果你不想引入额外依赖,自己写一个for循环也能实现延迟重试,但要注意别把重试时间卡得太长,不然会拖累消费者线程。
六、注意事项与常见坑
第一,千万别对Kafka里的消息做“发送后不管”。一定要关注发送回调,发送失败要落到日志或数据库,再异步补偿。
第二,消费者处理时间不要超过默认的拉取会话超时时间。如果你处理一条消息要十分钟,Kafka会认为你死了,把分区踢出去再重平衡,导致其他消费者继续消费。所以复杂任务要么拆小,要么单独起线程。
第三,消费组里的消费者数量变化,会触发重平衡,重平衡期间该组无法消费消息。尽量减少频繁增减消费者。
第四,序列化要统一。如果用字符串序列化器,消息里放文本格式就行。如果将来要放对象,最好用JSON序列化器,但一定注意兼容性,别换来换去导致旧数据读不了。
第五,不要只看发送端吞吐,消费端处理能力跟不上也会积压消息。这时可以加分区、加消费者,或者优化业务逻辑。
第六,Kafka不是万能的。有些场景用Kafka并不合适,比如需要精确到毫秒的请求响应,或者消息量很小,完全可以走同步调用。
七、应用场景与优缺点
7.1 适合做什么
Kafka最擅长的是大数据量、高写入吞吐的场景。比如用户行为日志采集、小程序埋点、订单事件分发、数据库同步(用CDC工具把变更日志发到Kafka)、流式计算等。这些场景共同特点是:消息量大,允许一定的延迟,需要削峰填谷。比如大促期间有大量订单涌入,如果大家直接去写数据库,数据库会炸。先写Kafka,再让服务按自己的速度慢慢处理,这就是削峰填谷。
7.2 不适合做什么
Kafka不适合做需要强一致性的分布式事务。它只能保证最终一致,没有事务回滚机制,不能像数据库那样提供强一致。另外它也不适合低延迟的对话式交互,比如你在浏览器里点一个按钮,等服务端马上返回结果,那是同步接口的事,不是消息队列的事。
7.3 优点和缺点
优点很突出:吞吐高,扩展容易,可以持久化保存消息,消费者宕机恢复后还能接着读。生态特别丰富,很多大数据计算框架都愿意接Kafka。缺点也同样明显:运维重,部署一个集群要点不少精力;分区数量一旦定好,后面想改很麻烦;而且如果不设置合理参数,很容易产生重复消息或乱序。总之,它是一个技术含量很高的中间件,需要认真对待。
八、总结
Spring Boot和Kafka配合,是现在后端系统里很常见的一套组合。用Spring Boot只需要写几行代码就能把生产者、消费者跑起来,但真正上线的时候,要花心思调整参数来追求高吞吐,要用手动确认和幂等来保证可靠,还要知道有哪些坑。消息队列不是银弹,它解决了解耦、削峰、异步的问题,也引入了数据一致性、重试、运维的新挑战。希望这篇文章能帮你把基础概念理清楚,在自己项目里少走弯路。试着从一个小场景开始,比如订单消息推送,你会慢慢感受到它的魅力的。
评论
围绕“Spring Boot与Kafka整合:高吞吐量消息处理的配置与可靠性保障”参与讨论