顾乔芝士网

持续更新的前后端开发技术栈

一个注解搞定接口限流的完整实现指南

一个注解搞定接口限流的完整实现指南


注解实现的功能如下

  1. 支持根据配置动态选择分布式限流器或者单机限流器
  2. 支持类级别使用限流
  3. 支持方法级别限流(优先)
  4. 支持多个方法使用相同的限流器(分组限流)
  5. 支持spel表达指定限流主体

限流器实现的功能

  1. 双存储结构:漏桶算法+滑动窗口
  2. 自旋锁优化:SpinLock实现轻量级同步
  3. 自动清理机制:过期数据定时回收

一、限流器核心实现(面向接口设计)

1. 漏桶算法实现

/**
 * @Author: 公众号: 加瓦点灯
 * @Date: 2025-03-20-18:00
 * @Description:
 */
public class LocalRateLimiter implements IRateLimiter {
    private double rate;         // 漏水速率(请求/秒)
    private long capacity;       // 桶容量(放大1000倍提升精度)
    private Map requestCountMap = new HashMap<>(); // 当前水量
    private Map requestTimeMap = new HashMap<>();  // 最后请求时间
    private SpinLock lock = new SpinLock(); // 自旋锁

    // 核心限流判断
    public boolean isGranted(String key) {
        try {
            lock.lock();
            long current = System.currentTimeMillis();
            cleanUp(current); // 清理过期数据
            
            Long lastTime = requestTimeMap.get(key);
            long count = requestCountMap.getOrDefault(key, 0L);
            
            // 首次访问初始化
            if (lastTime == null) {
                requestTimeMap.put(key, current);
                requestCountMap.put(key, 1000L);
                return true;
            }
            
            // 计算漏水量 = 时间间隔 * 漏水速率
            long leaked = (long) ((current - lastTime) * rate);
            count = Math.max(count - leaked, 0);
            
            // 判断剩余容量
            if (count < capacity requestcountmap.putkey count 1000 requesttimemap.putkey current return true return false finally lock.unlock 2 private void cleanuplong current if current - lastcleantime> EXPIRE_MS) {
            Iterator<Map.Entry> it = requestTimeMap.entrySet().iterator();
            while (it.hasNext()) {
                Map.Entry entry = it.next();
                if (entry.getValue() < current - EXPIRE_MS) {
                    it.remove();
                    requestCountMap.remove(entry.getKey());
                }
            }
            lastCleanTime = current;
        }
    }
}

实现要点

  • 使用双Map分离存储时间戳和计数器
  • 自旋锁保证线程安全(适合高并发场景)
  • 清理机制防止内存泄漏

限流器代码详细解读移步:轻量级限流器源码解读


二、注解定义与字段说明

/**
 * @Author: 公众号: 加瓦点灯
 * @Date: 2025-03-20-18:00
 * @Description:
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLimit {
    /**
     * SPEL表达式指定限流主体
     * 示例: #user.id / #request.getHeader('X-UID')
     */
    String key() default "";
    
    /**
     * 时间窗口内允许的请求数
     */
    int limit() default 1;
    
    /**
     * 时间窗口长度(秒)
     */
    int interval() default 1;
    
    /**
     * 限流器分组(相同分组共享限流器)
     */
    String group() default "";
    
    /**
     * 触发限流时的提示信息
     */
    String msg() default "请求过于频繁";
    
    /**
     * 类级别限流时排除的方法名
     */
    String[] excludeMethods() default {};
}

三、切面实现与限流控制

1. 切面处理流程

/**
 * @Author: 公众号: 加瓦点灯
 * @Date: 2025-03-20-18:00
 * @Description:
 */
@Aspect
@Component
public class AccessLimitAspect {
    @Around("within(@com.shemuel.site.annotation.AccessLimit *) && within(com.shemuel.site.controller..*) || " +
            "@annotation(com.shemuel.site.annotation.AccessLimit) && within(com.shemuel.site.controller..*)")
    public Object beforeClassAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Object target = joinPoint.getTarget();

        // 获取方法上的注解
        AccessLimit methodAnnotation = AnnotationUtils.findAnnotation(method, AccessLimit.class);
        // 获取类上的注解
        AccessLimit classAnnotation = AnnotationUtils.findAnnotation(target.getClass(), AccessLimit.class);

