顺便记录下,已经不是短视频电商了,经过好几个月的迭代和社会的毒打🐔🐔🐔,目前已经变成纯粹的私域团购类电商了,没得幺蛾子了。pyz裂开。。。🐔🐔🐔
爆炸凸(艹皿艹 ),最近由于合肥疫情👽,物流暂停自营这块要拉了😭😭😭
什么是限流?
在设计系统的时候需要对整个系统给一个预估容量,长时间超过系统能承受的TPS/QPS阈值,系统可能会被压垮,最终导致整个服务不够用。为了避免这种情况,我们就需要对整个服务进行限流。
另外实际业务中一些特殊的接口需要进行自定义限流,例如:
-
登录接口,限制每个设备 60秒 请求10次
-
发送短信验证码接口,限制每个设备 30秒 请求1次
-
下单接口,限制每个用户 60秒 请求10次
有哪些限流算法?
计数器算法
控制并发数量,控制某个资源可被同时访问的个数。在实际应用中可以通过信号量机制(如Java中的Semaphore)来实现。
Semaphore permit = new Semaphore(10, true); // 限制最大并发数是10
void process(){
try{
permit.acquire();
// 业务逻辑处理
} finally {
permit.release();
}
优点:算法实现简单,实现了并发数量的控制。
缺点:在一定程度上可以控制某资源的访问频率,但不能精确控制。
固定窗口算法
固定窗口算法的概念如下:
- 将时间划分为多个窗口
- 在每个窗口内每有一次请求就将计数器加一
- 如果计数器超过了限制数量,则本窗口内所有的请求都被丢弃,当时间到达下一个窗口时,计数器重置。
存在临界突发问题:限制 1 秒内最多通过 5 个请求,在第一个窗口的最后半秒内通过了 5 个请求,第二个窗口的前半秒内又通过了 5 个请求。这样看来就是在 1 秒内通过了 10 个请求。
缺点:存在临界突发问题,用户通过在时间窗口的重置节点处突发请求,可以瞬间超过我们的速率限制。用户有可能通过算法的这个漏洞,瞬间压垮我们的应用。
滑动窗口算法
滑动窗口算法概念如下:
- 将时间划分为多个区间;
- 在每个区间内每有一次请求就将计数器加一维持一个时间窗口,占据多个区间;
- 每经过一个区间的时间,则抛弃最老的一个区间,并纳入最新的一个区间;
- 如果当前窗口内区间的请求计数总和超过了限制数量,则本窗口内所有的请求都被丢弃。
滑动窗口计数器是通过将窗口再细分,并且按照时间 " 滑动 ",这种算法避免了固定窗口计数器带来的临界突发请求,但时间区间的精度越高,算法所需的空间容量就越大。
优点:弱化临界突发请求问题。
缺点:滑动窗口由于需要存储多份的计数器(每一个格子存一份),所以滑动窗口在实现上需要更多的存储空间。也就是说,如果滑动窗口的精度越高,需要的存储空间就越大。
漏桶算法
算法的示意图如下:
整个算法其实十分简单。首先,我们有一个固定容量的桶,有水流进来,也有水流出去。对于流进来的水来说,我们无法预计一共有多少水会流进来,也无法预计水流的速度。但是对于流出去的水来说,这个桶可以固定水流出的速率。而且,当桶满了之后,多余的水将会溢出。
- 流入请求速度不控制
- 以固定的速率漏出请求
优点:能强行限制出口的传输速率。
缺点:出口无法允许某种程度的突发传输。当短时间内有大量的突发请求时,即便此时服务器没有任何负载,每个请求也都得在队列中等待一段时间才能被响应。
令牌桶算法
算法的示意图如下:
令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务,令牌桶算法通过发放令牌,根据令牌的rate频率做请求频率限制,容量限制等。
- 令牌以固定速率放入桶中
- 允许出口处突发一下拿到桶中所有的令牌,出口允许突发流量流出。
优点:实现简单,且允许某些流量的突发,对用户友好,所以被业界采用地较多。
在传统的单体应用中限流只需要考虑到多线程即可,使用Google开源工具类guava即可。其中有一个RateLimiter专门实现了单体应用的限流,使用的是令牌桶算法。
限流实际手段
按照网络拓扑图从外到内依次限流为:
-
WAF可以设置整体服务的限流(防止CC攻击等)
-
Nginx限流 ,详情参考:从零开发短视频电商 利用Nginx限流保护应用安全
- 限制请求QPS
- 限制并发连接数
- 限制下载带宽等
-
阿里Sentinel限制业务API,下面是Sentinel的仓库地址与官方文档,读者也可以自己查阅文档学习:
-
应用Redis限流(可定制化程度高,只有这里方便实现一些业务上奇奇怪怪的限流。)
应用Redis限流示例
流量限制脚本
@Bean
public RedisTemplate<String, Serializable> limitRedisTemplate(LettuceConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Serializable> template = new RedisTemplate<>();
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
public DefaultRedisScript<Long> limitScript() {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("script/limit.lua")));
redisScript.setResultType(Long.class);
return redisScript;
}
limit.lua
local c
c = redis.call('get',KEYS[1])
-- 调用超过最大值,则直接返回-1
if c and tonumber(c) >= tonumber(ARGV[1]) then
return -1;
end
-- 执行计算器自加
c = redis.call('incr',KEYS[1])
if tonumber(c) == 1 then
-- 从第一次调用开始限流,设置对应键值的过期
redis.call('expire',KEYS[1],ARGV[2])
end
return c;
KEYS[1] 用来表示在redis 中用作键值的参数占位.
ARGV[1] 用来表示在redis 中用作参数的占位.
自定义限流注解
/**
* 限流类型
*/
public enum LimitType {
/**
* 全局限流
*/
ALL,
/**
* 根据请求者IP限流
*/
IP,
/**
* 根据请求者限流
*/
USER
}
/**
* 限流注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Limit {
/**
* 资源的key
* <p>
* 例如:limitType=IP.RedisKey为IP+key
*/
String key() default "";
/**
* 给定的时间段 单位秒
*/
int period() default 60;
/**
* 最多的访问限制次数
*/
int limit() default 10;
LimitType limitType() default LimitType.IP;
}
AOP拦截器
@Aspect
@Configuration
@Slf4j
public class LimitInterceptor {
private RedisTemplate<String, Serializable> redisTemplate;
private DefaultRedisScript<Long> limitScript;
@Autowired
public void setRedisTemplate(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Autowired
public void setLimitScript(DefaultRedisScript<Long> limitScript) {
this.limitScript = limitScript;
}
@Before("@annotation(limitAnnotation)")
public void interceptor(Limit limitAnnotation) {
String key = limitAnnotation.key();
LimitType limitType = limitAnnotation.limitType();
int limitPeriod = limitAnnotation.period();
int limitCount = limitAnnotation.limit();
switch (limitType) {
case IP:
key = StrUtil.join("-", getIpAddress(), key);
break;
case USER:
key = StrUtil.join("-", getUserID(), key);
break;
case ALL:
default:
}
try {
Long count = redisTemplate.execute(limitScript, CollUtil.newArrayList(key), limitCount, limitPeriod);
log.info("limitCount:{},current:{},cacheKey:{}", limitCount, count.intValue(), key);
// 超过限制会返回-1
if (count != null && count.intValue() == -1) {
throw new RuntimeException("LIMIT ERROR");
}
} catch (Exception e) {
throw new RuntimeException("服务器异常,请稍后再试");
}
}
限流示例
在需要限流的接口上添加注解@Limit(key = "laker", limit = 10, period = 60, limitType = LimitType.IP)
@GetMapping("/l")
@Limit(key = "laker", limit = 10, period = 60, limitType = LimitType.IP)
public String testlimit(String id) {
}
结果日志如下:
2022-03-23 23:59:55.105 INFO 22248 --- [nio-8080-exec-0] com.laker.cache.limit.LimitInterceptor : limitCount:10,current:1,cacheKey:0:0:0:0:0:0:0:1-laker
2022-03-23 23:59:55.265 INFO 22248 --- [nio-8080-exec-1] com.laker.cache.limit.LimitInterceptor : limitCount:10,current:2,cacheKey:0:0:0:0:0:0:0:1-laker
2022-03-23 23:59:55.514 INFO 22248 --- [nio-8080-exec-2] com.laker.cache.limit.LimitInterceptor : limitCount:10,current:3,cacheKey:0:0:0:0:0:0:0:1-laker
2022-03-23 23:59:55.778 INFO 22248 --- [nio-8080-exec-3] com.laker.cache.limit.LimitInterceptor : limitCount:10,current:4,cacheKey:0:0:0:0:0:0:0:1-laker
2022-03-23 23:59:56.042 INFO 22248 --- [nio-8080-exec-4] com.laker.cache.limit.LimitInterceptor : limitCount:10,current:5,cacheKey:0:0:0:0:0:0:0:1-laker
2022-03-23 23:59:56.291 INFO 22248 --- [nio-8080-exec-5] com.laker.cache.limit.LimitInterceptor : limitCount:10,current:6,cacheKey:0:0:0:0:0:0:0:1-laker
2022-03-23 23:59:56.538 INFO 22248 --- [nio-8080-exec-6] com.laker.cache.limit.LimitInterceptor : limitCount:10,current:7,cacheKey:0:0:0:0:0:0:0:1-laker
2022-03-23 23:59:56.787 INFO 22248 --- [nio-8080-exec-7] com.laker.cache.limit.LimitInterceptor : limitCount:10,current:8,cacheKey:0:0:0:0:0:0:0:1-laker
2022-03-23 23:59:57.095 INFO 22248 --- [nio-8080-exec-8] com.laker.cache.limit.LimitInterceptor : limitCount:10,current:9,cacheKey:0:0:0:0:0:0:0:1-laker
2022-03-23 23:59:57.337 INFO 22248 --- [nio-8080-exec-9] com.laker.cache.limit.LimitInterceptor : limitCount:10,current:10,cacheKey:0:0:0:0:0:0:0:1-laker
2022-03-23 23:59:57.602 INFO 22248 --- [io-8080-exec-10] com.laker.cache.limit.LimitInterceptor : limitCount:10,current:-1,cacheKey:0:0:0:0:0:0:0:1-laker
java.lang.RuntimeException: 服务器异常,请稍后再试
at com.laker.cache.limit.LimitInterceptor.interceptor(LimitInterceptor.java:72) ~[classes/:na]
at sun.reflect.GeneratedMethodAccessor35.invoke(Unknown Source) ~[na:na]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_102]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_102]
参考:
- https://blog.csdn.net/chenglc1612/article/details/103060270
- https://www.cnblogs.com/Chenjiabing/p/12534346.html
- https://www.cnblogs.com/carrychan/p/9435979.html