【高并发】如何实现亿级流量下的分布式限流?这些算法你必须掌握!!

简介: 在互联网应用中,高并发系统会面临一个重大的挑战,那就是大量流高并发访问,比如:天猫的双十一、京东618、秒杀、抢购促销等,这些都是典型的大流量高并发场景。关于秒杀,小伙伴们可以参见我的另一篇文章《【高并发】高并发秒杀系统架构解密,不是所有的秒杀都是秒杀!》关于【冰河技术】微信公众号,解锁更多【高并发】专题文章。注意:由于原文篇幅比较长,所以被拆分为:理论、算法、实战(HTTP接口实战+分布式限流实战)三大部分。理论篇参见《【高并发】如何实现亿级流量下的分布式限流?这些理论你必须掌握!!》

计数器

计数器法

限流算法中最简单粗暴的一种算法,例如,某一个接口1分钟内的请求不超过60次,我们可以在开始时设置一个计数器,每次请求时,这个计数器的值加1,如果这个这个计数器的值大于60并且与第一次请求的时间间隔在1分钟之内,那么说明请求过多;如果该请求与第一次请求的时间间隔大于1分钟,并且该计数器的值还在限流范围内,那么重置该计数器。

使用计数器还可以用来限制一定时间内的总并发数,比如数据库连接池、线程池、秒杀的并发数;计数器限流只要一定时间内的总请求数超过设定的阀值则进行限流,是一种简单粗暴的总数量限流,而不是平均速率限流。

微信图片_20211119135247.jpg

这个方法有一个致命问题:临界问题——当遇到恶意请求,在0:59时,瞬间请求100次,并且在1:00请求100次,那么这个用户在1秒内请求了200次,用户可以在重置节点突发请求,而瞬间超过我们设置的速率限制,用户可能通过算法漏洞击垮我们的应用。

微信图片_20211119135254.jpg

这个问题我们可以使用滑动窗口解决。

滑动窗口

微信图片_20211119135256.jpg

在上图中,整个红色矩形框是一个时间窗口,在我们的例子中,一个时间窗口就是1分钟,然后我们将时间窗口进行划分,如上图我们把滑动窗口划分为6格,所以每一格代表10秒,每超过10秒,我们的时间窗口就会向右滑动一格,每一格都有自己独立的计数器,例如:一个请求在0:35到达, 那么0:30到0:39的计数器会+1,那么滑动窗口是怎么解决临界点的问题呢?如上图,0:59到达的100个请求会在灰色区域格子中,而1:00到达的请求会在红色格子中,窗口会向右滑动一格,那么此时间窗口内的总请求数共200个,超过了限定的100,所以此时能够检测出来触发了限流。回头看看计数器算法,会发现,其实计数器算法就是窗口滑动算法,只不过计数器算法没有对时间窗口进行划分,所以是一格。

由此可见,当滑动窗口的格子划分越多,限流的统计就会越精确。

漏桶算法

算法的思路就是水(请求)先进入到漏桶里面,漏桶以恒定的速度流出,当水流的速度过大就会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。如下图所示。

微信图片_20211119135307.jpg

漏桶算法不支持突发流量。

令牌桶算法

微信图片_20211119135309.jpg

从上图中可以看出,令牌算法有点复杂,桶里存放着令牌token。桶一开始是空的,token以固定的速率r往桶里面填充,直到达到桶的容量,多余的token会被丢弃。每当一个请求过来时,就会尝试着移除一个token,如果没有token,请求无法通过。

令牌桶算法支持突发流量。

令牌桶算法实现

Guava框架提供了令牌桶算法的实现,可直接使用这个框架的RateLimiter类创建一个令牌桶限流器,比如:每秒放置的令牌桶的数量为5,那么RateLimiter对象可以保证1秒内不会放入超过5个令牌,并且以固定速率进行放置令牌,达到平滑输出的效果。

平滑流量示例

这里,我写了一个使用Guava框架实现令牌桶算法的示例,如下所示。

package io.binghe.limit.guava;
import com.google.common.util.concurrent.RateLimiter;
/**
 * @author binghe
 * @version 1.0.0
 * @description 令牌桶算法
 */
public class TokenBucketLimiter {
    public static void main(String[] args){
        //每秒钟生成5个令牌
        RateLimiter limiter = RateLimiter.create(5);
        //返回值表示从令牌桶中获取一个令牌所花费的时间,单位是秒
        System.out.println(limiter.acquire(1));
        System.out.println(limiter.acquire(1));
        System.out.println(limiter.acquire(1));
        System.out.println(limiter.acquire(1));
        System.out.println(limiter.acquire(1));
        System.out.println(limiter.acquire(1));
        System.out.println(limiter.acquire(1));
        System.out.println(limiter.acquire(1));
        System.out.println(limiter.acquire(1));
        System.out.println(limiter.acquire(1));
    }
}

