从零开发短视频电商 限流之全局Nginx限流,Api限流,特定业务接口限流

顺便记录下,已经不是短视频电商了,经过好几个月的迭代和社会的毒打🐔🐔🐔,目前已经变成纯粹的私域团购类电商了,没得幺蛾子了。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();
    }

优点:算法实现简单,实现了并发数量的控制

缺点:在一定程度上可以控制某资源的访问频率,但不能精确控制。

固定窗口算法

img

固定窗口算法的概念如下

  1. 将时间划分为多个窗口
  2. 在每个窗口内每有一次请求就将计数器加一
  3. 如果计数器超过了限制数量,则本窗口内所有的请求都被丢弃,当时间到达下一个窗口时,计数器重置。

存在临界突发问题:限制 1 秒内最多通过 5 个请求,在第一个窗口的最后半秒内通过了 5 个请求,第二个窗口的前半秒内又通过了 5 个请求。这样看来就是在 1 秒内通过了 10 个请求。

img

缺点:存在临界突发问题,用户通过在时间窗口的重置节点处突发请求,可以瞬间超过我们的速率限制。用户有可能通过算法的这个漏洞,瞬间压垮我们的应用。

滑动窗口算法

img

滑动窗口算法概念如下:

  1. 将时间划分为多个区间;
  2. 在每个区间内每有一次请求就将计数器加一维持一个时间窗口,占据多个区间;
  3. 每经过一个区间的时间,则抛弃最老的一个区间,并纳入最新的一个区间;
  4. 如果当前窗口内区间的请求计数总和超过了限制数量,则本窗口内所有的请求都被丢弃。

滑动窗口计数器是通过将窗口再细分,并且按照时间 " 滑动 ",这种算法避免了固定窗口计数器带来的临界突发请求,但时间区间的精度越高,算法所需的空间容量就越大。

优点:弱化临界突发请求问题。

缺点:滑动窗口由于需要存储多份的计数器(每一个格子存一份),所以滑动窗口在实现上需要更多的存储空间。也就是说,如果滑动窗口的精度越高,需要的存储空间就越大

漏桶算法

算法的示意图如下

image

整个算法其实十分简单。首先,我们有一个固定容量的桶,有水流进来,也有水流出去。对于流进来的水来说,我们无法预计一共有多少水会流进来,也无法预计水流的速度。但是对于流出去的水来说,这个桶可以固定水流出的速率。而且,当桶满了之后,多余的水将会溢出。

  • 流入请求速度不控制
  • 以固定的速率漏出请求

优点:能强行限制出口的传输速率。

缺点出口无法允许某种程度的突发传输。当短时间内有大量的突发请求时,即便此时服务器没有任何负载,每个请求也都得在队列中等待一段时间才能被响应。

令牌桶算法

算法的示意图如下

imgimg

令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务,令牌桶算法通过发放令牌,根据令牌的rate频率做请求频率限制,容量限制等。

  • 令牌以固定速率放入桶中
  • 允许出口处突发一下拿到桶中所有的令牌,出口允许突发流量流出

优点:实现简单,且允许某些流量的突发,对用户友好,所以被业界采用地较多。

在传统的单体应用中限流只需要考虑到多线程即可,使用Google开源工具类guava即可。其中有一个RateLimiter专门实现了单体应用的限流,使用的是令牌桶算法。

限流实际手段

按照网络拓扑图从外到内依次限流为:

应用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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lakernote

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值