限流(Rate Limiting)、降级(Degradation)和缓存(Caching)是提升系统稳定性和性能的三大法宝,帮助系统在面临高并发、资源紧张或依赖服务故障时,依然能够保持一定的稳定性、可用性和性能。
本文初步介绍了上面三大法宝的作用于使用场景,并详细讲解了 限流的三种算法(令牌桶算法、漏桶算法、滑动窗口算法)、并提供了部分Java实现与算法使用测试
1. 限流(Rate Limiting)
限流是指对系统或服务的请求进行速率控制,以避免因过多的请求而导致系统资源耗尽、崩溃或服务不可用。限流通过控制单位时间内的请求数量或并发量,来保护系统不被过高的流量压垮。
1.1 应用场景:
- API 接口服务,防止被恶意请求攻击(如 DDoS 攻击)。
- 电商网站的秒杀活动,限制用户请求频率,确保系统平稳运行。
- 数据库访问,防止过多的查询请求导致数据库性能下降。
1.2 三种限流算法与实现
1.2.1 令牌桶算法(Token Bucket)
工作原理可以简化为一个固定容量的桶,这个桶以恒定的速率向其中填充令牌(tokens)。当请求到达时,如果桶中有足够的令牌,请求就会被处理,并且会消耗相应数量的令牌;如果桶中没有足够的令牌,请求可能会被延迟或者拒绝。
Google的Guava和Redisson的限流都采用了令牌桶算法。
Java代码实现
调用 tryConsume方法获取令牌时,首先刷新令牌桶数量:(当前时间-上次刷新时间)* 一秒刷新数量。更新数量之后,然后获取令牌
import java.util.concurrent.atomic.AtomicLong;
public class TokenBucket {
private final long capacity; // 桶的容量
private final long refillRate; // 每秒填充的令牌数
private AtomicLong tokens; // 当前桶中的令牌数
private volatile long lastRefillTime; // 上次填充令牌的时间
public TokenBucket(long capacity, long refillRate) {
this.capacity = capacity;
this.refillRate = refillRate;
this.tokens = new AtomicLong(0); // 初始化桶为满
this.lastRefillTime = System.currentTimeMillis();
}
/**
* 尝试从桶中获取指定数量的令牌
*
* @param tokensNeeded 需要的令牌数
* @return 如果成功获取到足够的令牌,则返回true;否则返回false
*/
public synchronized boolean tryConsume(int tokensNeeded) {
// 刷新令牌桶
refillTokens();
if (this.tokens.get() >= tokensNeeded) {
// 尝试减少令牌数,这里使用CAS(Compare-And-Swap)操作来确保线程安全
while (!this.tokens.compareAndSet(this.tokens.get(), this.tokens.get() - tokensNeeded)) {
// 如果CAS失败,可能是因为其他线程已经改变了tokens的值,所以重新尝试
}
return true;
}
return false;
}
}
测试代码
public static void main(String[] args) {
// 创建一个令牌桶,容量为100,每秒填充10个令牌
TokenBucket tokenBucket = new TokenBucket(100, 10);
for (int i = 0; i < 10; i++) {
// 假设每个请求需要5个令牌
if (tokenBucket.tryConsume(10)) {
System.out.println("请求 " + (i + 1) + " 成功,消耗了10个令牌。");
} else {
System.out.println("请求 " + (i + 1) + " 失败,没有足够的令牌。");
}
// 为了演示效果,这里暂停一段时间,模拟请求的间隔时间
try {
Thread.sleep(500); // 暂停100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果:
请求 1 失败,没有足够的令牌。
请求 2 失败,没有足够的令牌。
请求 3 成功,消耗了10个令牌。
请求 4 失败,没有足够的令牌。
请求 5 成功,消耗了10个令牌。
请求 6 失败,没有足够的令牌。
请求 7 成功,消耗了10个令牌。
请求 8 失败,没有足够的令牌。
请求 9 成功,消耗了10个令牌。
请求 10 失败,没有足够的令牌
1.2.2 漏桶算法(Leaky Bucket)
漏桶算法(Leaky Bucket Algorithm)是一种常用的流量控制算法,主要用于控制数据的注入速率,平滑突发流量,并限制数据的最大突发量。漏桶算法可以想象为一个固定容量的桶,桶底有一个小孔,水(代表数据)以恒定的速率从桶底流出。当水(数据)注入桶中时,如果桶未满,则水(数据)会留在桶中;如果桶已满,则多余的水(数据)会被丢弃。
1.2.3 计数器(Fixed Window,固定窗口算法)
- 原理:通过维护一个计数器变量来限制在特定时间间隔内的请求数量。在指定周期内累加访问次数,当访问次数达到设定的阈值时,触发限流策略。时间周期结束时,计数器清零并重新开始计数。
- 优点:实现简单,直观易懂,设置明确的阈值。
- 缺点:存在窗口切换时的“突增问题”,即在一个时间窗口的末尾和下一个时间窗口的开始,可能会因为请求量突然增加而超出限制。
1.2.4 滑动窗口算法
- 原理:将固定窗口细分成多个小时间窗口(或称为滑动窗口),每个小窗口都有自己的计数器。随着时间的推移,窗口会向前滑动,并丢弃过期的小窗口数据。通过统计滑动窗口内的总请求数来实现限流。
- 优点:解决了固定窗口算法的“突增问题”,提供了更平滑的流量控制。
- 应用:Spring Cloud中的熔断框架Hystrix,以及Spring Cloud Alibaba中的Sentinel都采用滑动窗口来做数据统计。
2. 降级(Degradation)
降级是在系统资源紧张或依赖服务故障时,为了保证系统的核心功能依然可用,对非核心功能或服务进行暂时性的降级处理。
应用场景:
- 第三方服务调用失败时,可以降级为使用本地缓存数据或默认值。
- 系统资源不足时,关闭非关键功能,保证核心功能的正常运行。
- 在高并发场景下,对部分请求进行限流或拒绝服务,以保护系统整体可用性。
实现方式:
- 提前设计好降级策略,如设置服务依赖的优先级,当资源不足时按优先级进行降级。
- 使用开关控制功能是否启用,便于在紧急情况下快速切换。
- 监控服务状态,自动触发降级逻辑。
3. 加缓存(Caching)
加缓存是通过将计算结果或数据存储在内存中或更快的存储介质上,以减少对慢速资源(如数据库)的访问次数,从而提高系统的响应速度和处理能力。
应用场景:
- 读取操作远多于写入操作的场景,如商品详情页展示。
- 实时性要求不高的数据查询,如用户个人信息。
- 计算成本较高的操作结果,如复杂的查询语句结果。
实现方式:
- 本地缓存:如 Java 中的 EhCache、Guava Cache。
- 分布式缓存:如 Redis、Memcached,适用于分布式系统。
- CDN(内容分发网络):适用于静态资源缓存,减少服务器负载。