分享一个在公司代码学习到的分布式阻塞式限流器
之前学习了一个# 令牌桶限流器,但是这个适合单台服务器内部限流,今天又学习到了一个分布式的阻塞式限流器。分布式限流器只要用于多节点需要统一控制访问量的场景。通常需要依赖Redis或者ZK这种中间件来实现。
话不多说,直接贴代码,看实现。
1.分布式限流器的方法定义
java
/**
* 分布式阻塞式限流.
*
* @param key the key
* @param period the period,时间单位:秒
* @param count the count
* @param microseconds the microseconds of block
* @return the boolean
*/
boolean blockLimit(final String key, final int period, final int count, final int microseconds);
参数 | 含义 | 举例 |
---|---|---|
key |
用来区分限流对象的唯一标识,可以是接口名、用户ID、IP等 | "user:123:sendSms" 表示用户123发送短信的限流 |
period |
限流的时间窗口(单位:秒) ,在这个时间段内做限流控制 | 10 表示 10 秒内最多只能访问一定次数 |
count |
时间窗口内允许的最大访问次数 | 5 表示 10 秒内最多允许访问 5 次 |
microseconds |
超过限流阈值后,最多可以等待多久(单位:微秒) ,如果等待不到令牌,就返回失败 | 2_000_000 表示最多等 2 秒 |
2.分布式限流器方法的实现
arduino
/** 阻塞式限流 */
@Override
public boolean blockLimit(
final String key, final int period, final int count, final int microseconds) {
if (isNotFull(key, period, count)) {
return true;
} else {
int times = 0;
// 自旋锁阻塞
while (times * 100 < microseconds) {
times++;
try {
TimeUnit.MILLISECONDS.sleep(100);
if (isNotFull(key, period, count)) {
logger.info("自旋{}ms后获取锁", times * 100);
return true;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
logger.error("项目{}阻塞{}ms后,仍然没有获得锁,不再等待锁,继续执行", key, microseconds);
return false;
}
解释:在并发环境下,如果请求超过了设定的阈值(即:在指定时间内请求次数过多),则阻塞等待一段时间(通过自旋轮询判断是否可以放行) ,如果超时仍未获得资格,就返回 false
,代表限流失败。
3.接着贴出isNotFull的实现:
vbnet
@Override
public boolean isNotFull(String key, int period, int count) {
ImmutableList<String> keys = ImmutableList.of(StringUtils.join("limit", ":", key));
String luaScript = buildLuaScript();
RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
Long currentCount = stringRedisTemplate.execute(redisScript, keys, count, period);
return Objects.nonNull(currentCount) && currentCount <= count;
}
解释:在指定的时间窗口 period
内,判断某个操作(用 key
标识)是否已经超过了允许的请求次数 count
。 如果没有超限,返回 true
,可以执行;如果已达上限,返回 false
,触发限流
这里需要贴出lua脚本的代码。
lua:
swift
private @NotNull String buildLuaScript() {
return "local c"
+ "\nc = redis.call('get',KEYS[1])"
+
// 调用不超过最大值,则直接返回
"\nif c and tonumber(c) > tonumber(ARGV[1]) then"
+ "\nreturn tonumber(c);"
+ "\nend"
+
// 执行计算器自加
"\nc = redis.call('incr',KEYS[1])"
+ "\nif tonumber(c) == 1 then"
+
// 从第一次调用开始限流,设置对应键值的过期时间
"\nredis.call('expire',KEYS[1],ARGV[2])"
+ "\nend"
+ "\nreturn tonumber(c);";
}
解释:在固定的时间窗口 ARGV[2]
(比如 60 秒)内,判断某个 key(KEYS[1]
)对应的访问次数是否超过了阈值 ARGV[1]
(比如 100 次),如果没有,就允许放行并自动计数;否则拒绝。
当达到窗口时间的限流上限后,不会一直阻塞。设置的时间窗口结束后,Redis 中的 key 会自动过期,限流计数器就会重置,新的请求会被放行,这时便会重新自增计数。
最后总结一下这个分布式阻塞式限流器的整体思想: 当一个接口不止一个实例在调用时,我们需要在 多个节点之间统一地控制访问频率 如果超过了限制,不是立刻返回失败,而是等等看行不行(阻塞式的限流)。
总的来说就是通过 Redis + Lua 脚本实现固定时间窗口内的原子计数操作,结合业务代码进行自旋阻塞,确保请求在限流条件下有序接入。当某个 key 的请求次数在指定周期(如 1 秒)内超过设定上限时,系统通过循环等待(微秒级休眠)方式尝试重试,直到计数器被自动重置(键过期)后才允许继续处理,避免直接拒绝请求。