开放平台的限流研究

需求

1.每日配额:新增每日配额信息,申请通过后默认分配50万次每日配额数据;
2.并发配额:新增并发配额信息,申请通过后默认分配500次/分钟并发配额数据;

需求分析

对于开放平台来说,有一个功能是必须要有的,那就是API的流控。

对于每一个接入开放平台的应用,都会分配一个Appkey,这个Appkey下面会关联你申请了哪些API。然后接入的API有些是不限量,可以一直调用。有些是需要申请调用包,比如一天10万次的调用量。

除了总量的限制,还有对频率的限制。比如每秒每个API调用频率的限制,每秒每个Appkey调用频率的限制。

要实现这些流控的功能,最好的方式就是已经有一个流量治理平台,里面提供了限流的功能。像开放平台这种还不属于普通的系统限流,有点偏业务限流了,因为有具体的数量指标。

技术选型

1.MySQL+Redis(计数器)去实现。 存在数据一致性的问题。 强依赖redis影响性能
2.Spring Cloud Gateway原生限流(令牌桶算法-redis)
3.Sentinel

流量整形技术方案
1.若调用方配额设置为无限制,则不执行网关限流策略;
2.基于spring-gateway默认redis+lua脚本技术方案实施;(一秒为单位,不支持小数)
3.网关层通过zookeeper接收到管理端配额配置信息后,为每个调用方涉及的资源生成流量令牌桶;
4.每个经过网关的请求,根据请求方信息+资源信息判断内存限流配置信息,若无限流配置则直接放行;
5.若存在配置信息,则基于流量桶进行流量管理,超额则返回预设错误码及报错信息;

技术实现

Redis(计数器)

  • 请求过滤器
@Component
public class FlowLimitFilter implements GlobalFilter, Ordered {
    @Autowired
    private AsyncTaskService asyncTaskService;
    @Autowired
    private CommonService commonService;//获取应用信息

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String appId = request.getHeaders().getFirst(APPID);//获取应用id
        String requesttimemills = request.getHeaders().getFirst(REQUESTTIMEMILLS);//请求时间
        try{
            if(!StringUtils.isEmpty(appId)){
                //获取应用信息
                AppInfo appInfo = commonService.getAppInfoByAppId(appId);
                //时间转化
                String dateStr = DateUtil.timestampToTimeStr(requesttimemills);
                String minu = dateStr.substring(0,12);
                String day = dateStr.substring(0,8);
                //1、累计调用次数
                Future futureMinu = asyncTaskService.appMinuCounter("gateway:"+appId+":"+minu,"req");
                Future futureDay = asyncTaskService.appDayCounter("gateway:"+appId+":"+day,"req");
                //2、限流判断
                if(appInfo!=null && appInfo.getMinuFlag()!=0){
                    // 限流开关打开才会进行判断
                    long minuNum = (long)futureMinu.get();
                    if(minuNum>appInfo.getMinuQuota()){
                        return commonService.buildErrorResponse(HttpStatus.TOO_MANY_REQUESTS,"当前请求数量超过并发配额",exchange.getResponse());
                    }
                }
                if(appInfo!=null && appInfo.getFreqFlag()!=0){
                    long dayNum = (long)futureDay.get();
                    if(dayNum>appInfo.getAppFreq()){
                        return commonService.buildErrorResponse(HttpStatus.TOO_MANY_REQUESTS,"当前请求数量超过当日配额",exchange.getResponse());
                    }
                }

            }
        }catch (Exception e){
            log.error(e.getMessage(),e);
        }
        return chain.filter(exchange);//放行请求
    }

    @Override
    public int getOrder() {
        return 0;
    }

}

  • 异步redis计数器
