接口限流技术调研

1. 限流算法

工作中对外提供的API 接口设计很多时候要考虑限流,如果不考虑,可能会造成系统的连锁反应,轻者响应缓慢,重者系统宕机。而比较成熟的限流算法有令牌桶算法,本篇介绍令牌桶算法

原理如上图,系统以恒定速率不断产生令牌,令牌桶有最大容量,超过最大容量则丢弃,同时用户请求接口,如果此时令牌桶中有令牌则能访问获取数据,否则直接拒绝用户请求

2. 单机解决方案

Guava rateLimiter实现

//单机全局限流器,QPS为1

private static final RateLimiter RATE_LIMITER = RateLimiter.create(1);

@ApiOperation("单机限流")

@GetMapping("/test1")

public ResultDTO test1() {

    if(!RATE_LIMITER.tryAcquire()){

        log.info("限流了..");

        return ResultDTO.error("限流了..");

    }

    log.info("请求成功");

    return ResultDTO.ok("请求成功");

}

执行结果:

2021-06-28 14:01:22.803  INFO 20212 --- [nio-8080-exec-1] c.r.d.controller.RedisLimitController    : 请求成功

2021-06-28 14:01:22.987  INFO 20212 --- [nio-8080-exec-2] c.r.d.controller.RedisLimitController    : 请求成功

2021-06-28 14:01:23.159  INFO 20212 --- [nio-8080-exec-3] c.r.d.controller.RedisLimitController    : 限流了..

2021-06-28 14:01:23.347  INFO 20212 --- [nio-8080-exec-4] c.r.d.controller.RedisLimitController    : 限流了..

2021-06-28 14:01:23.520  INFO 20212 --- [nio-8080-exec-5] c.r.d.controller.RedisLimitController    : 限流了..

2021-06-28 14:01:23.695  INFO 20212 --- [nio-8080-exec-6] c.r.d.controller.RedisLimitController    : 限流了..

2021-06-28 14:01:23.876  INFO 20212 --- [nio-8080-exec-7] c.r.d.controller.RedisLimitController    : 请求成功

2021-06-28 14:01:24.045  INFO 20212 --- [nio-8080-exec-8] c.r.d.controller.RedisLimitController    : 限流了..

2021-06-28 14:01:24.234  INFO 20212 --- [nio-8080-exec-9] c.r.d.controller.RedisLimitController    : 限流了..

2021-06-28 14:01:24.406  INFO 20212 --- [io-8080-exec-10] c.r.d.controller.RedisLimitController    : 限流了..

2021-06-28 14:01:24.594  INFO 20212 --- [nio-8080-exec-1] c.r.d.controller.RedisLimitController    : 限流了..

2021-06-28 14:01:24.766  INFO 20212 --- [nio-8080-exec-2] c.r.d.controller.RedisLimitController    : 限流了..

2021-06-28 14:01:24.948  INFO 20212 --- [nio-8080-exec-3] c.r.d.controller.RedisLimitController    : 请求成功

2021-06-28 14:01:25.128  INFO 20212 --- [nio-8080-exec-4] c.r.d.controller.RedisLimitController    : 限流了..

2021-06-28 14:01:25.308  INFO 20212 --- [nio-8080-exec-5] c.r.d.controller.RedisLimitController    : 限流了..

2021-06-28 14:01:25.492  INFO 20212 --- [nio-8080-exec-6] c.r.d.controller.RedisLimitController    : 限流了..

3. 分布式环境下解决方案

Redis rateLimiter


需要限流的接口使用该注解

/**

 * 1(时间)分钟(单位)允许某个ip请求的最大次数(max)

 *

 * 如每隔2分钟,单IP限定访问次数不能超过10次

 */

@Target(ElementType.METHOD)

@Retention(RetentionPolicy.RUNTIME)

public @interface RedisRateLimiter {

    /**

     * 默认根据IP拦截

     */

    LimitType limitType() default LimitType.GENERAL;

    enum LimitType{

        GENERAL,IP,USERID;

    }

