《亿级流量网站架构核心技术》-高可用-限流详解

news2024/11/27 5:34:36

背景

项目中遇到一个场景,用户点击按钮给缺勤员工发送扣薪通知。按照需求是一个月只会发送一次通知,在代码逻辑中已经做了兜底策略--在发送邮件之前先判断本月通知是否已发送,发送完之后将发送状态进行持久化。发邮件的时间大概在1分钟之内。

本来觉得逻辑很完善了,可当用户使用时会连续点击按钮,就会出现类似多并发或者重复提交的场景。开始设计了几个思路。

  1. 加锁,由于服务部署在两个服务上,所以需要加分布式锁,之前也是遇到类似的问题也是这样搞的。

  2. 加防止重复提交校验,时间窗内不允许重复提交。但重复提交更多的是用于表单提交,其实这也也是一个思路。

  3. 限流,也是这篇文字介绍的内容,其主要代码贴在了分布式限流。通过设置时间窗口,和限流阈值来限制短时间内多次访问接口。

可能限流并不是所遇场景最优解决方案,也是为了学习吧。

以下内容整理自《亿级流量网站架构核心技术》

限流的目的是通过对并发访问/请求进行限速或者一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务,如定向到错误页面。也可以进行排队/等待,降级。

高并发常见的限流有:限制总并发数,限制时间窗口内的平均速率

限流算法

限流算法主要有两种:

平均速率限流:令牌桶算法,漏桶算法,

总数量限流:简单粗暴的计数器

平均速率限流:

令牌桶算法:是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。具体描述如下:

  1. 按照500毫秒的固定速率往桶中添加令牌,流入速率可以调整。

  2. 桶中最多存放b个令牌,当桶满时,就将新增的令牌丢弃

  3. 当有请求进来的时候,从桶中获取令牌

  4. 若桶中没有令牌,那么该请求就会被限流(丢弃或者缓冲区等待)

漏桶算法:请求如同滴入漏桶中的水,漏桶的容量是固定的,通过控制流出的速率来进行限流。

  1. 一个固定容量的漏桶,按照常量固定速率流出水滴。

  2. 如果桶是空的,则不需流出水滴.可以以任意速率流入水滴到漏桶。

  3. 如果流入水滴超出了桶的容量,则流入的水滴溢出了被丢弃,而漏桶容量是不变的。

令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时,则拒绝新的请求。

漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝。

令牌桶限制的是平均流入速率,允许突发请求,只要有令牌就可以处理,并允许一定程度的突发流量。

