多维度限流的实现
限流(Rate Limiting)是保护服务稳定性的重要手段,通常在短时间内大量请求涌入时,通过丢弃部分请求来保证系统稳定。限流的实现有多种方式,本文将介绍两种主要的限流方式:单实例限流和分布式限流,并探讨如何在实际应用中选择适合的限流模式。
单实例限流
单实例限流是针对单个服务实例设置的限流规则,主要用于防止某个实例因过载而崩溃。每个实例分配限流额度,确保单个实例接收到的QPS和Connection在设定阈值内,从而保障实例的稳定性。
示例应用场景: 假设在短视频APP中遇到突发热点新闻「xxx事件」,导致视频信息服务请求量激增。为避免服务崩溃,可以对请求量进行限流,确保部分用户能够正常使用APP,并进行服务扩容以进一步降低损失。
限流过滤器:
java
复制代码
public class RateLimiterFilter {
public void filterRequest(GatewayContext gatewayContext) {
// 示例代码,具体实现略
int statusCode = gatewayContext.getResponse().getHttpResponseStatus().code();
int responseBodySize = gatewayContext.getResponse().getFutureResponse().getResponseBodyAsBytes().length;
}
}
分布式限流
分布式限流通过SDK和中心存储(如Redis或MySQL)结合实现精确的集群限流模式。每个实例运行SDK限流逻辑,通过令牌桶算法向中心存储申请令牌,实现集群整体的流量限制。
优点:
- 能够精准控制集群整体的MAX QPS,即使限流值小于实例数。
- 实例的权重并不影响触发限流的概率,所有实例共享令牌桶。
缺点:
- 引入SDK和中心存储,增加运维成本。
- 请求前需要从中心存储申请令牌,带来一定性能开销。
常见限流算法
- 令牌桶算法:适合处理突发流量,每秒生成固定数量的令牌,请求需要消耗令牌才能被处理。
- 漏桶算法:通过固定速率处理请求,适合稳定流量的限流需求。
限流模式选择
- 单实例限流:适用于保护服务自身不被打垮的场景,如计算型服务、缓存型服务、代理型服务等。
- 分布式限流:适用于对集群整体限流精度有要求的服务,主要保护其依赖的第三方服务(数据库、缓存、中台服务等)。
实现限流器工厂接口
可以定义一个限流器工厂接口,用于创建不同类型的限流器:
java
复制代码
public interface RateLimiterFactory {
/**
* 获取对应的限流器
* @param serviceId
* @param flowControlConfig
* @return RateLimiter
*/
RateLimiter getRateLimiter(String serviceId, Rule.FlowControlConfig flowControlConfig);
}
Guava限流器实现
使用Guava的RateLimiter进行限流,示例代码如下:
public class GuavaRateLimiterFactory implements RateLimiterFactory {
private final Map<String, RateLimiter> resourceRateLimiterMap = new ConcurrentHashMap<>();
@Override
public RateLimiter getRateLimiter(String serviceId, Rule.FlowControlConfig flowControlConfig) {
return resourceRateLimiterMap.computeIfAbsent(serviceId, id -> {
if (flowControlConfig == null || flowControlConfig.getMaxPermits() <= 0) {
return null;
}
return RateLimiter.create(flowControlConfig.getMaxPermits());
});
}
}
Redis Lua脚本实现分布式限流
使用Lua脚本在Redis中实现限流器,确保操作的原子性。以下是Lua脚本的关键代码:
lua
复制代码
local num = redis.call('incr', KEYS[1])
if tonumber(num) == 1 then
redis.call('expire', KEYS[1], ARGV[1])
end
if tonumber(num) > tonumber(ARGV[2]) then
return 0
else
return 1
end
在Java代码中,使用Jedis库来执行Lua脚本,示例代码如下:
java
复制代码
public Boolean executeScript(String key, int expire, int limit) {
// Lua脚本字符串
String lua = buildLuaScript();
// 创建Jedis实例
Jedis jedis = new Jedis();
// 将Lua脚本加载到Redis,获取脚本的SHA-1哈希值
String scriptLoad = jedis.scriptLoad(lua);
// 执行Lua脚本
Object result = jedis.evalsha(scriptLoad, Arrays.asList(key), Arrays.asList(String.valueOf(expire), String.valueOf(limit)));
// 返回结果,1表示通过,0表示被限流
return result.equals(1L);
}
private String buildLuaScript() {
return "local num = redis.call('incr', KEYS[1])\n" +
"if tonumber(num) == 1 then\n" +
" redis.call('expire', KEYS[1], ARGV[1])\n" +
"end\n" +
"if tonumber(num) > tonumber(ARGV[2]) then\n" +
" return 0\n" +
"else\n" +
" return 1\n" +
"end";
}
结论
限流是保障服务稳定性的重要手段。在实际应用中,根据服务的不同特点和需求,可以选择合适的限流模式。单实例限流适用于保护服务自身,分布式限流则适用于对集群整体限流精度有要求的场景。通过合理配置限流规则,可以有效地提升服务的稳定性和抗压能力