参考资料
https://blog.youkuaiyun.com/xushiyu1996818/article/details/106764890/
https://zhuanlan.zhihu.com/p/60979444
https://blog.youkuaiyun.com/fubicheng208/article/details/106650146/
https://blog.youkuaiyun.com/weixin_38019299/article/details/120883690
流量控制
流量控制在计算机领域称为过载保护。何为过载保护?所谓“过载”,即需求超过了负载能力;而“保护”则是指当“过载”发生了,采取必要的措施保护自己不受“伤害”。在计算机领域,尤其是分布式系统领域,“过载保护”是一个重要的概念。一个不具备“过载保护”功能的系统,是非常危险和脆弱的,很可能由于瞬间的压力激增,引起“雪崩效应”,导致系统的各个部分都同时崩溃,停止服务。这就好像在没有保险丝的保护下,电压突然变高,导致所有的电器都会被损坏一样,“过载保护”功能是系统的“保险丝”。
接口限流——redis + lua脚本
主要是借助redis做限流,通过aop或拦截器做限流,利用lua脚步是为了做到原子性。
Lua
脚本和MySQL
数据库的存储过程比较相似,他们执行一组命令,所有命令的执行要么全部成功或者失败,以此达到原子性。也可以把Lua
脚本理解为,一段具有业务逻辑的代码块。而Lua
本身就是一种编程语言,虽然redis
官方没有直接提供限流相应的API
,但却支持了Lua
脚本的功能,可以使用它实现复杂的令牌桶或漏桶算法,也是分布式系统中实现限流的主要方式之一。
相比Redis
事务,Lua脚本
的优点:
- 减少网络开销:使用
Lua
脚本,无需向Redis
发送多次请求,执行一次即可,减少网络传输 - 原子操作:
Redis
将整个Lua
脚本作为一个命令执行,原子,无需担心并发 - 复用:
Lua
脚本一旦执行,会永久保存Redis
中,,其他客户端可复用
Lua
脚本大致逻辑如下:
-- 获取调用脚本时传入的第一个key值(用作限流的 key)
local key = KEYS[1]
-- 获取调用脚本时传入的第一个参数值(限流大小)
local limit = tonumber(ARGV[1])
-- 获取当前流量大小
local curentLimit = tonumber(redis.call('get', key) or "0")
-- 是否超出限流
if curentLimit + 1 > limit then
-- 返回(拒绝)
return 0
else
-- 没有超出 value + 1
redis.call("INCRBY", key, 1)
-- 设置过期时间
redis.call("EXPIRE", key, 2)
-- 返回(放行)
return 1
end
- 通过
KEYS[1]
获取传入的key参数 - 通过
ARGV[1]
获取传入的limit
参数 redis.call
方法,从缓存中get
和key
相关的值,如果为null
那么就返回0- 接着判断缓存中记录的数值是否会大于限制大小,如果超出表示该被限流,返回0
- 如果未超过,那么该key的缓存值+1,并设置过期时间为1秒钟以后,并返回缓存值+1
限流常在网关这一层做,比如
Nginx
、Openresty
、kong
、zuul
、Spring Cloud Gateway
等,而像spring cloud - gateway
网关限流底层实现原理,就是基于Redis + Lua
,通过内置Lua
限流脚本的方式。
核心代码
public boolean acquire(String key) {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<Long>();
String lua = "local key = KEYS[1]" +
" local period = ARGV[1]" +
" local limit= ARGV[2]" +
" local times = redis.call('incr',key)" +
" if times == 1 then" +
" redis.call('expire',KEYS[1], period)" +
" end" +
" if times > tonumber(limit) then" +
" return 0" +
" end" +
" return 1";
redisScript.setScriptText(lua);
redisScript.setResultType(Long.class);
//表示1s 内最多访问3次
//key [key ...],被操作的key,可以多个,在lua脚本中通过KEYS[1], KEYS[2]获取
//arg [arg ...],参数,可以多个,在lua脚本中通过ARGV[1], ARGV[2]获取。
//0: 超出限制,else:正常请求
Long count = (Long) stringRedisTemplate.execute(redisScript, Arrays.asList(key), "1", "3");
System.err.println(System.currentTimeMillis() + "<>" + count);
//0:超出范围
return count == 0;
JAVA SDK限流
原理看这篇:流量控制算法总结_xushiyu1996818的博客-优快云博客_流量控制
令牌桶算法
https://zhuanlan.zhihu.com/p/60979444
令牌桶算法的原理也比较简单,我们可以理解成医院的挂号看病,只有拿到号以后才可以进行诊病。
系统会维护一个令牌(
token
)桶,以一个恒定的速度往桶里放入令牌(token
),这时如果有请求进来想要被处理,则需要先从桶里获取一个令牌(token
),当桶里没有令牌(token
)可取时,则该请求将被拒绝服务。令牌桶算法通过控制桶的容量、发放令牌的速率,来达到对请求的限制。
生成令牌的速度是恒定的,而请求去拿令牌是没有速度限制的。这意味,面对瞬时大流量,该算法可以在短时间内请求拿到大量令牌,而且拿令牌的过程并不是消耗很大的事情。
实现方式: RateLimiter
- RateLimiter在并发环境下使用是安全的:它将限制所有线程调用的总速率。注意,它不保证公平调用。Rate limiter(直译为:速度限制器)经常被用来限制一些物理或者逻辑资源的访问速率。这和java.util.concurrent.Semaphore正好形成对照。
- 一个RateLimiter主要定义了发放permits的速率。如果没有额外的配置,permits将以固定的速度分配,单位是每秒多少permits。默认情况下,Permits将会被稳定的平缓的发放。
- 可以配置一个RateLimiter有一个预热期,在此期间permits的发放速度每秒稳步增长直到到达稳定的速率。
核心代码
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>26.0-jre</version>
<!-- or, for Android: -->
<version>26.0-android</version>
</dependency>
//一秒创建3个令牌
RateLimiter rateLimiter = RateLimiter.create(3.0);
public void query() throws InterruptedException {
//尝试获取令牌
if (rateLimiter.tryAcquire()) {
System.err.println(System.currentTimeMillis() + " 业务执行成功!");
TimeUnit.MILLISECONDS.sleep(200);
} else {
TimeUnit.MILLISECONDS.sleep(200);
System.err.println("************ " + System.currentTimeMillis() + " 业务执行失败!");
}
}
自己实现:
long timeStamp=getNowTime();
int capacity; // 桶的容量
int rate ;//令牌放入速度
int tokens;//当前水量
bool control() {
//先执行添加令牌的操作
long now = getNowTime();
tokens = max(capacity, tokens+ (now - timeStamp)*rate);
timeStamp = now; //令牌已用完,拒绝访问
if(tokens<1){
return false;
}else{//还有令牌,领取令牌
tokens--;
retun true;
}
}
漏桶算法
漏桶算法思路很简单,我们把水比作是
请求
,漏桶比作是系统处理能力极限
,水先进入到漏桶里,漏桶里的水按一定速率流出,当流出的速率小于流入的速率时,由于漏桶容量有限,后续进入的水直接溢出(拒绝请求),以此实现限流。
漏桶的出水速度是恒定的,那么意味着如果瞬时大流量的话,将有大部分请求被丢弃掉(也就是所谓的溢出)。
漏桶算法没有标准的sdk提供实现,可以自己去实现一个漏桶算法。
long timeStamp = getNowTime();
int capacity = 10000;// 桶的容量
int rate = 1;//水漏出的速度
int water = 100;//当前水量
public static bool control() {
//先执行漏水,因为rate是固定的,所以可以认为“时间间隔*rate”即为漏出的水量
long now = getNowTime();
water = Math.max(0, water - (now - timeStamp) * rate);
timeStamp = now;
if (water < capacity) { // 水还未满,加水
water ++;
return true;
} else {
return false;//水满,拒绝加水
}
}
注意:这里的timeStamp是上一次执行操作的时间,这次操作,要先执行漏水,漏水量是now-timeStamp 相关的函数
该算法很好的解决了时间边界处理不够平滑的问题,因为在每次请求进桶前都将执行“漏水”的操作,再无边界问题。
但是对于很多场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合
二者区别
两者主要区别在于“漏桶算法”能够强行限制数据的传输速率,而“令牌桶算法”在能够限制数据的平均传输速率外,还允许某种程度的突发传输。
在“令牌桶算法”中,只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的上限,所以它适合于具有突发特性的流量。
分布式限流——阿里Sentinel
官网:Sentinel · alibaba/spring-cloud-alibaba Wiki · GitHub
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
- docker部署—> 导包 —> 配置
- 想要给某个方法限流,就在方法上加注解(
@SentinelResource(value = "brand_list" ,blockHandlerClass = {SentinelUtil.class}, blockHander = handerException
); - 其中
value
为资源名、后面的blockHandlerClass
是流量超了之后的处理方式、规则所在的类、blockHander
是方法名,叫兜底方法 - 兜底方法必须与原方法的参数(多一个异常)、返回值类型必须一样
- 如果从控制台配置限流规则,则是存在了内存中,一旦重启就没了;所以使用nacos的配置中心
今日推歌
----《秘密》 张震岳
也许在你的心中早就已经有人进去
或许你不曾接受真正的爱真诚的情
遗忘吧过去的事
不要再怀疑
我彷佛可以听见你的心跳你的声音
不要只有在梦中才能看你才能靠近
我可以慢慢的等
直到你离去