Java最全可以了,基于Redis和Lua实现分布式令牌桶限流(2),分布式架构+RPC+kafka+多线程

更多:Java进阶核心知识集

包含:JVM,JAVA集合,网络,JAVA多线程并发,JAVA基础,Spring原理,微服务,Zookeeper,Kafka,RabbitMQ,Hbase,MongoDB,Cassandra,设计模式,负载均衡,数据库,一致性哈希,JAVA算法,数据结构,加密算法,分布式缓存等等

image

高效学习视频

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

可以了,基于Redis和Lua实现分布式令牌桶限流

限流业务的实现:

这里贴出限流业务的核心方法,通过调用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 是核心逻辑。这是我这边使用的令牌桶方案,都加了注解,如果看不懂就多看几遍,还是看不明白就看最后我的流程图。

–[[

  1. key - 令牌桶的 key

  2. intervalPerTokens - 生成令牌的间隔(ms)

  3. curTime - 当前时间

  4. initTokens - 令牌桶初始化的令牌数

  5. bucketMaxTokens - 令牌桶的上限

  6. resetBucketInterval - 重置桶内令牌的时间间隔

  7. currentTokens - 当前桶内令牌数

  8. 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。

可以了,基于Redis和Lua实现分布式令牌桶限流

从脚本逻辑上来说,就分成了三个步骤,分别是:

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);

总结

总的来说,面试是有套路的,一面基础,二面架构,三面个人。

最后,小编这里收集整理了一些资料,其中包括面试题(含答案)、书籍、视频等。希望也能帮助想进大厂的朋友

三面蚂蚁金服成功拿到offer后,他说他累了

三面蚂蚁金服成功拿到offer后,他说他累了

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

flowControl.doFilter(controlConfig);

if (res) {

apiRequestFailedCount.put(apiId, apiRequestFailedCount.getOrDefault(apiId, 0) + 1);

总结

总的来说,面试是有套路的,一面基础,二面架构,三面个人。

最后,小编这里收集整理了一些资料,其中包括面试题(含答案)、书籍、视频等。希望也能帮助想进大厂的朋友

[外链图片转存中…(img-NIiOArMN-1715340826656)]

[外链图片转存中…(img-L6J6BuHO-1715340826656)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 19
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现分布式限流可以使用 RedisLua 脚本来完成。以下是可能的实现方案: 1. 使用 Redis 的 SETNX 命令来实现基于令牌桶算法的限流 令牌桶算法是一种常见的限流算法,它可以通过令牌的放置和消耗来控制流量。在 Redis 中,我们可以使用 SETNX 命令来实现令牌桶算法。 具体实现步骤如下: - 在 Redis 中创建一个有序集合,用于存储令牌桶的令牌数量和时间戳。 - 每当一个请求到达时,我们首先获取当前令牌桶中的令牌数量和时间戳。 - 如果当前时间戳与最后一次请求的时间戳之差大于等于令牌桶中每个令牌的发放时间间隔,则将当前时间戳更新为最后一次请求的时间戳,并且将令牌桶中的令牌数量增加相应的数量,同时不超过最大容量。 - 如果当前令牌桶中的令牌数量大于等于请求需要的令牌数量,则返回 true 表示通过限流,将令牌桶中的令牌数量减去请求需要的令牌数量。 - 如果令牌桶中的令牌数量不足,则返回 false 表示未通过限流。 下面是使用 RedisLua 脚本实现令牌桶算法的示例代码: ```lua -- 限流的 key local key = KEYS[1] -- 令牌桶的容量 local capacity = tonumber(ARGV[1]) -- 令牌的发放速率 local rate = tonumber(ARGV[2]) -- 请求需要的令牌数量 local tokens = tonumber(ARGV[3]) -- 当前时间戳 local now = redis.call('TIME')[1] -- 获取当前令牌桶中的令牌数量和时间戳 local bucket = redis.call('ZREVRANGEBYSCORE', key, now, 0, 'WITHSCORES', 'LIMIT', 0, 1) -- 如果令牌桶为空,则初始化令牌桶 if not bucket[1] then redis.call('ZADD', key, now, capacity - tokens) return 1 end -- 计算当前令牌桶中的令牌数量和时间戳 local last = tonumber(bucket[2]) local tokensInBucket = tonumber(bucket[1]) -- 计算时间间隔和新的令牌数量 local timePassed = now - last local newTokens = math.floor(timePassed * rate) -- 更新令牌桶 if newTokens > 0 then tokensInBucket = math.min(tokensInBucket + newTokens, capacity) redis.call('ZADD', key, now, tokensInBucket) end -- 检查令牌数量是否足够 if tokensInBucket >= tokens then redis.call('ZREM', key, bucket[1]) return 1 else return 0 end ``` 2. 使用 RedisLua 脚本来实现基于漏桶算法的限流 漏桶算法是另一种常见的限流算法,它可以通过漏桶的容量和漏水速度来控制流量。在 Redis 中,我们可以使用 Lua 脚本来实现漏桶算法。 具体实现步骤如下: - 在 Redis 中创建一个键值对,用于存储漏桶的容量和最后一次请求的时间戳。 - 每当一个请求到达时,我们首先获取当前漏桶的容量和最后一次请求的时间戳。 - 计算漏水速度和漏水的数量,将漏桶中的容量减去漏水的数量。 - 如果漏桶中的容量大于等于请求需要的容量,则返回 true 表示通过限流,将漏桶中的容量减去请求需要的容量。 - 如果漏桶中的容量不足,则返回 false 表示未通过限流。 下面是使用 RedisLua 脚本实现漏桶算法的示例代码: ```lua -- 限流的 key local key = KEYS[1] -- 漏桶的容量 local capacity = tonumber(ARGV[1]) -- 漏水速度 local rate = tonumber(ARGV[2]) -- 请求需要的容量 local size = tonumber(ARGV[3]) -- 当前时间戳 local now = redis.call('TIME')[1] -- 获取漏桶中的容量和最后一次请求的时间戳 local bucket = redis.call('HMGET', key, 'capacity', 'last') -- 如果漏桶为空,则初始化漏桶 if not bucket[1] then redis.call('HMSET', key, 'capacity', capacity, 'last', now) return 1 end -- 计算漏水的数量和漏桶中的容量 local last = tonumber(bucket[2]) local capacityInBucket = tonumber(bucket[1]) local leak = math.floor((now - last) * rate) -- 更新漏桶 capacityInBucket = math.min(capacity, capacityInBucket + leak) redis.call('HSET', key, 'capacity', capacityInBucket) redis.call('HSET', key, 'last', now) -- 检查容量是否足够 if capacityInBucket >= size then return 1 else return 0 end ``` 以上是使用 RedisLua 脚本实现分布式限流的两种方案,可以根据实际需求选择适合的方案。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值