RabbitMQ如何削峰限流

https://www.jianshu.com/p/7b0a8685c24b

MQ的作用

1)解耦:在项目启动之初是很难预测未来会遇到什么困难的,消息中间件在处理过程中插入了一个隐含的,基于数据的接口层,两边都实现这个接口,这样就允许独立的修改或者扩展两边的处理过程,只要两边遵守相同的接口约束即可。
2)冗余(存储):在某些情况下处理数据的过程中会失败,消息中间件允许把数据持久化知道他们完全被处理
3)扩展性:消息中间件解耦了应用的过程,所以提供消息入队和处理的效率是很容易的,只需要增加处理流程就可以了。
4)削峰:在访问量剧增的情况下,但是应用仍然需要发挥作用,但是这样的突发流量并不常见。而使用消息中间件采用队列的形式可以减少突发访问压力,不会因为突发的超时负荷要求而崩溃
5)可恢复性:当系统一部分组件失效时,不会影响到整个系统。消息中间件降低了进程间的耦合性,当一个处理消息的进程挂掉后,加入消息中间件的消息仍然可以在系统恢复后重新处理
6)顺序保证:在大多数场景下,处理数据的顺序也很重要,大部分消息中间件支持一定的顺序性
7)缓冲:消息中间件通过一个缓冲层来帮助任务最高效率的执行
8)异步通信:通过把把消息发送给消息中间件,消息中间件并不立即处。

举个栗子,秒杀业务:

用户端发起下单操作
服务端完成秒杀业务逻辑(库存检查,库存冻结,余额检查,余额冻结,订单生成,余额扣减,库存扣减,生成流水,余额解冻,库存解冻)
用户端下单业务简单,每秒发起了10000个请求,服务端秒杀业务复杂,每秒只能处理2000个请求,很有可能用户端不限速的下单,导致服务端系统被压垮,引发雪崩。
为了避免雪崩,常见的优化方案有两种:
1)业务用户端队列缓冲,限速发送
2)业务服务端队列缓冲,限速执行

服务端的限速执行

rabbitmq提供了一种服务质量保障功能,即在非自动确认消息的前提下,如果一定数目的消息未被确认,不进行消费新的消息。
basicQos方法在RabbitMQ的Java驱动中对应三个方法:

void basicQos(int prefetchSize, int prefetchCount, boolean global) throws IOException;

// prefetchSize = 0
void basicQos(int prefetchCount, boolean global) throws IOException;

// prefetchSize = 0 , global = false
void basicQos(int prefetchCount) throws IOException;

prefetchSize:预读取的消息内容大小上限(包含),可以简单理解为消息有效载荷字节数组的最大长度限制,0表示无上限。
prefetchCount:预读取的消息数量上限,0表示无上限。
global:false表示prefetchCount单独应用于信道上的每个新消费者,true表示prefetchCount在同一个信道上的消费者共享。

消费者代码

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.client.QueueingConsumer.Delivery;
public class Consumer {
    public static void main(String[] args) throws Exception {
// 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
// 设置 RabbitMQ 地址
        connectionFactory.setHost("127.0.0.1");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("123456");
        // 创建一个新的连接
        Connection connection = connectionFactory.newConnection();
// 创建一个新的频道
        Channel channel = connection.createChannel();
        
        String exchangeName = "test_qos_exchange";
        String queueName = "test_qos_queue";
        String routingKey = "qos.#";
        
        channel.exchangeDeclare(exchangeName, "topic", true, false, null);
// 声明要关注的队列
        channel.queueDeclare(queueName, true, false, false, null);
        channel.queueBind(queueName, exchangeName, routingKey);
        //1 限流方式  第一件事就是 autoAck设置为 false
        channel.basicQos(0,3,false);
 // 不自动回复队列应答 -- RabbitMQ 中的消息确认机制,
        channel.basicConsume(queueName,false,new MyConsumer(channel));
    }
}

自定义消费者代码

  // DefaultConsumer类 实现了 Consumer 接口, 通过传入一个频道, 告诉服务器我们需要哪个频道的消息
// 如果频道中有消息, 就会执行回调函数 handleDelivery
public class MyConsumer extends DefaultConsumer {
    private Channel channel ;
    public MyConsumer(Channel channel) {
        super(channel);
        this.channel = channel;
    }
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        System.err.println("-----------consume message----------");
        System.err.println("consumerTag: " + consumerTag);
        System.err.println("envelope: " + envelope);
        System.err.println("properties: " + properties);
        System.err.println("body: " + new String(body));
//当我们需要确认一条消息已经被消费时,我们调用的 basicAck 方法的第一个参数是 Delivery Tag。
//Delivery Tag 用来标识信道中投递的消息。RabbitMQ 推送消息给 Consumer 时,会附带一个 Delivery Tag,
//以便 Consumer 可以在消息确认时告诉 RabbitMQ 到底是哪条消息被确认了。
//RabbitMQ 保证在每个信道中,每条消息的 Delivery Tag 从 1 开始递增。
        channel.basicAck(envelope.getDeliveryTag(), false);
        
    }
}

生产者代码

public class Producer {

    
    public static void main(String[] args) throws Exception {
        
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("127.0.0.1");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("admin");
// 创建一个新的连接
        Connection connection = connectionFactory.newConnection();
// 创建一个新的频道
        Channel channel = connection.createChannel();
        
        String exchange = "test_qos_exchange";
        String routingKey = "qos.save";
       
        String msg = "Hello RabbitMQ QOS Message";
       
        for(int i =0; i<5; i ++){
            channel.basicPublish(exchange, routingKey, true, null, msg.getBytes());
        }
    }
}