        AccessLimit activeAnnotation = null;

        if (classAnnotation != null) {
            activeAnnotation = classAnnotation;
        }
        // method上的优先生效
        if (methodAnnotation != null) {
            activeAnnotation = methodAnnotation;
        }

        if (activeAnnotation == null) {
            return joinPoint.proceed();
        }

        // 1. 解析SpEL表达式,获取限流Key对应的参数值
        String keyValue = parseKey(activeAnnotation.key(), joinPoint);
        log.info("拦截类注解 - 方法名: {}" , method.getName());
        log.info("拦截类注解 - 类名: {} " , joinPoint.getTarget().getClass().getName());
        String accessGroup = getAccessGroup(activeAnnotation.group(), target.getClass().getName(), method.getName());
        IRateLimiter rateLimiter = rateLimiterFactory.getRateLimiter(accessGroup);
        if (rateLimiter == null) {
            return joinPoint.proceed();
        }
        log.info("限流的key: " + keyValue);
        log.info("限流的group: " + accessGroup);
        boolean granted = rateLimiter.isGranted(keyValue);
        if (!granted) {
            return RestResult.error(activeAnnotation.msg());
        }
        return joinPoint.proceed();
    }

    /**
     * 指定了限流key, 按照key进行限流
     * key 是spel 则解析
     * 未指定key, 按照用户维度进行限流
     * 已登录的接口,都需要按照用户维度限流
     */
    public String parseKey(String keyRegex, ProceedingJoinPoint joinPoint) {
        String className = joinPoint.getTarget().getClass().getName();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        // 优先使用指定的spel表达式的key
        if (StringUtils.isNotEmpty(keyRegex)) {
            String[] parameterNames = parameterNameDiscoverer.getParameterNames(method);
            StandardEvaluationContext context = new StandardEvaluationContext();
            // 将方法参数名和值绑定到上下文
            Object[] args = joinPoint.getArgs();
            for (int i = 0; i < args.length; i++) {
                context.setVariable(parameterNames[i], args[i]);
            }

            // 解析表达式
            Expression expression = parser.parseExpression(keyRegex);
            String value = expression.getValue(context, String.class);
            // 解析成功,返回解析结果
            if (StringUtils.isNotEmpty(value)) {
                return  value;
            }
            // 解析失败,就按照原始的key进行限流
            return keyRegex;
        }
        // 如果key为空,使用用户id
        if (StpUtil.isLogin()) {
            // 已登录
            // ,按照用户 + 方法级别进行限流
            return  StpUtil.getLoginIdAsString();
        }

        // 没有登录,没有key,按照方法级别进行限流
        return  className + method.getName();
    }

    public String getAccessGroup(String group, String className, String methodName) {
        return StringUtils.isEmpty(group)
                ? className + "#" + methodName
                : group;
    }
}

四、启动时初始化限流器

1. 限流器工厂实现

package com.shemuel.site.utils;

import com.shemuel.site.annotation.AccessLimit;
import com.shemuel.site.common.LocalRateLimiter;
import com.shemuel.site.exception.ServiceException;
import com.shemuel.site.service.IRateLimiter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @Author: 公众号: 加瓦点灯
 * @Date: 2025-03-20-18:00
 * @Description:
 */
@Component
public class RateLimiterFactory {

    @Value("${rateLimiter.type: local}")
    private String rateLimiterType;

    private ConcurrentHashMap rateLimiterMap = new ConcurrentHashMap<>();


    public  IRateLimiter createRateLimiter(AccessLimit accessLimit) {
       if ("redis".equals(rateLimiterType)){
           throw new RuntimeException("redis限流暂未实现");
       }else {
           return new LocalRateLimiter( accessLimit.interval(), accessLimit.limit());
       }
    }

    public void addRateLimiter(String group, IRateLimiter rateLimiter) {
        rateLimiterMap.put(group, rateLimiter);
    }

    public IRateLimiter getRateLimiter(String group) {
        return rateLimiterMap.get(group);
    }

    public Set getAllRateLimiterGroups() {
        return rateLimiterMap.keySet();
    }

    public IRateLimiter removeRateLimiter(String group) {
        return rateLimiterMap.remove(group);
    }
}

2. 项目启动初始化限流器工厂

/**
 * @Author: 公众号: 加瓦点灯
 * @Date: 2025-03-20-18:00
 * @Description:
 */