代码的实现非常简单,就是使用Guava框架的RateLimiter类生成了一个每秒向桶中放入5个令牌的对象,然后不断从桶中获取令牌。我们先来运行下这段代码,输出的结果信息如下所示。

0.0
0.197294
0.191278
0.19997
0.199305
0.200472
0.200184
0.199417
0.200111
0.199759

从输出结果可以看出:第一次从桶中获取令牌时,返回的时间为0.0,也就是没耗费时间。之后每次从桶中获取令牌时,都会耗费一定的时间,这是为什么呢?按理说,向桶中放入了5个令牌后,再从桶中获取令牌也应该和第一次一样并不会花费时间啊!

因为在Guava的实现是这样的:我们使用RateLimiter.create(5)创建令牌桶对象时,表示每秒新增5个令牌,1秒等于1000毫秒,也就是每隔200毫秒向桶中放入一个令牌。

当我们运行程序时,程序运行到RateLimiter limiter = RateLimiter.create(5);时,就会向桶中放入一个令牌,当程序运行到第一个System.out.println(limiter.acquire(1));时,由于桶中已经存在一个令牌,直接获取这个令牌,并没有花费时间。然而程序继续向下执行时,由于程序会每隔200毫秒向桶中放入一个令牌,所以,获取令牌时,花费的时间几乎都是200毫秒左右。

突发流量示例

我们再来看一个突发流量的示例,代码示例如下所示。

package io.binghe.limit.guava;
import com.google.common.util.concurrent.RateLimiter;
/**
 * @author binghe
 * @version 1.0.0
 * @description 令牌桶算法
 */
public class TokenBucketLimiter {
    public static void main(String[] args){
        //每秒钟生成5个令牌
        RateLimiter limiter = RateLimiter.create(5);
        //返回值表示从令牌桶中获取一个令牌所花费的时间,单位是秒
        System.out.println(limiter.acquire(50));
        System.out.println(limiter.acquire(5));
        System.out.println(limiter.acquire(5));
        System.out.println(limiter.acquire(5));
        System.out.println(limiter.acquire(5));
    }
}

上述代码表示的含义为:每秒向桶中放入5个令牌,第一次从桶中获取50个令牌,也就是我们说的突发流量,后续每次从桶中获取5个令牌。接下来,我们运行上述代码看下效果。

0.0
9.998409
0.99109
1.000148
0.999752

运行代码时,会发现当命令行打印出0.0后,会等很久才会打印出后面的输出结果。

程序每秒钟向桶中放入5个令牌,当程序运行到 RateLimiter limiter = RateLimiter.create(5); 时,就会向桶中放入令牌。当运行到 System.out.println(limiter.acquire(50)); 时,发现很快就会获取到令牌,花费了0.0秒。接下来,运行到第一个System.out.println(limiter.acquire(5));时,花费了9.998409秒。小伙们可以思考下,为什么这里会花费10秒中的时间呢?

这是因为我们使用RateLimiter limiter = RateLimiter.create(5);代码向桶中放入令牌时,一秒钟放入5个,而System.out.println(limiter.acquire(50));需要获取50个令牌,也就是获取50个令牌需要花费10秒钟时间,这是因为程序向桶中放入50个令牌需要10秒钟。程序第一次从桶中获取令牌时,很快就获取到了。而第二次获取令牌时,花费了将近10秒的时间。

Guava框架支持突发流量,但是在突发流量之后再次请求时,会被限速,也就是说:在突发流量之后,再次请求时,会弥补处理突发请求所花费的时间。所以,我们的突发示例程序中,在一次从桶中获取50个令牌后,再次从桶中获取令牌,则会花费10秒左右的时间。

Guava令牌桶算法的特点

  • RateLimiter使用令牌桶算法,会进行令牌的累积,如果获取令牌的频率比较低,则不会导致等待,直接获取令牌。
  • RateLimiter由于会累积令牌,所以可以应对突发流量。也就是说如果同时请求5个令牌,由于此时令牌桶中有累积的令牌,能够快速响应请求。
  • RateLimiter在没有足够的令牌发放时,采用的是滞后的方式进行处理,也就是前一个请求获取令牌所需要等待的时间由下一次请求来承受和弥补,也就是代替前一个请求进行等待。(这里,小伙伴们要好好理解下)
