限流5种方式,令牌桶,应用限流,分布式限流,OpenResty,注解拦截器

3.限流

如果有百万用户进行抢购,商品数量远远低于用户数量,如果我们使用请求入队列或者查询缓存。 对于最终结果没有任何意义。 我们要减少资源浪费, 减轻我们得后端压力。 我们对秒杀服务进行限流。

限流算法:

任何限流不是漫无目的。 常用限流算法 令牌桶 漏桶。

3.1 令牌桶
使用谷歌Guava的RateLimter提供基于令牌桶算法的实现类。 可以非常简单的完成限流特技。 并且可以根据我们系统的实际情况来调整我们的速率。

代码:

package com.etc.access;

import com.alibaba.fastjson.JSON;
import com.etc.common.AbnoNum;
import com.etc.common.ResultBean;
import com.google.common.util.concurrent.RateLimiter;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @Author Mr.findelist
 * @program: seckillplus
 * @Date 2020/8/11  10:44
 * 使用谷歌Guava的RateLimter提供基于令牌桶算法的实现类。 可以非常简单的完成限流特技。
 * 并且可以根据我们系统的实际情况来调整我们的速率。
 **/
@Component
@Aspect
public class RateLimitAspect {


    /**
     * //用来存放不同接口 的令牌桶  (key 借口名字 value 令牌桶)
     * // 线程安全的集合
     */
    private ConcurrentHashMap<String, RateLimiter> map = new ConcurrentHashMap<>();

    @Autowired
    private HttpServletResponse response;

    @Pointcut("execution(public * com.etc.controller.*.*(..))")
    public void serciceLimit() {
    }

    /**
     * 令牌  限流 谷歌
     */

    private RateLimiter rateLimiter;


    @Around("serciceLimit()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Object obj = null;
        //获取拦截的方法的名字
        Signature sig = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) sig;
        // 2处理目标对象
        Object target = joinPoint.getTarget();
        //  3获取注解的信息
        Method method = target.getClass().getMethod(sig.getName(), methodSignature.getParameterTypes());
        //  4获取美妙令牌的数量
        RateLimite rate = method.getAnnotation(RateLimite.class);
        if (rate == null) {
            return joinPoint.proceed();
        }
        //  5校验令牌数量
        int limitNum = rate.limitNum();
        checkRateLimitNum(limitNum);
        //  7获取接口的名字
        String name = methodSignature.getName();
        //  8判断map是否包含 接口的名字key
        if (map.containsKey(name)) {
            rateLimiter = map.get(name);
        } else {
            map.put(name, RateLimiter.create(limitNum));
            rateLimiter = map.get(name);
        }
        try {
            if (rateLimiter.tryAcquire()) {
                obj = joinPoint.proceed();
            } else {
                render(AbnoNum.ACCESS_LIMIT_FREQUENTLY);
            }
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return obj;
    }

    private void render(AbnoNum cm) throws IOException {
        response.setContentType("application/json; charset=UTF-8");
        OutputStream outputStream = response.getOutputStream();
        String str = JSON.toJSONString(ResultBean.error(cm));
        outputStream.write(str.getBytes("UTF-8"));
        outputStream.flush();
        outputStream.close();
    }

    /**
     * 检验令牌
     */
    private void checkRateLimitNum(int linmit) {
        if (linmit <= 0) {
            throw new IllegalArgumentException("注解rate错误");
        }
    }
}

3.2 漏桶
3.3 应用限流
  1. Tomcat :在tomcat 容器中我们可以通过自定义线程池。 配置最大得链接数。 请求处理队列等参数来达到限流得目的。
   <Executor name="tomcatThreadPool" 
              namePrefix="catalina-exec-"
              maxThreads="150" 
              minSpareThreads="4"/>

name:线程名字

namePrefix: 每个允许线程有一个name字符串 ,前缀

maxThreads: 该线程可以容纳得最大得链接数: 默认150

minSpareThreads: 打开得不活跃得线程数

配置Connector

    <Connector executor="tomcatThreadPool" 
               port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" 
               minProcessors="5"	
               maxProcessors="75"
               acceptCount="1000"
               />

