【从0到1设计一个网关】重试与限流的实现

上文已经讲到了如何设计一个高可用的稳定的网关,那么这里就实现其中两种比较常用的方法。

重试

这里的重试,我将会在IO异常以及请求超时的时候进行一个请求重试。 首先,我们在路由过滤器中添加一个重试的函数,用于在请求出现如上两个异常的时候进行重试。 当然,我们需要增加一些额外的配置参数来设定重试的次数等信息。

而重试的代码,其实就是再一次调用doFilter方法去执行路由过滤器中的逻辑

css 复制代码
 private void doRetry(GatewayContext gatewayContext,int retryTimes){
        System.out.println("当前重试次数为"+retryTimes);
        gatewayContext.setCurrentRetryTimes(retryTimes+1);
        try {
            doFilter(gatewayContext);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

最后,我们让我们的服务去请求后端服务,并且在后端服务哪里设定一个长时间的阻塞sleep。 重试的简单实现是比较简单的,当然前提是你理解了之前的所有代码。或者说至少理解了请求转发这一块的代码

限流

常见的限流算法有令牌桶算法和漏桶算法。 这里两种算法我们都可以考虑使用一下。 同时,对于限流这一块,我们需要在配置中心配置限流规则。 例如限流的路径或者限流的服务。同时,根据你的服务是分布式服务还是单体服务,也需要考虑使用不同的方式来存储信息。 比如如果是分布式服务,就需要使用Redis,而如果是单体,那么考虑使用本地缓存即可,比如Guava或者Caffeine。 老样子,我们先编写一个接口,这个接口用于获取对应的限流过滤器。

css 复制代码
public interface GatewayFlowControlRule {

    /**
     * 执行流控规则过滤器
     * @param flowControlConfig
     * @param serviceId
     */
    void doFlowControlFilter(Rule.FlowControlConfig flowControlConfig, String serviceId);
}

之后,我们开始编写限流过滤器,根据请求获取对应的限流规则。

css 复制代码
@Slf4j
@FilterAspect(id=FLOW_CTL_FILTER_ID,
        name = FLOW_CTL_FILTER_NAME,
        order = FLOW_CTL_FILTER_ORDER)
public class FlowControlFilter implements Filter {
    @Override
    public void doFilter(GatewayContext ctx) throws Exception {
        Rule rule = ctx.getRule();
        if(rule != null){
            //获取流控规则
            Set<Rule.FlowControlConfig> flowControlConfigs = rule.getFlowControlConfigs();
            Iterator iterator = flowControlConfigs.iterator();
            Rule.FlowControlConfig flowControlConfig;
            while (iterator.hasNext()){
                GatewayFlowControlRule flowControlRule = null;
                flowControlConfig = (Rule.FlowControlConfig)iterator.next();
                if(flowControlConfig == null){
                    continue;
                }
                String path = ctx.getRequest().getPath();
                if(flowControlConfig.getType().equalsIgnoreCase(FLOW_CTL_TYPE_PATH)
                        && path.equals(flowControlConfig.getValue())){
                    flowControlRule = FlowControlByPathRule.getInstance(rule.getServiceId(),path);
                }else if(flowControlConfig.getType().equalsIgnoreCase(FLOW_CTL_TYPE_SERVICE)){
                    //TODO 可以自己实现基于服务的流控
                }
                if(flowControlRule != null){
                    //执行流量控制
                    flowControlRule.doFlowControlFilter(flowControlConfig,rule.getServiceId());
                }
            }
        }
    }
}

获取到指定的限流规则之后,就可以开始考虑着手编写如何进行具体的限流了。 比如我们要根据路径进行限流,那么我们首先需要的信息就是服务,以及请求路径。 并且我们需要保存这样子的规则, 每当对应的请求到来时,我们就从缓存中获取对应的限流规则。

java 复制代码
 /**
     * 存放路径-流控规则的map
     */
    private static ConcurrentHashMap<String, FlowControlByPathRule> servicePathMap = new ConcurrentHashMap<>();

    /**
     * 通过服务id和路径获取具体的流控规则过滤器
     *
     * @param serviceId
     * @param path
     * @return
     */
    public static FlowControlByPathRule getInstance(String serviceId, String path) {
        StringBuffer buffer = new StringBuffer();
        String key = buffer.append(serviceId).append(".").append(path).toString();
        FlowControlByPathRule flowControlByPathRule = servicePathMap.get(key);
        //当前服务不存在限流规则 则保存之
        if (flowControlByPathRule == null) {
            flowControlByPathRule = new FlowControlByPathRule(serviceId, path, new RedisCountLimiter(new JedisUtil()));
            servicePathMap.put(key, flowControlByPathRule);
        }
        return flowControlByPathRule;
    }

获取完毕限流规则之后,我们就可以着手分析,如何进行具体的限流方法了。 我们获取到指定的限流配置之后,比如是否是分布式服务,是否限流时间和限流限制次数等等信息之后,就可以开始编写具体的限流代码了。 比如如果配置中发现服务是分布式的,那么就使用Redis,然后保存当前的请求路径以及限制次数等信息。

java 复制代码
 /**
     * 执行限流
      * @param key 限流key 服务+路径
     * @param limit 限流次数
     * @param expire 超时时间
     * @return
     */
    public  boolean doFlowControl(String key,int limit,int expire){
        try {
            //执行脚本判断是否触发限流
            Object object = jedisUtil.executeScript(key,limit,expire);
            if(object == null){
                return true;
            }
            Long result = Long.valueOf(object.toString());
            if(FAILED_RESULT == result){
                return  false;
            }
        }catch (Exception e){
            throw  new RuntimeException("分布式限流发生错误");
        }
        return true;
    }

 public Object executeScript(String key, int limit, int expire){
        Jedis jedis = jedisPool.getJedis();
        String lua = buildLuaScript();
        String scriptLoad =jedis.scriptLoad(lua);
        try {
            Object result = jedis.evalsha(scriptLoad, Arrays.asList(key), Arrays.asList(String.valueOf(expire), String.valueOf(limit)));
            System.out.println(result);
            return result;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (jedis != null) {
                try {
                    jedis.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }


    // 构造lua脚本
    private static String buildLuaScript() {
        String lua = "local num = redis.call('incr', KEYS[1])\n" +
                "if tonumber(num) == 1 then\n" +
                "\tredis.call('expire', KEYS[1], ARGV[1])\n" +
                "\treturn 1\n" +
                "elseif tonumber(num) > tonumber(ARGV[2]) then\n" +
                "\treturn 0\n" +
                "else \n" +
                "\treturn 1\n" +
                "end\n";
        return lua;
    }

而如果不是分布式项目,就可以考虑使用Guava这种本地缓存。 实现方式大差不差,如下

java 复制代码
public class GuavaCountLimiter {

    /**
     * guava限流根据
     */
    private RateLimiter rateLimiter;
    /**
     * 最大请求数量
     */
    private double maxPermits;

    /**
     * 没有预热,尽可能按照这个速率来分发许可。速率限制器不会考虑之前的请求,也不会允许短时间内的请求速率超过指定的速率。
     * 这可能导致一些请求需要等待,以便速率限制器可以分发足够的许可。
     *
     * @param maxPermits
     */

    public GuavaCountLimiter(double maxPermits) {
        this.maxPermits = maxPermits;
        rateLimiter = RateLimiter.create(maxPermits);
    }

    /**
     * 创建一个具有预热期的速率限制器。预热期是指在速率限制器刚开始使用时,速率限制器允许请求超过其平均速率。
     * 预热结束后按照指定的速度分发许可。
     * 提供预热意味着可以在应用启动或负载增加时,允许一些瞬时的高请求速率,然后逐渐调整到稳定的速率。
     *
     * @param maxPermits           表示每秒的最大许可数量
     * @param warmUpPeriodAsSecond 预热时长
     */
    public GuavaCountLimiter(double maxPermits, long warmUpPeriodAsSecond) {
        this.maxPermits = maxPermits;
        rateLimiter = RateLimiter.create(maxPermits, warmUpPeriodAsSecond, TimeUnit.SECONDS);
    }

    /**
     * 路径 - 限流器
     */
    public static ConcurrentHashMap<String, GuavaCountLimiter> resourceRateLimiterMap = new ConcurrentHashMap<String,
            GuavaCountLimiter>();

    public static GuavaCountLimiter getInstance(String serviceId, Rule.FlowControlConfig flowControlConfig) {
        if (StringUtils.isEmpty(serviceId) || flowControlConfig == null || StringUtils.isEmpty(flowControlConfig.getValue()) || StringUtils.isEmpty(flowControlConfig.getConfig()) || StringUtils.isEmpty(flowControlConfig.getType())) {
            return null;
        }
        StringBuffer buffer = new StringBuffer();
        String key = buffer.append(serviceId).append(".").append(flowControlConfig.getValue()).toString();
        GuavaCountLimiter countLimiter = resourceRateLimiterMap.get(key);
        if (countLimiter == null) {
            //获得当前路径对应的流控次数
            Map<String, Integer> configMap = JSON.parseObject(flowControlConfig.getConfig(), Map.class);
            //判断是否包含流控规则
            if (!configMap.containsKey(FLOW_CTL_LIMIT_DURATION) || !configMap.containsKey(FLOW_CTL_LIMIT_PERMITS)) {
                return null;
            }
            //得到流控时间和时间内限制次数
            double permits = configMap.get(FLOW_CTL_LIMIT_PERMITS);
            countLimiter = new GuavaCountLimiter(permits);
            resourceRateLimiterMap.putIfAbsent(key, countLimiter);
        }
        return countLimiter;
    }

    /**
     * 获取令牌
     * @param permits 需要获取的令牌数量
     * @return 是否获取成功
     */
    public boolean acquire(int permits) {
        boolean success = rateLimiter.tryAcquire(permits);
        if (success) {
            return true;
        }
        return false;
    }
}

所以到目前位置,就已经大概的实现了如何进行限流的代码了。 之后,我们配置完毕配置中心的信息之后,就可以开始测试我们的限流代码了。

javascript 复制代码
{
    "rules": [
        {
            "id":"1",
            "name":"test-1",
            "protocol":"http",
            "serviceId":"backend-http-server",
            "prefix":"/user",
            "paths":[
                "/http-server/ping","/user/update"
            ],
            "filterConfigs":[{
                    "id":"load_balance_filter",
                    "config":{
                        "load_balance":"Random"
                    }
                },{
                    "id":"flow_ctl_filter"
            }],
            "flowControlConfigs":[{
                "type":"path",
                "model":"distributed",
                "value":"/http-server/ping",
                "config":{
                    "duration":20,
                    "permits":2
                }
            }],
            "retryConfig":{
                "times":5
            },
            "hystixConfigs":[{
                "path":"/http-server/ping",
                "timeoutInMilliseconds":5000,
                "threadCoreSize":2,
                "fallbackResponse":"熔断超时"
            }]
        }
    ]
}

我们使用apifox发送过量的请求,就会发现,报错如下 到目前位置我们已经实现了重试和限流,那么下一篇文章我们就需要实现熔断与降级。 因为其实限流和熔断降级是一起的。

相关推荐
2401_857622661 小时前
SpringBoot框架下校园资料库的构建与优化
spring boot·后端·php
2402_857589361 小时前
“衣依”服装销售平台:Spring Boot框架的设计与实现
java·spring boot·后端
哎呦没3 小时前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
_.Switch3 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
杨哥带你写代码4 小时前
足球青训俱乐部管理:Spring Boot技术驱动
java·spring boot·后端
AskHarries5 小时前
读《show your work》的一点感悟
后端
A尘埃5 小时前
SpringBoot的数据访问
java·spring boot·后端
yang-23075 小时前
端口冲突的解决方案以及SpringBoot自动检测可用端口demo
java·spring boot·后端
Marst Code5 小时前
(Django)初步使用
后端·python·django
代码之光_19805 小时前
SpringBoot校园资料分享平台:设计与实现
java·spring boot·后端