消息队列削峰和分发

削峰演示场景

一个模块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模块的维护者就需要升级,并且这里我们为了简单起见,模块都在同一个服务内,事实上,在生产环境生产端和消费端基本都是不同的服务,甚至是不同的团队在维护。

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

相关推荐
Devin~Y2 天前
大厂Java面试实战:Spring Boot + Redis + Kafka + Kubernetes + RAG 的三轮追问(附答案解析)
java·spring boot·redis·spring cloud·kafka·kubernetes·resilience4j
Devin~Y3 天前
大厂Java面试实战:Spring Boot/Cloud + Redis/Kafka + K8s + RAG/Agent 追问全流程(小Y翻车记)
java·spring boot·redis·spring cloud·kafka·kubernetes·micrometer
Devin~Y3 天前
大厂Java面试实录:Spring Boot/Cloud、Kafka、Redis、K8s 与 Spring AI(RAG/Agent)三轮连环问
java·spring boot·redis·mysql·spring cloud·kafka·kubernetes
frankfishinwater4 天前
Kafka 代码架构分析
分布式·架构·kafka
隔壁寝室老吴5 天前
使用Flink2.0消费低版本的Kafka
分布式·kafka
indexsunny5 天前
互联网大厂Java面试实战:Spring Boot微服务与Kafka消息队列深度解析
java·spring boot·微服务·面试·kafka·消息队列·电商
富士康质检员张全蛋5 天前
Kafka架构 主题中的分区
分布式·kafka
富士康质检员张全蛋5 天前
kafka 环境部署
分布式·kafka
富士康质检员张全蛋5 天前
Kafka架构 Kafka核心概念
kafka
FL4m3Y4n5 天前
分布式消息推送系统协议设计【C++ grpc kafka】
c++·分布式·kafka