漏桶限制的是常量流出速率 (即流出速率是一个固定常量值,通过逐步扩大流出速率,从而平滑突发流入速率。

令牌桶允许一定程度的突发,而漏桶主要目的是平滑流入速率。

总数量限流

计数器限流,主要用来限制总并发数,比如数据库连接池大小、线程池大小、秒杀并发数都是计数器的用法。只要全局总请求数或者一定时间段的总请求数达到设定阙值,则进行限流。

单体应用限流

对于单体应用而言,限流方式从整体和具体接口两方面进行考虑。即使拓展到分布式系统,对于每台机器部署的应用来说,以下方式依然具有参考意义。

整体层面

从整体来考虑限流,可以从总的并发数和总的资源考虑。

  1. 限流总的并发数

对于应用系统来说,一定会有极限并发/请求数,即总有一个 TPS/QPS 阙值。一旦超了阙值,则系统就会不响应用户请求或响应得非常慢。因此需要最好根据需求设计QPS阈值,进行过载保护,以防止大量请求涌入击垮系统。

  1. 限流总的资源数

同样对于数据库连接、线程这样的资源而言,也需要加以限制。可以使用池化技术来限制总资源数,如连接池、线程池。假设分配给每个应用的数据库连接是 100,那么本应用最多可以使用 100 个资源,超出则可以等待或者抛异常

具体接口

对于具体接口限流的方式主要有以下几种

  1. 限流某个接口的并发数

如抢购接口,可能会有突发访问情况,若访问量太大则容易造成系统崩溃,这个时候就需要限制这个接口的总并发/请求数总请求数了。要么让用户排队,要么告诉用户没货了

  1. 限流某个接口的时间窗请求数

对于一些被其他系统调用的基础服务,可能请求并发量不是很多,但是频繁的进行大规模的查询也可能将基础服务搞崩溃。这时候就需要根据一个时间窗口内来时限制某个接口每秒/每分钟的请求数调用量,对每秒/每分钟的调用量进行限速。

  1. 平滑限流某个接口的请求数

上面的限流方式都不能很好地应对突发请求,即瞬间请求可能都被允许,从而导致些问题。因此,在一些场景中需要对突发请求进行平滑限流,降低平均速率请求,比每隔 200 毫秒处理一个请求。这个时候用到上面提到的两种算法:令牌桶和漏桶算法。

分布式限流

分布式限流最关键的是要将限流服务做成原子化,而解决方案可以使用 Redis+Lua或者 Nginx+Lua 技术进行实现,通过这两种技术可以实现高并发和高性能。

如下操作因是在一个 Lua 脚本中,又因 Redis 是单线程模型,因此线程安全。

    @Bean
    public DefaultRedisScript<Long> limitScript() {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(limitScriptText());
        redisScript.setResultType(Long.class);
        return redisScript;
    }

    /**
     * 限流脚本
     */
    private String limitScriptText(){
                //  限流key
        return "local key = KEYS[1]\n" +   
                // 限流阈值
                "local count = tonumber(ARGV[1])\n" +
                // 限流时间窗
                "local time = tonumber(ARGV[2])\n" +
                // 获取限流key的缓存值,
                "local current = redis.call('get', key);\n" +
                // 若缓存值大于阈值,则返回缓存值
                "if current and tonumber(current) > count then\n" +
                "    return current;\n" +
                "end\n" +
                // 否则,缓存值加1,并加入缓存
                "current = redis.call('incr', key)\n" +
                // 如果当前值==1,即初始化的时候,设置过期时间
                "if tonumber(current) == 1 then\n" +
                "    redis.call('expire', key, time)\n" +
                "end\n" +
                "return current;";
    }
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter{
    // 限流key
    public String key() default "key";
  
    // 限流时间,单位秒
    public int time() default 60;

     //限流次数
    public int count() default 100;
}
@Aspect
@Component
public class RateLimiterAspect{
    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;

    @Autowired
    private RedisScript<Long> limitScript;


    // 配置织入点
    @Pointcut("@annotation(RateLimiter)")
    public void rateLimiterPointCut(){}

    @Before("rateLimiterPointCut()")
    public void doBefore(JoinPoint point) throws Throwable {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        if (method != null){
            return;
        }
        RateLimiter rateLimiter = method.getAnnotation(RateLimiter.class);
        if(Objects.isNull(rateLimiter)){
          return;
        }
        String key = rateLimiter.key();
        int time = rateLimiter.time();
        int count = rateLimiter.count();
      
        List<Object> keys = Collections.singletonList(key);
        // 获取缓存窗口中该key的请求数
        Long number = redisTemplate.execute(limitScript, keys, count, time);
        if (StringUtils.isNull(number) || number.intValue() > count){
            throw new ServiceException("访问过于频繁,请稍后再试");
        }
    }

节流

有时候我们想在特定时间窗口内对重复的相同事件最多只处理一次,或者想限制多个连续相同事件最小执行时间间隔,那么可使用节流(Throttle)实现,其防止多个相同事件连续重复执行。

节流算法有以下思路:

  1. 固定时间窗口内的多个连续事件最多只处理一个,如throttleFirst/throttleLast

  2. 两个连续事件的先后执行时间不得小于某个时间窗口,如throttleWithTimeout

思路一:throttleFirst/ throttleLast 

在一个时间窗口内,如果有重复的多个相同事件要处理则只处理第一个或最后一个。减少事件处理频率,从而减少无用处理,提升性能。

场景:网页中的 resize 和 mousemove 事件,当我们快速滚动页面连续触发事件时。可能因此造成 UI 反应慢、浏览器卡顿,因此我们可以使用throttleFirst/ throttleLast ,在一个固定时间窗口内的多个连续事件最多只处理第一个或者最后一个。

思路二:throttleWithTimeout,也叫 debounce(去抖),

基于两个连续事件的相对时间,当两个连续事件的间隔时间小于最小间隔时间窗口,就会丢弃上一个事件,而如果最后一个事件等待了最小间隔时间窗口后还没有新的事件到来,那么会处理最后一个事件。

场景:如搜索关键词自动补全,如果用户每录入一个字就发送一次请求,而先输入的字的自动补全会被很快到来的下一个字符覆盖,那么会导致先期的自动补全是无用的。throttleWithTimeout 就是来解决这个问题的,通过它来减少频繁的网络请求,避免每输入一个字就导致一次请求。

总结:以上两种思路都是为了处理快速连续的操作,通过某些算法去除无用处理,只进行一次请求。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1816423.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

看了这面经,测开上岸不远了

前段时间和4位来自百度、美团、快手、滴滴的高级测开大厂学长学姐&#xff0c;进行了一场直播&#xff0c;负责解答24届春招补录&25届找实习同学的问题 当天直播时长达2个小时&#xff0c;对于如何找测开实习&#xff0c;需要怎么准备项目&#xff0c;简历怎么写&#xff…

每天五分钟计算机视觉:如何在现有经典的卷积神经网络上进行微调

本文重点 在深度学习领域,卷积神经网络(Convolutional Neural Networks,CNN)因其强大的特征提取和分类能力而广泛应用于图像识别、自然语言处理等多个领域。然而,从头开始训练一个CNN模型往往需要大量的数据和计算资源,且训练时间较长。幸运的是,迁移学习(Transfer Le…

MySQL系列-语法说明以及基本操作(二)

1、MySQL数据表的约束 1.1、MySQL主键 “主键&#xff08;PRIMARY KEY&#xff09;”的完整称呼是“主键约束”。 MySQL 主键约束是一个列或者列的组合&#xff0c;其值能唯一地标识表中的每一行。这样的一列或多列称为表的主键&#xff0c;通过它可以强制表的实体完整性。 …

C++的爬山算法

爬山算法&#xff08;Hill Climbing Algorithm&#xff09;是一种局部搜索算法&#xff0c;它通过迭代搜索的方式寻找问题的局部最优解。在爬山过程中&#xff0c;算法总是选择当前状态邻域中最好&#xff08;即函数值最大或最小&#xff09;的状态作为下一个状态&#xff0c;直…

小程序 js+Canvas 绘制半圆环虚线进度条

效果图&#xff1a; 思路&#xff1a;过程分为三步&#xff0c;第1步&#xff0c;先画虚线底部背景&#xff0c;第2步&#xff0c;画动态的虚线&#xff08;已选虚线蓝颜色&#xff09;&#xff0c;第3步&#xff0c;画动态的外标&#xff08;已选虚线外位置的标&#xff09;&a…

AOSP12隐藏首页搜索框----隐藏google 搜索栏

目录 第一步&#xff1a;修改文件 第二步&#xff1a;修改文件 第三步&#xff1a;重新编译源码&#xff0c;启动模拟器 第四步、运行效果 第一步&#xff1a;修改文件 源码文件路径: packages/apps/Launcher3/res/layout/search_container_workspace.xml&#xff0c;将…

Navicat for MySQL 11软件下载附加详细安装教程

根据使用者情况表明Navicat Premium 能使你快速地在各种数据库系统间传输数据&#xff0c;或传输到一份指定 SQL 格式和编码的纯文本文件&#xff0c;计划不同数据库的批处理作业并在指定的时间运行&#xff0c;其他功能包括导入向导、导出向导、查询创建工具、报表创建工具、数…

【6】第一个Java程序:Hello World

一、引言 Java&#xff0c;作为一种广泛使用的编程语言&#xff0c;其强大的跨平台能力和丰富的库函数使其成为开发者的首选。对于初学者来说&#xff0c;编写并运行第一个Java程序是一个令人兴奋的时刻。本文将指导你使用Eclipse这一流行的集成开发环境&#xff08;IDE&#…

【对抗样本】【FGSM】Explaining and Harnessing Adversarial Examples 代码复现

简介 参考Pytorch官方的代码Adversarial Example Generation 参数设置(main.py) # 模型选择&#xff1a;GPU device mps if torch.backends.mps.is_available() else cpu # 数据集位置 dataset_path ../../../Datasets batch_size 1 shuffle True download False # 学习率…

express入门03增删改查

目录 1 搭建服务器2 静态文件托管3 引入bootstrap4 引入jquery5 编写后端接口5.1 添加列表查询方法5.2 添加路由5.3 添加数据表格 总结 我们前两篇介绍了如何利用express搭建服务器&#xff0c;如何实现静态资源托管。那利用这两篇的知识点&#xff0c;我们就可以实现一个小功能…

WebSocket 快速入门 与 应用

WebSocket 是一种在 Web 应用程序中实现实时、双向通信的技术。它允许客户端和服务器之间建立持久性的连接&#xff0c;以便可以在两者之间双向传输数据。 以下是 WebSocket 的一些关键特点和工作原理&#xff1a; 0.特点&#xff1a; 双向通信&#xff1a;WebSocket 允许服务…

艾宾浩斯winform单词系统+mysql

为用户提供集词典、题库、记忆单词功能于一体的应用&#xff0c;为用户提供目的性强、科学高效、多样化的记忆单词方法&#xff0c;使用户学习英语和记忆单词的效率得到提高 单词记忆模块 管理模块 查询单词 阅读英文 查看词汇 记忆单词 收藏单词 字段管理设置 统计 艾宾浩斯wi…

springBoot多数据源使用、配置

又参加了一个新的项目&#xff0c;虽然是去年做的项目&#xff0c;拿来复用改造&#xff0c;但是也学到了很多。这个项目会用到其他项目的数据&#xff0c;如果调用他们的接口取数据&#xff0c;我还是觉得太麻烦了。打算直接配置多数据源。 然后去另一个数据库系统中取出数据…

【C语音 || 数据结构】二叉树--堆

文章目录 前言堆1.1 二叉树的概念1.2 满二叉树和完美二叉树1.3 堆的概念1.4 堆的性质1.4 堆的实现1.4.1堆的向上调整算法1.4.1堆的向下调整算法1.4.1堆的接口实现1.4.1.1堆的初始化1.4.1.2堆的销毁1.4.1.3堆的插入1.4.1.4堆的删除1.4.1.4堆的判空1.4.1.4 获取堆的数据个数 前言…

当客户一上来就问你产品价格,你可以多尝试问问

做外贸业务&#xff0c;每个对产品不了解的客户&#xff0c;很多人一上来都会习惯性地问我们价格。一些新手业务会比较直接&#xff0c;一下子就把价格报出去了&#xff0c;很容易因为报错价格导致客户杳无音讯。 其实这个时候&#xff0c;我们最应该做的是尝试跟客户多聊一聊…

vuInhub靶场实战系列--Kioptrix Level #4

免责声明 本文档仅供学习和研究使用,请勿使用文中的技术源码用于非法用途,任何人造成的任何负面影响,与本人无关。 目录 免责声明前言一、环境配置1.1 靶场信息1.2 靶场配置 二、信息收集2.1 主机发现2.1.1 netdiscover2.1.2 arp-scan主机扫描 2.2 端口扫描2.3 指纹识别2.4 目…

MySQL-子查询(DQL 结束)

054-where后面使用子查询 什么是子查询 select语句中嵌套select语句就叫做子查询。select语句可以嵌套在哪里&#xff1f; where后面、from后面、select后面都是可以的。 select ..(select).. from ..(select).. where ..(select)..where后面使用子查询 案例&#xff1a;找…

国际贸易条件简称的解析说明

声明&#xff1a;本文仅代表作者观点和立场&#xff0c;不代表任何公司&#xff01;仅用于SAP软件应用学习参考。 SAP创建销售订单的界面有个国际贸易条件的字段&#xff0c;这个字段选择值主要有如下选择值&#xff0c;国际贸易条件简称的具体解析说明如下&#xff1a; EXW &…

【文档智能】包含段落的开源的中文版面分析模型

github&#xff1a;https://github.com/360AILAB-NLP/360LayoutAnalysis 权重下载地址&#xff1a;https://huggingface.co/qihoo360/360LayoutAnalysis 一、背景 在当今数字化时代&#xff0c;文档版式分析是信息提取和文档理解的关键步骤之一。文档版式分析&#xff0c;也…

数据价值管理-数据验收标准

前情提要&#xff1a;数据价值管理是指通过一系列管理策略和技术手段&#xff0c;帮助企业把庞大的、无序的、低价值的数据资源转变为高价值密度的数据资产的过程&#xff0c;即数据治理和价值变现。第一讲介绍了业务架构设计的基本逻辑和思路。前面我们讲完了数据资产建设标准…