引言
在当今互联网时代,应用程序和服务的流量增长迅速,用户访问的高峰期和流量波动使得如何有效管理和保护系统变得尤为重要。在这个背景下,限流(Rate Limiting)成为了一种关键的技术手段。限流是一种通过限制系统中某个组件或用户的请求频率,以防止其超出系统处理能力范围的方法。这种手段不仅能够保障系统的稳定性和可用性,还有助于防范恶意攻击、提高系统的安全性。
什么是限流?
顾名思义,限流是一种控制请求流量的策略,通过设置阈值,确保系统在任何给定时间内都不会接收过多的请求。
这样做的目的主要有两个方面:保护系统免受过载的风险,以及确保公平地分配资源给所有的用户。在限流的背后,通常会有一系列算法和策略来判断是否应该拒绝或延迟某个请求。
限流广泛应用于各种互联网服务和应用程序中,如以下场景:
-
API服务保护:在微服务架构中,不同服务间通过API进行通信。通过限制对每个API的请求速率,可以防止某个服务因为过多请求导致崩溃,也可以防止某个服务被恶意攻击。
-
用户认证和授权:在身份验证和授权系统中,通过对用户的请求进行限流,可以避免暴力破解密码、防止大规模爬虫等安全问题。
-
消息队列:在消息队列系统中,限制生产者和消费者的速率,以平衡生产者和消费者之间的负载,防止消息堆积过多。
-
支付系统:对于支付系统来说,限流是确保交易过程可靠性和安全性的关键手段。通过控制请求速率,可以防止恶意的大额交易或者重复交易。
-
网关服务:在网关服务中,限流可以防止流量暴增,确保后端服务不会因为过多的请求而宕机。
通过合理的限流策略,可以有效地应对系统面临的各种挑战,确保其在高负载和不断变化的环境中依然能够提供稳定、高效的服务。接下来,我将先简单介绍四种常见的限流算法。
限流算法
常见的有四种限流算法:固定窗口限流算法、滑动窗口限流算法、漏桶算法、令牌桶算法;
固定窗口限流算法
算法原理
固定窗口限流算法将时间划分为固定大小的窗口,比如1秒,然后在每个窗口内限制请求的数量。例如,如果在一个1秒的窗口内允许最多处理10个请求,那么在这个窗口内只能处理10个请求,超过的部分将被限制;通过一个计数器count计数,一个窗口结束后(如1秒后)计数器count清零。
固定窗口限流算法存在流量突刺的问题,比如当第一个窗口0.9 ~ 1.0s时发送了10个请求,第二个窗口1.0 ~ 1.1s时又发送了10个请求,这两个窗口分别看都没有超过流量阈值,但是在0.9 ~ 1.1s这0.2s内并发数达到了20个请求,已经超出了1s内最多10个请求的界限,这是一个临界问题,所以固定窗口算法虽然简单,但是需要考虑到这种情况。
滑动窗口限流算法
算法原理
滑动窗口限流算法在固定时间内维护一个可滑动的窗口,根据窗口内的请求数量进行限流。随着时间的推移,窗口内的请求数量会不断更新,从而适应流量的变化。这个算法可以解决固定窗口的临界值问题,它是将固定窗口进行划分为小窗口,每个小窗口单独计数,当时间大于该小窗口时间范围时,向前平移一个小窗口;
比如单位时间1秒内最多处理10个请求,并将其划分为5个小窗口,每个小窗口0.2s,并各有一个单独的计数器,每过0.2s小窗口右滑动一个,当平移到第六个小窗口时,舍弃第一个小窗口,还是5个小窗口(1s);画图理解一下:
滑动窗口限流算法可以很好的应对固定窗口限流算法中临界问题,比如当0.9 ~ 1.0s时发送了10个请求,在1.0 ~ 1.1s内若又来了10个请求,则在0.2~1.2s这个时间区间内请求数目已经超过阈值,所以1.0 ~ 1.1s内的10个请求会被限制。
滑动窗口单位时间内划分的小窗口越多,滑动窗口的滚动就越平滑,统计就越精确,但很难取到一个特别合适的滑动单位,实现也相对复杂。
漏桶算法
算法原理
漏桶算法将请求以固定的速率处理,超出速率的请求将被放入漏桶中,然后按照固定的速率从漏桶中释放。这样可以平滑处理突发流量,确保系统在单位时间内处理的请求数量不超过设定的速率。
该算法以固定速率处理请求,当漏桶满了后,拒绝请求;
如每秒处理10个请求,桶容量为20,每0.1s固定处理一个请求,若1s内来了10个请求,那么都可以依次处理完;若1s内来了30个请求,后10个请求会溢出桶,桶内留有20个请求,这1s处理10个,剩下10个第2s处理。
漏桶算法可以保证固定速率处理请求,保证服务的安全,但不支持突发流量,当出现突发流量时,会存在大量请求被溢出无法处理,并且只能一个一个处理请求,不能实现并发处理。
令牌桶算法
算法原理
令牌桶算法通过维护一个固定容量的令牌桶,按照固定速率往桶中放入令牌。对于每个请求,需要从令牌桶中获取一个令牌,有令牌的才能执行操作;如果令牌桶中没有足够的令牌,则请求被限制。
令牌桶算法是对漏桶算法的改进,不仅能限制调用的平均速率,还能保证一定程度的流量突发,拿到令牌的请求可以并发处理,性能更高;
这些限流算法在实际应用中根据场景的不同选择会有所不同。例如,固定窗口限流适用于有明显的时间窗口,而滑动窗口限流则更适用于需要平滑处理流量变化的场景。漏桶算法和令牌桶算法常用于需要对请求进行均速处理的场景。根据具体的业务需求,选择合适的限流算法是非常重要的。
限流的实现方式
在java中,可以使用多种技术来实现限流,以下是一些常见的限流实现技术:
Guava RateLimiter
Guava是Google提供的一个Java开发库,其中包含了一个非常方便的限流工具——RateLimiter。RateLimiter基于令牌桶算法实现,可以用来控制对特定资源的访问速率。
// 示例代码
RateLimiter rateLimiter = RateLimiter.create(10); // 每秒处理10个请求
if (rateLimiter.tryAcquire()) {
// 处理业务逻辑
} else {
// 请求被限流
}
Spring Cloud Gateway
基于Spring Cloud构建的微服务架构,可以使用Spring Cloud Gateway来进行全局的限流控制。Spring Cloud Gateway提供了基于令牌桶算法的限流过滤器。
# 配置代码
spring:
cloud:
gateway:
routes:
- id: my_route
uri: http://example.org
filters:
- TokenRateLimiter=10
Servlet Filter
在传统的Java Web应用中,可以通过自定义Servlet Filter来实现请求的限流控制。在Filter中,可以使用类似令牌桶或漏桶的算法来实现限流。
// 示例代码
public class RateLimitFilter implements Filter {
private static final int RATE_LIMIT = 10;
private Semaphore semaphore = new Semaphore(RATE_LIMIT);
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (semaphore.tryAcquire()) {
try {
// 处理业务逻辑
chain.doFilter(request, response);
} finally {
semaphore.release();
}
} else {
// 请求被限流
// 可以返回自定义的限流响应或直接抛出异常
}
}
// 其他方法...
}
Redis + Lua脚本
使用Redis作为分布式缓存,结合Lua脚本可以实现分布式限流。通过在Redis中维护计数器,通过Lua脚本原子性地进行判断和更新。
// 示例代码(使用Lettuce客户端)
public boolean acquireToken(String key, int limit, long intervalInMillis) {
RedisScript<Boolean> redisScript = new DefaultRedisScript<>(
"local currentTokens = redis.call('get', KEYS[1]) or 0\n" +
"if tonumber(currentTokens) > 0 then\n" +
" redis.call('decr', KEYS[1])\n" +
" return true\n" +
"else\n" +
" local newTokens = redis.call('incr', KEYS[1])\n" +
" redis.call('pexpire', KEYS[1], ARGV[1])\n" +
" if tonumber(newTokens) <= tonumber(ARGV[2]) then\n" +
" return true\n" +
" else\n" +
" redis.call('decr', KEYS[1])\n" +
" return false\n" +
" end\n" +
"end",
Boolean.class
);
List<String> keys = Collections.singletonList(key);
Boolean result = lettuceRedisTemplate.execute(redisScript, keys, intervalInMillis, limit);
return result != null && result;
}
Redisson
Redisson是一个基于Redis的Java驱动,提供了丰富的分布式对象和服务,同时也支持分布式限流的实现。Redisson中提供了RRateLimiter对象,它是基于令牌桶算法实现的分布式限流器。
官方文档:
import org.redisson.Redisson;
import org.redisson.api.RRateLimiter;
import org.redisson.api.RedissonClient;
public class RedissonRateLimiterExample {
public static void main(String[] args) {
// 初始化Redisson客户端
RedissonClient redisson = Redisson.create();
// 创建分布式限流器
RRateLimiter rateLimiter = redisson.getRateLimiter("myRateLimiter");
// 设定每秒钟产生10个令牌
rateLimiter.trySetRate(RateType.OVERALL, 10, 1, RateIntervalUnit.SECONDS);
// 在业务代码中使用限流器
if (rateLimiter.tryAcquire()) { // tryAcquire(数目)获取令牌,可以设置获取令牌数目数目,还可以用tryAcquireAsync异步获取
// 处理业务逻辑
System.out.println("Request allowed");
} else {
// 请求被限流
System.out.println("Request limited");
}
// 关闭Redisson客户端
redisson.shutdown();
}
}
在这个例子中,我们使用Redisson创建了一个分布式限流器,并设定了每秒钟产生10个令牌的速率。在实际业务中,可以根据需求调整速率和单位。
在业务代码中,void trySetRate(RateType type, long rate, long rateInterval, RateIntervalUnit rateIntervalUnit)
该方法是 Redisson 中用于设置令牌生成速率的方法,它用于初始化或更新令牌桶中令牌的产生速率。该方法通常用于初始化限流器的速率。
参数:
type
:限流器的类型,包括RateType.OVERALL
(总体速率,所有客户端共享速率)和RateType.PER_CLIENT
(每个客户端独立速率)。rate
:速率,即每个rateInterval
时间内生成的令牌数。rateInterval
:速率的时间间隔。rateIntervalUnit
:速率的时间间隔单位。
tryAcquire()
方法来尝试获取一个令牌(tryAcquire(n)
获取n个令牌),如果成功获取到令牌,则允许处理业务逻辑,否则请求被限流。
需要注意的是,使用Redisson的分布式限流器,要确保Redisson客户端与Redis服务器之间的连接是可靠的,以便正常地执行限流策略。
这些技术可以根据具体的应用场景和需求选择合适的实现方式。每种方式都有其优势和限制,需要根据具体情况进行权衡。
总结
限流算法是分布式系统中重要的一环,通过对请求进行精细控制,可以确保系统在高并发场景下仍然能够稳定运行。在选择限流算法时,需根据业务场景和性能需求进行选择,并结合实际情况调整参数。限流不仅仅是性能优化的手段,更是保障系统稳定性的一项重要工具。