相关文章
|
16天前
|
设计模式 存储 算法
分布式系统架构5:限流设计模式
本文是小卷关于分布式系统架构学习的第5篇,重点介绍限流器及4种常见的限流设计模式:流量计数器、滑动窗口、漏桶和令牌桶。限流旨在保护系统免受超额流量冲击,确保资源合理分配。流量计数器简单但存在边界问题;滑动窗口更精细地控制流量;漏桶平滑流量但配置复杂;令牌桶允许突发流量。此外,还简要介绍了分布式限流的概念及实现方式,强调了限流的代价与收益权衡。
59 11
|
1月前
|
消息中间件 架构师 数据库
本地消息表事务:10Wqps 高并发分布式事务的 终极方案,大厂架构师的 必备方案
45岁资深架构师尼恩分享了一篇关于分布式事务的文章,详细解析了如何在10Wqps高并发场景下实现分布式事务。文章从传统单体架构到微服务架构下分布式事务的需求背景出发,介绍了Seata这一开源分布式事务解决方案及其AT和TCC两种模式。随后,文章深入探讨了经典ebay本地消息表方案,以及如何使用RocketMQ消息队列替代数据库表来提高性能和可靠性。尼恩还分享了如何结合延迟消息进行事务数据的定时对账,确保最终一致性。最后,尼恩强调了高端面试中需要准备“高大上”的答案,并提供了多个技术领域的深度学习资料,帮助读者提升技术水平,顺利通过面试。
本地消息表事务:10Wqps 高并发分布式事务的 终极方案,大厂架构师的 必备方案
|
2月前
|
算法 关系型数据库 MySQL
分布式唯一ID生成:深入理解Snowflake算法在Go中的实现
在分布式系统中,确保每个节点生成的 ID 唯一且高效至关重要。Snowflake 算法由 Twitter 开发,通过 64 位 long 型数字生成全局唯一 ID,包括 1 位标识位、41 位时间戳、10 位机器 ID 和 12 位序列号。该算法具备全局唯一性、递增性、高可用性和高性能,适用于高并发场景,如电商促销时的大量订单生成。本文介绍了使用 Go 语言的 `bwmarrin/snowflake` 和 `sony/sonyflake` 库实现 Snowflake 算法的方法。
62 1
分布式唯一ID生成:深入理解Snowflake算法在Go中的实现
|
29天前
|
存储 算法 安全
分布式系统架构1:共识算法Paxos
本文介绍了分布式系统中实现数据一致性的重要算法——Paxos及其改进版Multi Paxos。Paxos算法由Leslie Lamport提出,旨在解决分布式环境下的共识问题,通过提案节点、决策节点和记录节点的协作,确保数据在多台机器间的一致性和可用性。Multi Paxos通过引入主节点选举机制,优化了基本Paxos的效率,减少了网络通信次数,提高了系统的性能和可靠性。文中还简要讨论了数据复制的安全性和一致性保障措施。
39 1
|
2月前
|
存储 缓存 算法
分布式缓存有哪些常用的数据分片算法?
【10月更文挑战第25天】在实际应用中,需要根据具体的业务需求、数据特征以及系统的可扩展性要求等因素综合考虑,选择合适的数据分片算法,以实现分布式缓存的高效运行和数据的合理分布。
|
2月前
|
分布式计算 Java 开发工具
阿里云MaxCompute-XGBoost on Spark 极限梯度提升算法的分布式训练与模型持久化oss的实现与代码浅析
本文介绍了XGBoost在MaxCompute+OSS架构下模型持久化遇到的问题及其解决方案。首先简要介绍了XGBoost的特点和应用场景,随后详细描述了客户在将XGBoost on Spark任务从HDFS迁移到OSS时遇到的异常情况。通过分析异常堆栈和源代码,发现使用的`nativeBooster.saveModel`方法不支持OSS路径,而使用`write.overwrite().save`方法则能成功保存模型。最后提供了完整的Scala代码示例、Maven配置和提交命令,帮助用户顺利迁移模型存储路径。
|
3月前
|
NoSQL Java Redis
京东双十一高并发场景下的分布式锁性能优化
【10月更文挑战第20天】在电商领域,尤其是像京东双十一这样的大促活动,系统需要处理极高的并发请求。这些请求往往涉及库存的查询和更新,如果处理不当,很容易出现库存超卖、数据不一致等问题。
78 1
|
3月前
|
算法
基于粒子群算法的分布式电源配电网重构优化matlab仿真
本研究利用粒子群算法(PSO)优化分布式电源配电网重构,通过Matlab仿真验证优化效果,对比重构前后的节点电压、网损、负荷均衡度、电压偏离及线路传输功率,并记录开关状态变化。PSO算法通过迭代更新粒子位置寻找最优解,旨在最小化网络损耗并提升供电可靠性。仿真结果显示优化后各项指标均有显著改善。
|
3月前
|
NoSQL Java Redis
Redlock分布式锁高并发下有什么问题
Redlock分布式锁在高并发场景下可能面临的问题主要包括:网络延迟、时钟偏移、单点故障、宕机重启问题、脑裂问题以及效率低等。接下来,我将使用Java代码示例来说明其中一些问题。
121 12
|
3月前
|
存储 缓存 NoSQL
大数据-38 Redis 高并发下的分布式缓存 Redis简介 缓存场景 读写模式 旁路模式 穿透模式 缓存模式 基本概念等
大数据-38 Redis 高并发下的分布式缓存 Redis简介 缓存场景 读写模式 旁路模式 穿透模式 缓存模式 基本概念等
81 4