executor: 表示使用该参数值对应得线程池

minProcessors: 服务器在启动得时候处理得线程数

maxProcessors: 最大可以创建得线程数

acceptCount: 指定得所有可以处理的请求的线程数被使用是 。 可以放入到队列的请求数。 如果超过就不予处理。

3.4分布式限流:

Nginx

如何使用nginx的基本限流, 比如烁单个ip地址每秒可以访问 20次,nginx的限流模块。 一旦我们使用了限流的模块。 超过将返回503错误给客户端

配置nginx.conf

#统一在http域中进行配置	
#限制请求	
limit_req_zone $binary_remote_addr $uri zone=api_read:20m rate=50r/s;	
#按ip配置一个连接 zone	
limit_conn_zone $binary_remote_addr zone=perip_conn:10m;	
#按server配置一个连接 zone	
limit_conn_zone $server_name zone=perserver_conn:100m;	
server {	
        listen       80;	
        server_name  seckill.52itstyle.com;	
        index index.jsp;	
        location / {	
              #请求限流排队通过 burst默认是0	
              limit_req zone=api_read burst=5;	
              #连接数限制,每个IP并发请求为2	
              limit_conn perip_conn 2;	
              #服务所限制的连接数(即限制了该server并发连接数量)	
              limit_conn perserver_conn 1000;	
              #连接限速	
              limit_rate 100k;	
              proxy_pass      http://seckill;	
        }	
}	
upstream seckill {	
        fair;	
        server  172.16.1.120:8080 weight=1  max_fails=2 fail_timeout=30s;	
        server  172.16.1.130:8080 weight=1  max_fails=2 fail_timeout=30s;	
}
3.5OpenResty

人家也有一些开源的限流的方案。 自带了一个叫 lua_resty_limit_traffic 模块。使用起来更加方便了。

限制接口总并发数/请求数

显示接口时间窗的请求数

代码如下:

local limit_conn = require "resty.limit.conn"
--限制200个并发请求下的请求和一个100个并发的额外的突发请求。也就是我们延迟
--请求300个并发连接以内以及200个以上连接,并拒绝任何超过300的新请求连接。
--另外,我们假设默认请求时间为0.5秒,也就是说通过下面的log_by_lua中的leaving()调用动态调整。
--以上是官网给的配置参数的的说明介绍。("my_limit_conn_store", 200, 100, 0.5) 这个是官网给的参数
--我们可以调整参数为如下("my_limit_conn_store", 1, 0, 0.5)
        -- 限制一个 ip 客户端最大 1 个并发请求
        -- burst 设置为 0,如果超过最大的并发请求数,则直接返回503,
        -- 如果此处要允许突增的并发数,可以修改 burst 的值(漏桶的桶容量)
        -- 最后一个参数其实是你要预估这些并发(或者说单个请求)要处理多久,以便于对桶里面的请求应用漏桶算法

local lim, err = limit_conn.new("my_limit_conn_store", 200, 100, 0.5)
                if not lim then
                    ngx.log(ngx.ERR,
                            "failed to instantiate a resty.limit.conn object: ", err)
                    return ngx.exit(500)
                end
--以下调用必须是每个请求。 这里我们使用远程(IP)地址作为限制key
-- commit 为true 代表要更新shared dict中key的值,
-- false 代表只是查看当前请求要处理的延时情况和前面还未被处理的请求数
local key = ngx.var.binary_remote_addr
                local delay, err = lim:incoming(key, true)
                if not delay then
                    if err == "rejected" then
                        return ngx.exit(503)
                    end
                    ngx.log(ngx.ERR, "failed to limit req: ", err)
                    return ngx.exit(500)
                end
---- 如果请求连接计数等信息被加到shared dict中,则在ctx中记录下,
-- 因为后面要告知连接断开,以处理其他连接

                if lim:is_committed() then
                    local ctx = ngx.ctx
                    ctx.limit_conn = lim
                    ctx.limit_conn_key = key
                    ctx.limit_conn_delay = delay
                end

