更多:Java进阶核心知识集
包含:JVM,JAVA集合,网络,JAVA多线程并发,JAVA基础,Spring原理,微服务,Zookeeper,Kafka,RabbitMQ,Hbase,MongoDB,Cassandra,设计模式,负载均衡,数据库,一致性哈希,JAVA算法,数据结构,加密算法,分布式缓存等等
高效学习视频
限流业务的实现:
这里贴出限流业务的核心方法,通过调用doFilter 方法实现判断是否需要进行限流。具体调用哪一种限流器通过这两个对象实现的:LimiterStrategy 与 LimiterStrategy 分别是具体的限流算法与限流策略。
@Override
public boolean doFilter(FlowControlConfig flowControlConfig) {
if (Objects.isNull(flowControlConfig)) {
log.error(“[{}] 流控参数为空”, this.getClass().getSimpleName());
return true;
}
String key;
boolean filterRes = true;
try {
key = generateRedisLimiterKey(flowControlConfig);
LimiterStrategy limiterStrategy = getLimiterStrategyByCode(flowControlConfig.getLimiterType());
LimiterPolicy limiterPolicy = getLimiterPolicyByCode(flowControlConfig.getLimiterType(), flowControlConfig);
filterRes = limiterStrategy.access(key, limiterPolicy);
if (!filterRes) {
log.warn(“Limiter Id:[{}],key :[{}]已达流量上限值:{},被限制请求!”, flowControlConfig.getId(), key, flowControlConfig.getValue());
// todo 接入消息告警
}
} catch (Exception e) {
log.error(“[{}] 限流器内部出现异常! 入参:{}”, this.getClass().getSimpleName(), JSONObject.toJSON(flowControlConfig));
e.printStackTrace();
}
return !filterRes;
}
令牌桶限流器算法的对象:
package com.teavamc.rpcgateway.core.flow.limiter.policy;
import com.google.common.collect.Lists;
import java.util.List;
/**
-
令牌桶限流器的执行对象
-
@Package com.teavamc.rpcgateway.core.limiter.policy
-
@date 2021/1/28 上午11:09
*/
public class TokenBucketLimiterPolicy extends AbstractLimiterPolicy {
/**
-
限流时间间隔
-
(重置桶内令牌的时间间隔)
*/
private final long resetBucketInterval;
/**
- 最大令牌数量
*/
private final long bucketMaxTokens;
/**
- 初始可存储数量
*/
private final long initTokens;
/**
- 每个令牌产生的时间
*/
private final long intervalPerPermit;
/**
-
令牌桶对象的构造器
-
@param bucketMaxTokens 桶的令牌上限
-
@param resetBucketInterval 限流时间间隔
-
@param maxBurstTime 最大的突发流量的持续时间(通过计算)
*/
public TokenBucketLimiterPolicy(long bucketMaxTokens, long resetBucketInterval, long maxBurstTime) {
// 最大令牌数
this.bucketMaxTokens = bucketMaxTokens;
// 限流时间间隔
this.resetBucketInterval = resetBucketInterval;
// 令牌的产生间隔 = 限流时间 / 最大令牌数
intervalPerPermit = resetBucketInterval / bucketMaxTokens;
// 初始令牌数 = 最大的突发流量的持续时间 / 令牌产生间隔
// 用 最大的突发流量的持续时间 计算的结果更加合理,并不是每次初始化都要将桶装满
initTokens = Math.min(maxBurstTime / intervalPerPermit, bucketMaxTokens);
}
public long getResetBucketInterval() {
return resetBucketInterval;
}
public long getBucketMaxTokens() {
return bucketMaxTokens;
}
public long getInitTokens() {
return initTokens;
}
public long getIntervalPerPermit() {
return intervalPerPermit;
}
@Override
public String[] toParams() {
List list = Lists.newArrayList();
list.add(String.valueOf(getIntervalPerPermit()));
list.add(String.valueOf(System.currentTimeMillis()));
list.add(String.valueOf(getInitTokens()));
list.add(String.valueOf(getBucketMaxTokens()));
list.add(String.valueOf(getResetBucketInterval()));
return list.toArray(new String[]{});
}
}
这个代码已经写得很明白了,东西也不多。但是构造器这里还是要理解一下,特别是maxBurstTime 这个字段,记录这个 api 经历的最大突发流量的时间。
Lua 脚本的解析:
令牌桶的实现是通过 lua 来完成的,所以 lua 是核心逻辑。这是我这边使用的令牌桶方案,都加了注解,如果看不懂就多看几遍,还是看不明白就看最后我的流程图。
–[[
-
key - 令牌桶的 key
-
intervalPerTokens - 生成令牌的间隔(ms)
-
curTime - 当前时间
-
initTokens - 令牌桶初始化的令牌数
-
bucketMaxTokens - 令牌桶的上限
-
resetBucketInterval - 重置桶内令牌的时间间隔
-
currentTokens - 当前桶内令牌数
-
bucket - 当前 key 的令牌桶对象
]] –
local key = KEYS[1]
local intervalPerTokens = tonumber(ARGV[1])
local curTime = tonumber(ARGV[2])
local initTokens = tonumber(ARGV[3])
local bucketMaxTokens = tonumber(ARGV[4])
local resetBucketInterval = tonumber(ARGV[5])
local bucket = redis.call(‘hgetall’, key)
local currentTokens
– 若当前桶未初始化,先初始化令牌桶
if table.maxn(bucket) == 0 then
– 初始桶内令牌
currentTokens = initTokens
– 设置桶最近的填充时间是当前
redis.call(‘hset’, key, ‘lastRefillTime’, curTime)
– 初始化令牌桶的过期时间, 设置为间隔的 1.5 倍
redis.call(‘pexpire’, key, resetBucketInterval * 1.5)
– 若桶已初始化,开始计算桶内令牌
– 为什么等于 4 ? 因为有两对 field, 加起来长度是 4
– { “lastRefillTime(上一次更新时间)”,“curTime(更新时间值)”,“tokensRemaining(当前保留的令牌)”,“令牌数” }
elseif table.maxn(bucket) == 4 then
– 上次填充时间
local lastRefillTime = tonumber(bucket[2])
– 剩余的令牌数
local tokensRemaining = tonumber(bucket[4])
– 当前时间大于上次填充时间
if curTime > lastRefillTime then
– 拿到当前时间与上次填充时间的时间间隔
– 举例理解: curTime = 2620 , lastRefillTime = 2000, intervalSinceLast = 620
local intervalSinceLast = curTime - lastRefillTime
– 如果当前时间间隔 大于 令牌的生成间隔
– 举例理解: intervalSinceLast = 620, resetBucketInterval = 1000
if intervalSinceLast > resetBucketInterval then
– 将当前令牌填充满
currentTokens = initTokens
– 更新重新填充时间
redis.call(‘hset’, key, ‘lastRefillTime’, curTime)
– 如果当前时间间隔 小于 令牌的生成间隔
else
– 可授予的令牌 = 向下取整数( 上次填充时间与当前时间的时间间隔 / 两个令牌许可之间的时间间隔 )
– 举例理解 : intervalPerTokens = 200 ms , 令牌间隔时间为 200ms
– intervalSinceLast = 620 ms , 当前距离上一个填充时间差为 620ms
– grantedTokens = 620/200 = 3.1 = 3
local grantedTokens = math.floor(intervalSinceLast / intervalPerTokens)
– 可授予的令牌 > 0 时
– 举例理解 : grantedTokens = 620/200 = 3.1 = 3
if grantedTokens > 0 then
– 生成的令牌 = 上次填充时间与当前时间的时间间隔 % 两个令牌许可之间的时间间隔
– 举例理解 : padMillis = 620%200 = 20
– curTime = 2620
– curTime - padMillis = 2600
local padMillis = math.fmod(intervalSinceLast, intervalPerTokens)
– 将当前令牌桶更新到上一次生成时间
redis.call(‘hset’, key, ‘lastRefillTime’, curTime - padMillis)
end
– 更新当前令牌桶中的令牌数
– Math.min(根据时间生成的令牌数 + 剩下的令牌数, 桶的限制) => 超出桶最大令牌的就丢弃
currentTokens = math.min(grantedTokens + tokensRemaining, bucketMaxTokens)
end
else
– 如果当前时间小于或等于上次更新的时间, 说明刚刚初始化, 当前令牌数量等于桶内令牌数
– 不需要重新填充
currentTokens = tokensRemaining
end
end
– 如果当前桶内令牌小于 0,抛出异常
assert(currentTokens >= 0)
– 如果当前令牌 == 0 ,更新桶内令牌, 返回 0
if currentTokens == 0 then
redis.call(‘hset’, key, ‘tokensRemaining’, currentTokens)
return 0
else
– 如果当前令牌 大于 0, 更新当前桶内的令牌 -1 , 再返回当前桶内令牌数
redis.call(‘hset’, key, ‘tokensRemaining’, currentTokens - 1)
return currentTokens
end
其实这个脚本很简单,一个 key 拥有一个令牌桶,令牌桶是通过 Redis 中的 Hash 数据类型进行储存的。每个令牌桶拥有两个 field,分别是上一次填充时间lastRefillTime与当前桶内令牌数量tokensRemaining。
从脚本逻辑上来说,就分成了三个步骤,分别是:
1.确认 key 的令牌桶是否存在,如果不存在就初始化。
2.计算并更新当前令牌桶内的令牌数量:
-
如果当前距离上次填充令牌的时间间隔超出重置时间,就重置令牌桶。
-
计算距离上次填充的时间间隔是否超过了生产令牌的间隔时间,若大于间隔就计算生产了多少令牌与上次产生令牌的时间。
-
若距离上次填充至今没有产生令牌就直接用。
3.明确了当前桶内的令牌数之后,就判断是否放行:
-
令牌等于 0,返回 0,不放行。
-
令牌大于0,减少一个当前的桶内令牌,放行。
限流器的模拟使用:
开启一个接口,模拟对接口并发调用。
@PostMapping(value = “/test”)
public void testFlowControl(@RequestBody FlowControlConfig controlConfig) {
Long apiId = controlConfig.getId();
log.info(“接收到 ApiId :{} 的请求”, apiId);
apiRequestCount.put(apiId, apiRequestCount.getOrDefault(apiId, 0) + 1);
// 执行限流
boolean res = flowControl.doFilter(controlConfig);
if (res) {
apiRequestFailedCount.put(apiId, apiRequestFailedCount.getOrDefault(apiId, 0) + 1);
总结
总的来说,面试是有套路的,一面基础,二面架构,三面个人。
最后,小编这里收集整理了一些资料,其中包括面试题(含答案)、书籍、视频等。希望也能帮助想进大厂的朋友
flowControl.doFilter(controlConfig);
if (res) {
apiRequestFailedCount.put(apiId, apiRequestFailedCount.getOrDefault(apiId, 0) + 1);
总结
总的来说,面试是有套路的,一面基础,二面架构,三面个人。
最后,小编这里收集整理了一些资料,其中包括面试题(含答案)、书籍、视频等。希望也能帮助想进大厂的朋友
[外链图片转存中…(img-NIiOArMN-1715340826656)]
[外链图片转存中…(img-L6J6BuHO-1715340826656)]