削峰演示场景
一个模块A接受一波流量,收到之后,立刻调用另一个模块B,那么此时B承担了基本相同的流程。举个例子,模块A一瞬间收到100个请求,那么B基本也是一瞬间收到100个请求,如果B的承压能力非常差,或者B有什么资源限制,那么这100个请求下来,B可能就挂了或者报错了。
面对这种下游扛不住的场景,我们还可以有第二种流程:模块A不用一次性把消息打给B,而是只用将信息传递到一个中转站,B按自身的消费能力从中转站拉取消息,再自己去做就可以了,这个中转站,就是消息队列,可以起到削峰的作用。
在之前给的代码其实已经实现了削峰的功能。在 Spring Kafka 中,起到削峰作用的其实是隐藏的默认配置:
-
单线程消费 :默认情况下,
@KafkaListener是单线程执行的。这意味着无论 A 塞得多快,B 都是一个一个按顺序做,数据库永远不会过载。 -
并发控制 :如果想让 B 快一点,但又怕把 MySQL 压垮,可以配置**
concurrency** 参数。比如设置concurrency = 3,就相当于给 B 开了 3 个窗口
java
// 示例:设置并发数为 3,既加快了处理速度,又保证了 MySQL 不会因为瞬时 100 个请求而挂掉
@KafkaListener(topics = "decoupling-topic", concurrency = "3")
public void listen(String message) {
countService.doHeavyWork();
}
最理想的状态:线程数concurrency = 分区数partition
总结:消息队列削峰,一般解决的是突发流量激增额情况。如果是流量持续增大,并且单机性能优化达到极致,那么只能水平扩容。削峰,也可以理解为在解耦的基础上,进一步考虑了流量激增的情况;而解耦更侧重考虑整体的相应时间
分发演示场景
一个模块A需要发送同一条消息给模块B,C,D
先来看一下使用消息队列进行分发前的代码是怎么样的:
java
@PostMapping(value = "/dispatch", consumes = "application/json; charset=utf-8")
public ResponseEntity<String> dispatch(@RequestBody IncrCountReq dat) {
String msg = "to the moon!";
countService.msgAll("svr1", msg);
countService2.msgAll("svr2", msg);
countService3.msgAll("svr3", msg);
return ResponseEntity.ok().build();
}
public void msgAll(String svrName, String msg) {
System.out.println("svr " + svrName + " received msg : " + msg);
}
svr1,svr2,svr3就可以看作B,C,D三个模块。结果如下:

功能上没有任何问题,但是这样3个模块,我就得写三个对应的代码。那如果20个模块要感知这条消息,我们就需要写20个。所以我们用消息队列进行分发:
java
@PostMapping(value = "/dispatch_with_mq", consumes = "application/json"; charset=utf-8")
public ResponseEntity<String> dispatchWithMQ(@RequestBody IncrCountReq dat) {
String msg = "to the moon!";
kafkaTemplate.send("tp-mq-dispatch", msg);
return ResponseEntity.ok().build();
}
对应的要有消费者逻辑:
java
@KafkaListener(topics = "tp-mq-dispatch", groupId = "TEST_GROUP1")
public void dispatchForSvr1(ConsumerRecord<?, ?> record, Acknowledgment ack) {
Optional<?> message = Optional.ofNullable(record.value());
if (message.isPresent()) {
Object msg = message.get();
String topic = record.topic();
System.out.println("svr1 收到Kafka消息! Topic:" + topic + ", Message:" + msg);
try {
countService.incrManyTimes(10000);
ack.acknowledge();
log.info("Kafka消费成功! Topic:" + topic + ", Message:" + msg);
} catch (Exception e) {
e.printStackTrace();
log.error("Kafka消费失败! Topic:" + topic + ", Message:" + msg);
}
}
}
@KafkaListener(topics = "tp-mq-dispatch", groupId = "TEST_GROUP2")
public void dispatchForSvr2(ConsumerRecord<?, ?> record, Acknowledgment ack) {
Optional<?> message = Optional.ofNullable(record.value());
if (message.isPresent()) {
Object msg = message.get();
String topic = record.topic();
System.out.println("svr2 收到Kafka消息! Topic:" + topic + ", Message:" + msg);
try {
countService.incrManyTimes(10000);
ack.acknowledge();
log.info("Kafka消费成功! Topic:" + topic + ", Message:" + msg);
} catch (Exception e) {
e.printStackTrace();
log.error("Kafka消费失败! Topic:" + topic + ", Message:" + msg);
}
}
}
@KafkaListener(topics = "tp-mq-dispatch", groupId = "TEST_GROUP3")
public void dispatchForSvr3(ConsumerRecord<?, ?> record, Acknowledgment ack) {
Optional<?> message = Optional.ofNullable(record.value());
if (message.isPresent()) {
Object msg = message.get();
String topic = record.topic();
System.out.println("svr3 收到Kafka消息! Topic:" + topic + ", Message:" + msg);
try {
countService.incrManyTimes(10000);
ack.acknowledge();
log.info("Kafka消费成功! Topic:" + topic + ", Message:" + msg);
} catch (Exception e) {
e.printStackTrace();
log.error("Kafka消费失败! Topic:" + topic + ", Message:" + msg);
}
}
}
看到这里可能会问,消费者代码不也是不断在增加么?对,但是这是消费端的事情,A模块的维护者就需要升级,并且这里我们为了简单起见,模块都在同一个服务内,事实上,在生产环境生产端和消费端基本都是不同的服务,甚至是不同的团队在维护。

总结: 消息队列分发,既然是分发,一般就是发给多个消费者,而且发完之后啥也不管,就是解耦