local conn = err

        -- 其实这里的 delay 肯定是上面说的并发处理时间的整数倍,
        -- 举个例子,每秒处理100并发,桶容量200个,当时同时来500个并发,则200个拒掉
        -- 100个在被处理,然后200个进入桶中暂存,被暂存的这200个连接中,0-100个连接其实应该延后0.5秒处理,
        -- 101-200个则应该延后0.5*2=1秒处理(0.5是上面预估的并发处理时间)

                if delay >= 0.001 then
        --请求超过200连接比但低于300个连接,所以我们故意将它延迟到这里以符合200连接限制。
                    ngx.sleep(delay)
                end

代码实现:

/**
     * 获取秒杀路径
     * @param user
     * @param goodsId
     * @return
     */
    @GetMapping("path")
    @ResponseBody
    public Result getMiaoShaPath(MiaoShaUser user, @RequestParam("goodsId") long goodsId,HttpServletRequest request) {
        if (user == null) {
            return Result.error(CodeMsg.SESSION_ERROR);
        }

        //查询访问次数5秒  5次
        String url = request.getRequestURI();
        String key = url + user.getId();

        Integer count = redisService.get(AccessKey.getUserUrlCount, key, Integer.class);
        if (count == null){
            redisService.set(AccessKey.getUserUrlCount, key, 1);
        }else if(count < 5){
            redisService.incr(AccessKey.getUserUrlCount, key);
        }else {
            return Result.error(CodeMsg.ACCESS_LIMIT_FREQUENTLY);
        }

        String miaoShaPath = iMiaoShaOrderService.createMiaoShaPath(user.getId(), goodsId);
        return Result.success(miaoShaPath);
    }
3.6 计数器方式

普通实现

    @AccessLimit(seconds = 5,maxConunt = 5)
    @GetMapping("path")
    @ResponseBody
    public Result getMiaoShaPath(MiaoShaUser user, @RequestParam("goodsId") long goodsId,HttpServletRequest request) {
        if (user == null) {
            return Result.error(CodeMsg.SESSION_ERROR);
        }

//        //查询访问次数5秒  5次
//        String url = request.getRequestURI();
//        String key = url + user.getId();
//
//        Integer count = redisService.get(AccessKey.getUserUrlCount, key, Integer.class);
//        if (count == null){
//            redisService.set(AccessKey.getUserUrlCount, key, 1);
//        }else if(count < 5){
//            redisService.incr(AccessKey.getUserUrlCount, key);
//        }else {
//            return Result.error(CodeMsg.ACCESS_LIMIT_FREQUENTLY);
//        }

        String miaoShaPath = iMiaoShaOrderService.createMiaoShaPath(user.getId(), goodsId);
        return Result.success(miaoShaPath);
    }

注解拦截器方式:

自定义注解

package com.etc.access;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Author Mr.findelist
 * @program: seckillplus
 * @Date 2020/8/10  15:08
 **/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AccessLimit {

    //限流时间
    int seconds();
    //限流数量
    int maxCount();
    //判断是否必须登录
    boolean nendLogin() default true;

}

配置拦截器

package com.etc.config;

import com.etc.access.AccessInterCeptor;
import com.etc.access.AccessLimit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

/**
 * @Author kalista
 * aDescription自动义拦截器
 * addInterceptors:拦截器
 * addViewControllers:页面跳转
 * addResourceHandLers:静态资源
 * configureDefaultServletHandling:默认静态资源处理器
 * configureViewResolvers:视图解析器
 * configureContentNegotiation:配置内容裁决的一些参数
 * addCorsMappings:跨域
 * configureMessageConverters:信息转换器
 * addArgumentResolvers自定义参数处理器
 * @Date 2020/ 7/2016:53
 */

@Configuration
public class MvcConf implements WebMvcConfigurer {
    @Autowired
    private UserArgumentResolvers userArgumentResolvers;

    @Autowired
    private AccessInterCeptor accessInterCeptor;

