Rabbitmq(2)

##缓存请求

只运行一个的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();
}



这个东西到这里应该能应付大部分情景了。还有很多没有涉及到的地方,之后再继续吧,欢迎大佬批评指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lx18854869896

和谐社会靠你了,老铁...

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值