##缓存请求
只运行一个的demo代码肯定不能掌握这个MQ的精髓,下面结合Springboot运行一个缓存请求,实现一个消峰或者限流的功能。
首先是什么情况下需要借助这个mq来缓存请求实现消峰?肯定是系统平时负载不大,平稳运行,但是偶尔有大量并发请求进来访问某个或者某几个接口,系统负载较大甚至有丢失请求的可能。如果系统一直负载很高,所有的接口都不能及时响应,那么得根据系统瓶颈考虑其他方案。
要想实现消峰,首先应该满足这么一个条件就是业务逻辑响应时间较长,至少得比往队列里面放消息的时间长。否则的话使用队列就没有意义。
我在开发过程中刚好遇到过这么一个场景,就是我的下游系统开放接口接受上系统的数据,数据处理逻辑较为复杂,响应时间超过500ms,平时没啥流量,一道月初月末年初年末有大量请求过来,导致数据会丢失,系统运行也缓慢。
这时MQ就派上用场了,声明一个较长的可以持久化的队列,比如我系统的业务数据最多就50000条,那就声明一个60000的队列,然后接到请求后直接把参数往mq里面放,之后一个个慢慢消费。
限流最典型的场景就是秒杀,大量请求进来,只能有特定数量的能够成功防止超卖,使用mq就比较简单直接声明一个固定长度的队列,或者配置消费者的消费策略,比如一个个消费手动确认。
这里直接使用Spring-amqp,上代码:
1,maven 依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
</dependencies>
2,启动类
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class,args);
}
}
3,配置类
@Configuration
public class PushParamToMqConfig {
public static final String EXCHANGE_NAME_PARAMS = "exchange.params";
//声明一个exchange,Spring会自动调用 channel.exchangeDeclare()
@Bean(name = "pushParamToMqTopicExchange")
public TopicExchange pushParamToMqTopicExchange() {
return ExchangeBuilder
.topicExchange(EXCHANGE_NAME_PARAMS)
.durable(true)
.build();
}
//声明一个队列
@Bean(name = "pushParamToMqQueue")
public Queue pushParamToMqQueue(){
return QueueBuilder.durable().build();
}
@Bean
public Jackson2JsonMessageConverter messageConverter(){
return new Jackson2JsonMessageConverter(); //使用这个消息转换器将消息对象转换成json后getBytes()存入消息代理
//ObjectProvider< MessageConverter > messageConverter; ObjectProvider如果你提供了响应的对象就注入,没有也不会报错。有个默认的值
}
}
4,TestController
@RestController
public class TestController {
@Resource
private RabbitTemplate rabbitTemplate;
@PostMapping("/get")
public String get(@RequestBody TestParam param) throws InterruptedException {
return doGet(param);
}
@PostMapping("/mqGet")
public String mqGet(@RequestBody TestParam param) {
if(param != null){
rabbitTemplate.convertAndSend(PushParamToMqConfig.EXCHANGE_NAME_PARAMS,ROUTING_KEY,param);
}
return "success";
}
public String doGet(TestParam param)throws InterruptedException{
System.out.println(param.getName()+":"+param.getAge());
Thread.sleep(1000);
return "success";
}
public static final String ROUTING_KEY = "com.kuafu.controller.TestController.doGet";
@Resource(name = "pushParamToMqTopicExchange")
private TopicExchange topicExchange;
@Resource(name = "pushParamToMqQueue")
private Queue queue;
@Bean
public Binding binding(){
return BindingBuilder.bind(queue).to(topicExchange).with(ROUTING_KEY); //将exchange、queue和routing key 绑定在一起。
}
int i = 0;
//消费者处理消息
@RabbitListener(queues = "#{pushParamToMqQueue.name}")
public void receive1(TestParam testParam) throws InterruptedException {
System.out.println(++i);
doGet(testParam);
}
}
5,测试结果
springboot内置的tomcat默认两百个线程,在10秒内超过1500个用户访问该接口响应时间明显增长,超过4秒,吞吐量在170/s左右
所谓的tomcat调优,跟系统的配置有绝大关系。由于我的开发环境机器是6核,16G,i7处理器,增加tomcat线程数到500个,系统吞吐量有明显提升。415/s,这里就不去关注开发机器的最佳配置了。
那就使用 500 个线程来测试mq对系统的提升,业务逻辑定为一秒。
500 个线程直接访问的话 最大吞吐量在415/s左右,最大访问人数2500左右,超过2500系统响应时间明显增长。
如果把参数放在mq里面的话,最大访问人数超过4000,最大吞吐量在660/s,
虽然提升没有预想的明显,也有50%左右,但是这个提升跟业务的复杂程度成正比。
这只是最简单的实现,系统还有很多缺陷,后续会一步步优化。
优化1,给连接工厂配置一个名字,这个名字在UI管理界面可以看到方便管理,代码:
@Bean
public SimplePropertyValueConnectionNameStrategy cns() {
return new SimplePropertyValueConnectionNameStrategy("spring.application.name");
}
CachingConnectionFactory factory = new CachingConnectionFactory(connectionFactory);
factory.setConnectionNameStrategy(cns());
优化2,配置推送的重试机制,代码
//重试3次
RetryTemplate retryTemplate = new RetryTemplate();
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
backOffPolicy.setInitialInterval(500);
backOffPolicy.setMultiplier(10.0);
backOffPolicy.setMaxInterval(10000);
retryTemplate.setBackOffPolicy(backOffPolicy);
优化3,springAMQP默认是一步推送消息,并且推送失败的话直接把消息就丢弃了,如何知道推送成功失败呢?配置消息推送的异步回调:
publisher-returns: true #这里是生产者往队列推送消息之后是否需要返回通知。若为false 这个publisher-confirm-type配置为simple
publisher-confirm-type: correlated #开启publisher-confirm,这里支持两种类型:simple: 同步等待confirm结果直到超时;correlated:异步回调,定义ConfirmCallback,MQ返回结果时会回调这个ConfirmCallback
template:mandatory: true
配置好上面三个属性,然后重写这个回调方法
RabbitTemplate template = new RabbitTemplate();
template.setConfirmCallback((correlationData, ack, reason) -> {
if(ack){
//成功推送
System.out.println("comfirmCallback------------success");
if(correlationData != null){
System.out.println(correlationData.getId());
}else{
System.out.println("correlationData is null");
}
}else{
System.out.println("comfirmCallback------------failure");
System.out.println(reason);
}
});
优化4,给生产者配置单独的连接。如果消息代理器内存满了或者其他原因 会自动断开与生产者的连接,如果消费者共用同一个连接的话,会有问题。
rabbitTemplate.setUsePublisherConnection(true);
优化5,手动确认消息,可以全局配置,listener.simple.acknowledge-mode: manual,也可以单独配置 如下:
@RabbitListener(queues = "#{pushParamToMqQueue.name}", ackMode = "MANUAL")
public void manual1(TestParam testParam, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException, InterruptedException {
System.out.println(testParam.getName());
channel.basicAck(tag, false);//逻辑搞完之后再通知 消息队列可以丢弃消息,若指定了ackMode="MANUAL"
}
优化6,给消费者队列起个名字:如果是默认名称的队列,那么每次启动项目都会创建一个新的队列,那么在消费者停机的这段时间的消息不就没有了?
而且队列是持久化,不能会自动删除的,启动了10次项目就有10个持久化的队列,而且这10个队列都跟exchange绑定,发送的消息会被转发到着所有的10个队列
关键还没有消费者绑定它,数据就一直存在。所以这个地方还是得起个名字才合适
public static final String QUEUE_NAME_PARAMS = "queue.params";
@Bean(name = "pushParamToMqQueue")
public Queue pushParamToMqQueue(){
return QueueBuilder.durable(QUEUE_NAME_PARAMS).build();
}
这个东西到这里应该能应付大部分情景了。还有很多没有涉及到的地方,之后再继续吧,欢迎大佬批评指正。