    /**
     * 自定义参数处理器(不需要)
     * @param resolvers
     */
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(userArgumentResolvers);
    }

    /**
     * 拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(accessInterCeptor);
    }

    /**
     * 设置默认登录页面(不需要)
     * @param registry
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("login");
        registry.addViewController("index.html").setViewName("login");
        registry.addViewController("login.html").setViewName("login");
    }
}

判断令牌

package com.etc.access;

import com.alibaba.fastjson.JSON;
import com.etc.common.AbnoNum;
import com.etc.common.ResultBean;
import com.etc.domian.User;
import com.etc.redis.AccessKey;
import com.etc.redis.RedisService;
import com.etc.service.impl.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;

/**
 * @Author Mr.findelist
 * @program: seckillplus
 * @Date 2020/8/10  15:12
 **/
@Service
public class AccessInterCeptor extends HandlerInterceptorAdapter {
    @Autowired
    private UserServiceImpl userService;

    @Autowired
    private RedisService redisService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断是否来自方法体
        if (handler instanceof HandlerMethod) {
            User user = getUser(request, response);
            HandlerMethod hm = (HandlerMethod) handler;
            //包含哪个注解
            AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);

            if (accessLimit == null) {
                return true;
            }
            //获取注解的值
            int count = accessLimit.maxCount();
            int seconds = accessLimit.seconds();
            boolean nedlogin = accessLimit.nendLogin();
            String key = request.getRequestURL().toString();
            //用户登录状态
            if (nedlogin) {
                if (user == null) {
                    render(response, AbnoNum.USERNAME_IS_ENIST);
                }
                key += "_" + user.getId();
            }
            //从缓存获取key 次数
            Integer cou = redisService.get(AccessKey.getUserUrlcount, key, Integer.class);
            AccessKey accessKey = new AccessKey(seconds, "access:");
            //没有就加入缓存   小于限流次数就加一  超过就 退出
            if (cou == null) {
                redisService.set(accessKey, key, 1);
            } else if (cou < count) {
                redisService.incr(accessKey, key);
            } else {
                render(response, AbnoNum.ACCESS_LIMIT_FREQUENTLY);
                return false;
            }
        }
        return true;
    }

    public void render(HttpServletResponse response, AbnoNum abnoNum) throws IOException {
        //写入信息到页面上
        response.setContentType("application/json; charset=UTF-8");
        OutputStream outputStream = response.getOutputStream();
        String s = JSON.toJSONString(ResultBean.error(abnoNum));
        outputStream.write(s.getBytes("utf-8"));
        outputStream.flush();
        outputStream.close();

    }

    public User getUser(HttpServletRequest request, HttpServletResponse response) {
        String parameter = request.getParameter("token");
        String cookieValue = getCookieValue(request, "token");
        if (StringUtils.isEmpty(parameter) && StringUtils.isEmpty(cookieValue)) {
            return null;
        }
        String cookie = StringUtils.isEmpty(cookieValue) ? parameter : cookieValue;
        return userService.getcookie(response, cookie);
    }

    /**
     * 获取cookie中token值
     */
    private String getCookieValue(HttpServletRequest request, String cookieName) {
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(cookieName)) {
                    return cookie.getValue();
                }
            }
        }

        return null;
    }

}


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Spring Cloud Gateway中,可以通过多方式实现限流。一常见的方式是使用Hystrix来进行线程池隔离,当超过线程池的负载时,可以采取熔断的逻辑来限制并发。另一方式是通过时间窗口的平均速度来控制流量,可以通过IP、URI或用户访问频次等维度进行限流。一般限流操作是在网关层面进行的,比如使用Nginx、Openresty、Kong、Zuul或Spring Cloud Gateway等。此外,还可以通过AOP等方式应用层实现限流。 在Spring Cloud Gateway中,可以使用RequestRateLimiter过滤器来实现限流功能。这个过滤器的核心代码是org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter#isAllowed方法,可以根据自定义的限流策略继承AbstractRateLimiter来实现。在配置过滤器中,可以指定名字为RequestRateLimiter来调用该限流器的实现类。 限流在高并发系统中非常重要,一方面可以防止大量的请求导致服务器过载,使服务不可用,另一方面可以防止网络攻击。常见的限流算法包括计数器算法等。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [Spring Cloud Gateway 限流](https://blog.csdn.net/qq_33349086/article/details/107444976)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [Gateway网关限流](https://blog.csdn.net/m0_37543627/article/details/117066783)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值