@Service
@Slf4j
public class AsyncTaskService {
    @Autowired
    private RedisUtils redisUtils;
    /**
     * 请求计数器
     */
    @Async
    public void reqNumCounter(String appId, String requesttimemills, String hkey){
        try{
            String dateStr = DateUtil.timestampToTimeStr(requesttimemills);
            String minu = dateStr.substring(0,12);
            String day = dateStr.substring(0,8);
            String minuKey = appId+":"+minu;
            String dayKey = appId+":"+day;
            long dayCount = redisUtils.hincr(dayKey,hkey);
            long minuCount = redisUtils.hincr(minuKey,hkey);
            if("req".equals(hkey)&&dayCount==1){
                redisUtils.expire(dayKey,26,TimeUnit.HOURS);
            }
            if("req".equals(hkey)&&minuCount==1){
                redisUtils.expire(minuKey,2,TimeUnit.HOURS);
            }
            log.info("应用:{} 状态:{} redis计数+1",appId,hkey);
        }catch (Exception e){
            log.error("redis计数失败:"+e.getMessage(),e);
        }
    }

    @Async
    public Future<Long> appMinuCounter(String key, String hkey){

        long count = 0;
        try{
            count = redisUtils.hincr(key,hkey);
            if("req".equals(hkey)&&count==1){
                redisUtils.expire(key,2,TimeUnit.HOURS);
            }
        }catch (Exception e){
            log.error("redis计数失败(分钟):"+e.getMessage(),e);
        }
        return new AsyncResult<Long>(count);
    }

    @Async
    public Future<Long> appDayCounter(String key, String hkey){
        long count = 0;
        try{
            count = redisUtils.hincr(key,hkey);
            if("req".equals(hkey)&&count==1){
                redisUtils.expire(key,26,TimeUnit.HOURS);
            }
        }catch (Exception e){
            log.error("redis计数失败(日):"+e.getMessage(),e);
        }
        return new AsyncResult<Long>(count);
    }
}

Spring Cloud Gateway限流

Sentinel

参考

推荐阅读
开放平台的限流通常都是怎么实现的?

—redis操作
https://blog.youkuaiyun.com/lydms/article/details/105224210
https://blog.youkuaiyun.com/sdrfengmi/article/details/103693212
—RedisRateLimiter源码解析
https://blog.youkuaiyun.com/weixin_42073629/article/details/106934827
–Gateway 网关限流
https://juejin.cn/post/7005060165892309022#heading-10
https://juejin.cn/post/7012076662955180068#heading-16
https://blog.youkuaiyun.com/weixin_42073629/article/details/106934827
https://www.cnblogs.com/yinjihuan/p/10514534.html?ivk_sa=1024320u
https://www.cnblogs.com/qianwei/p/10127700.html


参考代码

        <!-- redis依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <dependency>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
            <version>6.0.1.RELEASE</version>
        </dependency>
  • Redis工具类
@Component
public class RedisUtils {

    private final static Long LOCK_EXP = 15l;

    @Autowired
    private RedisTemplate redisTemplate;

    public Map hgetall(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    public Object get(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    public void setex(String key, Object value,long seconds ,TimeUnit timeUnit) {
        redisTemplate.opsForValue().set(key, value, seconds, timeUnit);
    }

    public Long incr(String key) {
        return redisTemplate.opsForValue().increment(key,1);
    }

    public Long hincr(String key,String hkey) {
        return redisTemplate.opsForHash().increment(key,hkey,1);
    }

    public void expire(String key,long timeout, TimeUnit unit) {
        redisTemplate.expire(key,timeout,unit);
    }

    public void del(String key) {
        redisTemplate.delete(key);
    }

    public void lock(String key) {
        setex(key, LOCK_EXP, 0,TimeUnit.SECONDS);
    }

    public void unlock(String key) {
        del(key);
    }

    public boolean sismember(String key, String member) {
        return redisTemplate.opsForSet().isMember(key, member);
    }

}

  • RedisConfig配置类
@Configuration
public class RedisConfig {
    @Resource
    private LettuceConnectionFactory lettuceConnectionFactory;
    /**
     * RedisTemplate配置
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        lettuceConnectionFactory.setValidateConnection(false);
        lettuceConnectionFactory.setShareNativeConnection(false);
        // 设置序列化
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(
                Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置redisTemplate
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        RedisSerializer<?> stringSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringSerializer);// key序列化
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);// value序列化
        redisTemplate.setHashKeySerializer(stringSerializer);// Hash key序列化
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);// Hash value序列化
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值