函数:void basicPublish(String exchange, String routingKey, boolean mandatory, boolean immediate, BasicProperties props, byte[] body) throws IOException;

exchange 做消息转发用
routingKey:路由键,#匹配0个或多个单词,*匹配一个单词,在topic
mandatory:true:如果exchange根据自身类型和消息routeKey无法找到一个符合条件的queue,那么会调用basic.return方法将消息返还给生产者。false:出现上述情形broker会直接将消息扔掉
immediate:true:如果exchange在将消息route到queue(s)时发现对应的queue上没有消费者,那么这条消息不会放入队列中。当与消息routeKey关联的所有queue(一个或多个)都没有消费者时,该消息会通过basic.return方法返还给生产者。
BasicProperties :需要注意的是BasicProperties.deliveryMode,0:不持久化 1:持久化 这里指的是消息的持久化,配合channel(durable=true),queue(durable)可以实现,即使服务器宕机,消息仍然保留

mandatory标志告诉服务器至少将该消息发送到一个队列中,否则将消息返还给生产者;immediate标志告诉服务器如果该消息关联的queue上有消费者,则马上将消息投递给它,如果所有queue都没有消费者,直接把消息返还给生产者,不用将消息入队列等待消费者了。
参考文章RabbitMQ学习(五)消费端削峰限流 - 简书

流量削峰的由来

主要是还是来自于互联网的业务场景,例如,马上即将开始的春节火车票抢购,大量的用户需要同一时间去抢购;以及大家熟知的阿里双11秒杀, 短时间上亿的用户涌入,瞬间流量巨大(高并发),比如:200万人准备在凌晨12:00准备抢购一件商品,但是商品的数量缺是有限的100-500件左右。

这样真实能购买到该件商品的用户也只有几百人左右, 但是从业务上来说,秒杀活动是希望更多的人来参与,也就是抢购之前希望有越来越多的人来看购买商品。

但是,在抢购时间达到后,用户开始真正下单时,秒杀的服务器后端缺不希望同时有几百万人同时发起抢购请求。

我们都知道服务器的处理资源是有限的,所以出现峰值的时候,很容易导致服务器宕机,用户无法访问的情况出现。

这就好比出行的时候存在早高峰和晚高峰的问题,为了解决这个问题,出行就有了错峰限行的解决方案。

同理,在线上的秒杀等业务场景,也需要类似的解决方案,需要平安度过同时抢购带来的流量峰值的问题,这就是流量削峰的由来。

怎样来实现流量削峰方案

削峰从本质上来说就是更多地延缓用户请求,以及层层过滤用户的访问需求,遵从“最后落地到数据库的请求数要尽量少”的原则。

1.消息队列解决削峰

要对流量进行削峰,最容易想到的解决方案就是用消息队列来缓冲瞬时流量,把同步的直接调用转换成异步的间接推送,中间通过一个队列在一端承接瞬时的流量洪峰,在另一端平滑地将消息推送出去。

消息队列中间件主要解决应用耦合,异步消息, 流量削锋等问题。常用消息队列系统:目前在生产环境,使用较多的消息队列有 ActiveMQ、RabbitMQ、 ZeroMQ、Kafka、MetaMQ、RocketMQ 等。

在这里,消息队列就像“水库”一样,拦蓄上游的洪水,削减进入下游河道的洪峰流量,从而达到减免洪水灾害的目的。

具体的消息队列MQ选型和应用场景可以参考(点击查看):高并发架构系列:详解分布式之消息队列的特点、选型、及应用场景

2.流量削峰漏斗:层层削峰

针对秒杀场景还有一种方法,就是对请求进行分层过滤,从而过滤掉一些无效的请求。

分层过滤其实就是采用“漏斗”式设计来处理请求的,如下图所示:

这样就像漏斗一样,尽量把数据量和请求量一层一层地过滤和减少了。

1)分层过滤的核心思想

通过在不同的层次尽可能地过滤掉无效请求。

通过CDN过滤掉大量的图片,静态资源的请求。

再通过类似Redis这样的分布式缓存,过滤请求等就是典型的在上游拦截读请求。

2)分层过滤的基本原则

对写数据进行基于时间的合理分片,过滤掉过期的失效请求。

对写请求做限流保护,将超出系统承载能力的请求过滤掉。

涉及到的读数据不做强一致性校验,减少因为一致性校验产生瓶颈的问题。

对写数据进行强一致性校验,只保留最后有效的数据。

最终,让“漏斗”最末端(数据库)的才是有效请求。例如:当用户真实达到订单和支付的流程,这个是需要数据强一致性的。

总结

1.对于秒杀这样的高并发场景业务,最基本的原则就是将请求拦截在系统上游,降低下游压力。如果不在前端拦截很可能造成数据库(mysql、oracle等)读写锁冲突,甚至导致死锁,最终还有可能出现雪崩等场景。

2.划分好动静资源,静态资源使用CDN进行服务分发。

3.充分利用缓存(redis等):增加QPS,从而加大整个集群的吞吐量。

4.高峰值流量是压垮系统很重要的原因,所以需要Kafka等消息队列在一端承接瞬时的流量洪峰,在另一端平滑地将消息推送出去。

  • 5
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值