    /**

     * 限制时间长度

     */

    long timeLimitLength() default 1;

    /**

     * 限制时间长度的单位

     */

    TimeUnit timeLimitLengthUnit() default TimeUnit.SECONDS;

    /**

     * 允许时间内最大访问数

     */

    long max() default 1;

}

/**

 * 自定义注解:标记限流key

 * @author dengyingxiang

 */

@Retention(value = RetentionPolicy.RUNTIME)

@Target(value = {ElementType.PARAMETER,ElementType.FIELD})

@Documented

public @interface LimitKey {

}

切面

@Slf4j

@Component

@Aspect

@RequiredArgsConstructor(onConstructor_ = @Autowired)

public class RedisRateLimitAspect {

    private final static String REDIS_RATE_LIMIT_KEY_PREFIX = "limit:";

    private final StringRedisTemplate stringRedisTemplate;

    private final RedisScript<Long> limitRedisScript;

    @Pointcut("@annotation(com.rock.demo.annotation.RedisRateLimiter)")

    public void rateLimit() {

    }

    @Before("rateLimit()")

    public void pointCut(JoinPoint joinPoint) throws IllegalAccessException {

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();

        Method method = signature.getMethod();

        // 通过 AnnotationUtils.findAnnotation 获取 RateLimiter 注解

        RedisRateLimiter redisRateLimit = AnnotationUtils.findAnnotation(method, RedisRateLimiter.class);

        RedisRateLimiter.LimitType limitType = redisRateLimit.limitType();

        if (redisRateLimit != null) {

            //获取时间限制

            long timeLimitLength = redisRateLimit.timeLimitLength();

            //获取时间限制单位

            TimeUnit timeLimitLengthUnit = redisRateLimit.timeLimitLengthUnit();

            //时间单位最大访问数目

            long max = redisRateLimit.max();

            String limitKey = "";

            //ip限流则用ip做limitKeyValue,其他的从参数中获取

            if (RedisRateLimiter.LimitType.IP.name().equals(limitType.name())) {

                HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();

                limitKey = IpUtil.getIpAddr(request);

            else {

                Object[] args = joinPoint.getArgs();

                // 1.入参是(String id)

                limitKey = getIdForSingle(joinPoint, args);

                if (StringUtils.isEmpty(limitKey)) {

                    // 2.入参是DTO,LimitKey注解放到DTO.id上。

                    limitKey = getIdForParamsDTO(args);

                }

            }

            //redis存储的key;"limit:"${className}"."${methodName}:${limitKey}

            String storeKey = REDIS_RATE_LIMIT_KEY_PREFIX + method.getDeclaringClass().getSimpleName() + "." + method.getName() + ":" + limitKey;

            long now = System.currentTimeMillis();

            //将2分钟转化为毫秒时间戳,以获得2分钟前时间

            long limitTimeLengthMills = timeLimitLengthUnit.toMillis(timeLimitLength);

            //应该移除的分值区间

            long removeScore = now - limitTimeLengthMills;

            Long r = stringRedisTemplate.execute(

                    limitRedisScript,

                    Lists.newArrayList(storeKey),

                    "" + now,

                    "" + limitTimeLengthMills,   //设置key的保存时间,该key在2分钟的允许时间内做zadd操作

                    "" + removeScore,     //移除当前时间2分钟前过期的score

                    "" + max);//当前接口访问上线

            if (r != null) {

                if (r == 0) {

                    log.error("【{}】在 " + timeLimitLength + formatTimeUnit(timeLimitLengthUnit) + " 内已达到访问上限,当前接口上限 {}", storeKey, max);

                    throw new RuntimeException("手速太快了,慢点儿吧~");

                else {

                    log.info("【{}】在 " + timeLimitLength + formatTimeUnit(timeLimitLengthUnit) + " 内访问 {} 次", storeKey, r);

                }

            }

        }

    }

    private String formatTimeUnit(TimeUnit timeUnit) {

        if (timeUnit == TimeUnit.MINUTES) {

            return "分钟";

        else if (timeUnit == TimeUnit.SECONDS) {

            return "秒";

        else if (timeUnit == TimeUnit.HOURS) {

            return "小时";

        }

        return "illegal timeUnit args";

    }

    private String getIdForSingle(JoinPoint joinPoint, Object[] args) {

        if (Objects.nonNull(args) && args.length > 0) {

            MethodSignature signature = (MethodSignature) joinPoint.getSignature();

            Annotation[][] parameterAnnotations = signature.getMethod().getParameterAnnotations();

            if (Objects.isNull(parameterAnnotations)) {

                return null;

            }

            // 循环判断是否有limitkey

            for (int i = 0; i < parameterAnnotations.length; i++) {

                for (Annotation annotation : parameterAnnotations[i]) {

                    if (annotation instanceof LimitKey) {

                        return (String)args[i];

                    }

                }

            }

        }

        return null;

    }

    private String getIdForParamsDTO( Object[] args) throws IllegalAccessException {

        for (Object arg : args) {

            String id = getIdForDto(arg);

            if(StringUtils.isNotEmpty(id)){

                return id;

            }

        }

        return null;

    }

    private String getIdForDto(Object arg) throws IllegalAccessException {

        Field[] fields = arg.getClass().getDeclaredFields();

        if(Objects.nonNull(fields) && fields.length > 0){

            for (Field field : fields) {

                field.setAccessible(true);

                if (field.getType().equals(String.class) && field.isAnnotationPresent(LimitKey.class)) {

                    return (String)field.get(arg);

                }

            }

        }

        return null;

    }

}

redis配置文件

@Configuration

public class RedisConfig {

    @Bean

    @SuppressWarnings("unchecked")

    public RedisScript<Long> limitRedisScript() {

        DefaultRedisScript redisScript = new DefaultRedisScript<>();

        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("scripts/redis/limit.lua")));

        redisScript.setResultType(Long.class);

        return redisScript;

    }

}

LUA脚本

local key = KEYS[1]

local now = tonumber(ARGV[1])

local limitTimeLengthMills = tonumber(ARGV[2])

local removeScore = tonumber(ARGV[3])

local max = tonumber(ARGV[4])

redis.call('ZREMRANGEBYSCORE',key,0,removeScore)

local current = tonumber(redis.call('ZCARD',key))

local next = current+1

if next>max then

    return 0

else

    redis.call('ZADD',key,now,now)

    redis.call('PEXPIRE',key,limitTimeLengthMills)

    return next

end

脚本说明:

1.先删除已过期时间端的数据;

     ZREMRANGEBYSCORE key min max

     移除有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。

2.统计sortedset里面的数据量;

     ZCARD key

     返回有序集 key 的基数。

3.判断不通过返回0;

4.判断通过则,添加一条记录,并返回1

控制层

@ApiOperation("分布式ip限流")

@RedisRateLimiter(max = 1,limitType = RedisRateLimiter.LimitType.IP,timeLimitLength = 1,timeLimitLengthUnit = TimeUnit.SECONDS)

@GetMapping("/test2")

public ResultDTO test2() {

    log.info("【test2】被执行了。。。。。");

    return ResultDTO.ok("成功访问到api [2]~");

}

@ApiOperation("分布式关键字限流")

@RedisRateLimiter(max = 1,limitType = RedisRateLimiter.LimitType.GENERAL,timeLimitLength = 1,timeLimitLengthUnit = TimeUnit.SECONDS)

@GetMapping("/test3/{limitKey}")

public ResultDTO test3(@LimitKey @ApiParam(value = "限流的key", required = true@PathVariable("limitKey") String limitKey) {

    log.info("【test3】被执行了。。。。。");

    return ResultDTO.ok("成功访问到api [3]~");

}

执行结果:

2021-06-28 14:22:58.324  INFO 20212 --- [io-8080-exec-10] c.rock.demo.aspect.RedisRateLimitAspect  : 【limit:RedisLimitController.test3:111】在 1秒 内访问 1 

2021-06-28 14:22:58.325  INFO 20212 --- [io-8080-exec-10] c.r.d.controller.RedisLimitController    : 【test3】被执行了。。。。。

2021-06-28 14:22:58.892 ERROR 20212 --- [nio-8080-exec-1] c.rock.demo.aspect.RedisRateLimitAspect  : 【limit:RedisLimitController.test3:111】在 1秒 内已达到访问上限,当前接口上限 1

2021-06-28 14:22:58.894 ERROR 20212 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: 手速太快了,慢点儿吧~] with root cause

java.lang.RuntimeException: 手速太快了,慢点儿吧~

    at com.rock.demo.aspect.RedisRateLimitAspect.pointCut(RedisRateLimitAspect.java:98) ~[classes/:na]

    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_271]

    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_271]

    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_271]

    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_271]

    at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:644) ~[spring-aop-5.2.14.RELEASE.jar:5.2.14.RELEASE]

    at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:626) ~[spring-aop-5.2.14.RELEASE.jar:5.2.14.RELEASE]

    at org.springframework.aop.aspectj.AspectJMethodBeforeAdvice.before(AspectJMethodBeforeAdvice.java:44) ~[spring-aop-5.2.14.RELEASE.jar:5.2.14.RELEASE]

    at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:55) ~[spring-aop-5.2.14.RELEASE.jar:5.2.14.RELEASE]

    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175) ~[sp

参考文档:

接口限流&令牌桶算法&Redis分布式限流 - 简书

Redis 命令参考 — Redis 命令参考

<think>嗯,用户这次想深入了解Sentinel的限流实现机制。从对话历史看,我之前已经详细介绍了核心原理,但用户可能觉得不够具体,需要更落地的技术细节。用户提供的四个引用材料很有价值。引用[1]提到滑动窗口算法是核心,引用[3]展示的代码片段揭示关键入口是SphU.entry(),而引用[4]的源码片段直接暴露了FlowRuleChecker的校验逻辑。这些恰好能组成完整的实现链条。用户应该是个开发者,正在调研限流组件选型或解决线上流量问题。ta跳过基础概念直接问实现方式,说明有一定技术背景,可能更关心:-如何避免传统固定窗口的临界突变问题(所以需要展开讲滑动窗口的创新点)-代码层面怎样不侵入业务逻辑(entry/exit的AOP设计值得强调)-集群限流的挑战(引用[2]提到的集群功能需要补充说明)需要特别注意两点:1.引用[4]提到FlowRuleChecker会根据规则类型调用不同执行器,这个分层设计对理解扩展性很重要2.用户给的代码片段中finally块的exit()方法容易被人忽略,其实它是统计耗时的关键,会影响令牌桶/漏桶的精度上次回答覆盖了原理层,这次应该聚焦三个新维度:-滑动窗口的具体数据结构实现(比如引用[4]的DefaultNode如何存储时间片段)-规则执行路径的代码级解析(从entry()到canPass()的调用链)-集群限流时怎么解决时钟同步问题(引用[2]提到但没展开)意外发现用户给的引用[3]示例代码有个隐患:resource是硬编码字符串,实际生产应该用常量或枚举,这点可以作为优化建议补充。另外引用[1][2]都强调可视化,但用户没问,暂时放在扩展部分即可。最后的代码片段要选最有代表性的,比如展示令牌桶的WarmUpController实现片段(因为涉及预热算法),同时保持中文注释帮助理解。</think> Sentinel 的限流实现基于**滑动窗口算法**和**责任链模式**,通过规则校验和实时统计实现精准流量控制。以下是关键原理及实现方式的详细说明: --- ### **一、核心限流原理** 1. **滑动窗口算法**(核心统计模型) - 将时间切分为多个小窗口(如1秒=2个500ms窗口),每个窗口独立统计请求量。 - 窗口按时间滑动(例如每500ms更新一次),避免固定窗口的临界突变问题。 - 例如:限流1000 QPS时,若当前窗口(0-500ms)已通过800请求,则后续200ms最多允许200请求。 2. **多级流量统计** - **Node**:存储资源的实时指标(如QPS、线程数)。 - **ClusterNode**:聚合相同资源的集群统计(用于集群限流)。 - **StatisticNode**:实现滑动窗口的具体计数逻辑,通过`LeapArray`数据结构管理时间窗口。 --- ### **二、实现流程(责任链模式)** 限流逻辑由`ProcessorSlotChain`串联执行,关键步骤如下: ```java // 伪代码:Sentinel核心处理链 public void entry(String resource) { try { // 1. 触发规则校验链 fireEntry(resource); // 2. 执行实际业务逻辑 doBusinessLogic(); } catch (BlockException e) { // 3. 规则触发则限流 handleBlockedRequest(); } finally { // 4. 退出时更新统计 exit(); } } ``` #### **具体Slot的作用**: 1. **FlowSlot** - 调用`FlowRuleChecker.checkFlow()`校验规则。 - 根据规则类型(直接拒绝/WarmUp/排队)选择控制器(如`DefaultController`)。 2. **StatisticSlot** - 实时更新资源指标(QPS/响应时间)。 - 触发后续熔断降级等规则。 --- ### **三、限流规则执行逻辑** 以**本地限流**为例(`passLocalCheck()`): ```java private boolean passLocalCheck(FlowRule rule, Context context, Node node, int count) { // 1. 选择统计节点(根据规则策略) Node selectedNode = selectNodeByStrategy(rule, context, node); // 2. 调用流量控制器(如令牌桶/漏桶) return rule.getRater().canPass(selectedNode, count); } ``` #### **流量控制器类型**: | 控制器类型 | 实现类 | 适用场景 | |--------------------|----------------------|------------------------------| | **直接拒绝** | `DefaultController` | 硬性QPS/线程数限制 | | **冷启动** | `WarmUpController` | 系统预热避免瞬时过载 | | **匀速排队** | `RateLimiterController` | 请求间隔均匀化(漏桶算法) | --- ### **四、关键技术设计** 1. **动态规则加载** - 规则通过`FlowRuleManager`管理,支持控制台实时推送更新。 2. **集群限流** - 基于Token Server协调各节点配额(需部署独立服务)。 3. **低性能损耗** - 滑动窗口计数使用无锁设计(`AtomicLong`),StatisticSlot异步统计。 --- ### **五、代码示例** 定义资源与规则: ```java // 初始化限流规则 private static void initFlowRule() { List<FlowRule> rules = new ArrayList<>(); FlowRule rule = new FlowRule("orderQuery"); // 资源名 rule.setCount(50); // QPS阈值 rule.setGrade(RuleConstant.FLOW_GRADE_QPS); rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT); // 直接拒绝 rules.add(rule); FlowRuleManager.loadRules(rules); } // 业务代码中埋点 try (Entry entry = SphU.entry("orderQuery")) { // 业务逻辑 } catch (BlockException e) { // 被限流时的处理 } ``` --- ### **适用场景分析** | **场景** | **推荐控制器** | **优势** | |----------------------|----------------------|------------------------------| | API接口突发流量 | `DefaultController` | 快速拒绝超出阈值请求 | | 秒杀系统预热 | `WarmUpController` | 避免冷启动压垮系统 | | MQ消费端削峰填谷 | `RateLimiterController` | 匀速消费,保护下游服务 | --- ### **注意事项** 1. **规则生效范围**:`limitApp`参数可指定针对特定调用方限流。 2. **资源名唯一性**:不同资源独立统计,需避免命名冲突。 3. **集群限流延迟**:Token Server模式存在网络通信开销,不适用于毫秒级精度场景。 > 参考:Sentinel 限流的实现依赖于实时统计和规则链的高效协同,其滑动窗口模型在精度与性能间取得了良好平衡[^1]。在实际应用中需根据流量特征选择合适的控制策略[^2],并通过 Sentinel 控制台实时监控调整规则[^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值