@Component
@Slf4j
public class RateLimiterInitializer implements ApplicationRunner {

    @Autowired
    private ApplicationContext applicationContext;

    @Autowired
    private RateLimiterFactory rateLimiterFactory;

    @Override
    public void run(ApplicationArguments args) {
        Map beansWithAnnotation = applicationContext.getBeansWithAnnotation(RestController.class);

        for (Object controller : beansWithAnnotation.values()) {
            Class clazz = AopUtils.getTargetClass(controller);
            processClassAnnotations(clazz);
        }
        log.info("RateLimiterInitializer initialized {}", JSON.toJSONString(rateLimiterFactory.getAllRateLimiterGroups()));
    }

    private void processClassAnnotations(Class clazz) {
        AccessLimit classAnnotation = clazz.getAnnotation(AccessLimit.class);
        Set excludes = new HashSet<>();
        // 先处理类上的注解
        if (classAnnotation != null) {
            // 记录排除的方法
            excludes.addAll(new HashSet<>(Arrays.asList(classAnnotation.excludeMethods())));
        }
        // 遍历所有方法
        for (Method method : clazz.getDeclaredMethods()) {

            // 优先处理类注解
            if (classAnnotation != null) {
                doCreateLimiter(clazz, classAnnotation, method);
            }

            AccessLimit methodAnnotation = method.getAnnotation(AccessLimit.class);
            // 再处理方法注解
            if (methodAnnotation != null) {
                doCreateLimiter(clazz, methodAnnotation, method);
            }
        }

        // 处理排除的方法
        if (!CollectionUtils.isEmpty(excludes)) {
            for (String exclude : excludes) {
                String group = classAnnotation.group().isEmpty()
                        ? clazz.getName() + "#" + exclude
                        : classAnnotation.group();
                rateLimiterFactory.removeRateLimiter(group);
            }
        }

    }

    private void doCreateLimiter(Class clazz, AccessLimit classAnnotation, Method method) {
        String group = classAnnotation.group().isEmpty()
                ? clazz.getName() + "#" + method.getName()
                : classAnnotation.group();
        IRateLimiter rateLimiter = rateLimiterFactory.createRateLimiter(classAnnotation);
        rateLimiterFactory.addRateLimiter(group, rateLimiter);
    }

五、使用场景示例

1. 类级别限流(所有方法共享)

/**
 * @Author: 公众号: 加瓦点灯
 * @Date: 2025-03-20-18:00
 * @Description:
 */
@RestController
@AccessLimit(
    group = "orderAPI", 
    limit = 100, 
    interval = 60
)
public class OrderController {
    // 所有方法共享每秒100次限制
    @GetMapping("/list") 
    public Result listOrders() { ... }
    
    // 排除特定方法
    @AccessLimit(excludeMethods = "getDetail")
    @GetMapping("/detail")
    public Result getDetail() { ... }
}

2. 方法级别独立限流

/**
 * @Author: 公众号: 加瓦点灯
 * @Date: 2025-03-20-18:00
 * @Description:
 */
@RestController
public class PaymentController {
    @AccessLimit(
        limit = 10, 
        interval = 5
    )
    @PostMapping("/pay")
    public Result createPayment() { ... }
}

3. 跨接口共享限流器

// 支付相关接口共享限流
@AccessLimit(group = "payment", limit = 50, interval = 10)
@PostMapping("/alipay")
public Result aliPay() { ... }

@AccessLimit(group = "payment", limit = 50, interval = 10)
@PostMapping("/wechatPay")
public Result wechatPay() { ... }

4. SPEL动态限流主体

// 根据用户ID限流
@AccessLimit(
    key = "#user.id", 
    limit = 5, 
    interval = 60
)
@GetMapping("/profile")
public Result getProfile(@Auth User user) { ... }

// 根据请求IP限流  
@AccessLimit(
    key = "#request.remoteAddr",
    limit = 100, 
    interval = 60
)
@GetMapping("/api")
public Result publicApi(HttpServletRequest request) { ... }

通过以上实现,我们构建了一个灵活、可扩展的注解式限流系统。开发人员只需通过简单的注解配置,即可实现从简单到复杂的各种限流场景,同时保持业务代码的整洁性。

最后

欢迎关注gzh:加瓦点灯, 您的支持是我创作的最大动力!

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言