百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

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

mhr18 2025-04-09 17:59 14 浏览 0 评论

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


注解实现的功能如下

  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:加瓦点灯, 您的支持是我创作的最大动力!

相关推荐

【推荐】一个开源免费、AI 驱动的智能数据管理系统,支持多数据库

如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!.前言在当今数据驱动的时代,高效、智能地管理数据已成为企业和个人不可或缺的能力。为了满足这一需求,我们推出了这款开...

Pure Storage推出统一数据管理云平台及新闪存阵列

PureStorage公司今日推出企业数据云(EnterpriseDataCloud),称其为组织在混合环境中存储、管理和使用数据方式的全面架构升级。该公司表示,EDC使组织能够在本地、云端和混...

对Java学习的10条建议(对java课程的建议)

不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...

SQLShift 重大更新:Oracle→PostgreSQL 存储过程转换功能上线!

官网:https://sqlshift.cn/6月,SQLShift迎来重大版本更新!作为国内首个支持Oracle->OceanBase存储过程智能转换的工具,SQLShift在过去一...

JDK21有没有什么稳定、简单又强势的特性?

佳未阿里云开发者2025年03月05日08:30浙江阿里妹导读这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。阅前声明:本文介绍的内容基于AJDK21.0.5[1]以及以上...

「松勤软件测试」网站总出现404 bug?总结8个原因,不信解决不了

在进行网站测试的时候,有没有碰到过网站崩溃,打不开,出现404错误等各种现象,如果你碰到了,那么恭喜你,你的网站出问题了,是什么原因导致网站出问题呢,根据松勤软件测试的总结如下:01数据库中的表空间不...

Java面试题及答案最全总结(2025版)

大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...

数据库日常运维工作内容(数据库日常运维 工作内容)

#数据库日常运维工作包括哪些内容?#数据库日常运维工作是一个涵盖多个层面的综合性任务,以下是详细的分类和内容说明:一、数据库运维核心工作监控与告警性能监控:实时监控CPU、内存、I/O、连接数、锁等待...

分布式之系统底层原理(上)(底层分布式技术)

作者:allanpan,腾讯IEG高级后台工程师导言分布式事务是分布式系统必不可少的组成部分,基本上只要实现一个分布式系统就逃不开对分布式事务的支持。本文从分布式事务这个概念切入,尝试对分布式事务...

oracle 死锁了怎么办?kill 进程 直接上干货

1、查看死锁是否存在selectusername,lockwait,status,machine,programfromv$sessionwheresidin(selectsession...

SpringBoot 各种分页查询方式详解(全网最全)

一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...

《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略

《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...

LoadRunner(loadrunner录制不到脚本)

一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...

Redis数据类型介绍(redis 数据类型)

介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...

RMAN备份监控及优化总结(rman备份原理)

今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...

取消回复欢迎 发表评论: