开发者

springboot自定义注解RateLimiter限流注解技术文档详解

目录
  • 什么是限流
    • 系统架构
    • 核心组件详解
      • 1. 限流注解 (@RateLimiter)
      • 2. 限流类型枚举 (RateLimitType)
      • 3. 限流异常类 (RateLimitException)
      • 4. 全局异常处理器 (RateLimitExceptionHandler)
      • 5. IP工具类 (IpUtils)
    • 技术实现原理
      • 1. AOP切面拦截
      • 2. 缓存数据结构
    • 完整代码示例
      • 1. 控制器示例
      • 2. 统一返回对象
    • 使用指南
      • 1. 基本使用
      • 2. 不同场景的配置建议
      • 3. 双重限流配置
  • 总结

    什么是限流

    限流是一种控制系统访问频率的技术手段,就像高速公路的收费站控制车流量一样。

    生活场景类比:

    • 银行ATM机:每张卡每天最多取款5次
    • 手机验证码:每个手机号每分钟最多发送1条
    • 网站登录:每个IP每分钟最多尝试5次

    技术价值:

    1. 防止恶意攻击:阻止暴力破解、恶意爬虫
    2. 保护系统稳定:避免瞬间大量请求压垮服务器
    3. 提升用户体验:确保正常用户的访问质量
    4. 节约成本:减少不必要的资源消耗

    系统架构

    ┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
    │   用户请求      │───→│   限流切面      │───→│   业务接口      │
    │   (HTTP API)   │    │  (AOP拦截)     │    │  (Controller)   │
    └─────────────────┘    └─────────────────┘    └─────────────────┘
                                  │
                                  ▼
                        ┌─────────────────┐
                        │   限流服务      │
                        │ (核心逻辑处理)  │
                        └─────────────────┘
                                  │
                                  ▼
                        ┌─────────────────┐
                        │   缓存存储      │
                        │ (EhCache/Redis) │
                        └─────────────────┘

    工作流程:

    1. 用户发起HTTP请求
    2. Spring AOP切面拦截带有@RateLimiter注解的方法
    3. 限流服务根据注解配置生成限流键
    4. 从缓存中获取当前访问次数
    5. 判断是否超过限制,决定放行或拒绝
    6. 更新缓存中的计数器

    核心组件详解

    1. 限流注解 (@RateLimiter)

    这是系统的核心注解,定义了限流的各种参数:

    package cn.jbolt.config.anno.rateLimiter;
    
    import org.springframework.core.annotation.AliasFor;
    import Java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE, ElementType.METHOD})
    public @interface RateLimiter {
    
        /**
         * 缓存前缀 - 用于区分不同业务的限流数据
         */
        String prefix() default "jblimit:";
    
        /**
         * 时间窗口(秒) - 限流的时间范围
         */
        int time() default 60;
    
        /**
         * 允许访问次数 - 时间窗口内最大访问次数
         */
        @AliasFor(attribute = "count")
        int value() default 12;
    
        /**
         * 限制类型 - 决定按什么维度限流
         */
        RateLimitType limitType() default RateLimitType.DEFAULT;
    
        /**
         * 限制提示消息 - 触发限流时返回的错误信息
         */
        String msg() default "操作过于频繁,请稍后重试";
    
        /**
         * 允许访问次数 - 与value互为别名
         */
        @AliasFor(attribute = "value")
        int count() default 12;
    
        /**
         * 自定义键 - 当limitType为CUSTOM时使用
         */
        String customKey() default "";
    
        /**
         * 是否启用 - 可用于动态开关限流功能
         */
        boolean enabled() default true;
    
        /**
         * 额外的时间窗口限制(秒)
         * 实现双重限流:比如1秒最多1次 + 1分钟最多10次
         */
        int extraTime() default -1;
    
        /**
         * 额外时间窗口内的允许访问次数
         */
        int extraCount() default -1;
    
        /**
         * 额外限制的提示消息
         */
        String extraMsg() default "";
    }

    2. 限流类型枚举 (RateLimitType)

    package cn.jbolt.config.anno.rateLimiter;
    
    public enum RateLimitType {
        /**
         * 默认限制(全局)
         * 所有请求共享一个计数器
         */
        DEFAULT,
        
        /**
         * 基于IP地址限制
         * 每个IP独立计数
         */
        IP,
        
        /**
         * 基于用户ID限制
         * 每个登录用户独立计数
         */
        USER,
        
        /**
         * 基于自定义KEY限制
         * 根据业务逻辑自定义限流维度
         */
        CUSTOM
    }

    3. 限流异常类 (RateLimitException)

    package cn.jbolt.config.exception;
    
    public class RateLimitException extends RuntimeException {
        
        private final String message;
        private final int retryAfter;
        
        public RateLimitException(String message) {
            this(message, 0);
        }
        
        public RateLimitException(String message, int retryAfter) {
            super(message);
            this.message = message;
            this.retryAfter = retryAfter;
        }
        
        @Override
        public String getMessage() {
            return message;
        }
        
        public int getRetryAfter() {
            return retryAfter;
        }
    }

    4. 全局异常处理器 (RateLimitExceptionHandler)

    package cn.jbolt.config.handler;
    
    import cn.jbolt.config.exception.RateLimitException;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    
    import javax.servlet.http.HttpServletResponse;
    import java.util.HashMap;
    import java.util.Map;
    
    @RestControllerAdvice
    public class RateLimitExceptionHandler {
        
        @ExceptionHandler(RateLimitException.class)
        public ResponseEntity<Map<String, Object>> handleRateLimitException(
                RateLimitException e, HttpServletResponse response) {
            
            Map<String, Object> result = new HashMap<>();
            result.putphp("code", HttpStatus.TOO_MANY_REQUESTS.value());
            result.put("message", e.getMessage());
            result.put("data", null);
            
            // 设置HTTP响应头,告诉客户端多久后可以重试
            if (e.getRetryAfter() > 0) {
                response.setHeader("Retry-After", String.valueOf(e.getRetryAfter()));
            }
            response.setHeader("X-RateLimit-Window", "60");
       android     
            return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(result);
        }
    }

    5. IP工具类 (IpUtils)

    package cn.jbolt.util;
    
    import org.springframework.util.StringUtils;
    import javax.servlet.http.HttpServletRequest;
    
    public class IpUtils {
        
        private static final String[] IP_HEADER_NAMES = {
            "X-Forwarded-For",
            "X-Real-IP", 
            "Proxy-Client-IP",
            "WL-Proxy-Client-IP",
            "HTTP_CLIENT_IP",
            "HTTP_X_FORWARDED_FOR"
        };
        
        private static final String UNKNOWN = "unknown";
        private static final String LOCALHOST_IPV4 = "127.0.0.1";
        private static final String LOCALHOST_IPV6 = "0:0:0:0:0:0:0:1";
        
        /**
         * 获取客户端真实IP地址
         * 处理代理服务器、负载均衡器等场景
         */
        public static String getClientIp(HttpServletRequest request) {
            if (request == null) {
                return UNKNOWN;
            }
            
            String ip = null;
            
            // 依次检查各种可能的IP头
            for (String header : IP_HEADER_NAMES) {
                ip = request.getHeader(header);
                if (isValidIp(ip)) {
                    break;
                }
            }
            
            // 如果头信息中没有找到,则使用getRemoteAddr
            if (!isValidIp(ip)) {
                ip = request.getRemoteAddr();
                if (LOCALHOST_IPV6.equals(ip)) {
                    ip = LOCALHOST_IPV4;
                }
            }
            
            // 处理多个IP的情况(X-Forwarded-For可能包含多个IP)
            if (StringUtils.hasText(ip) && ip.contains(",")) {
                ip = ip.split(",")[0].trim();
            }
            
            return StringUtils.hasText(ip) ? ip : UNKNOWN;
        }
        
        /**
         * 检查IP是否有效
         */
        private static boolean isValidIp(String ip) {
            return StringUtils.hasText(ip) && !UNKNOWN.equalsIgnoreCase(ip);
        }
    }

    技术实现原理

    1. AOP切面拦截

    系统使用Spring AOP在方法执行前进行拦截,这是一个核心的限流切面类:

    package cn.jbolt.config.ASPect;
    
    import cn.jbolt.config.anno.rateLimiter.RateLimiter;
    import cn.jbolt.config.anno.rateLimiter.RateLimitType;
    import cn.jbolt.config.exception.RateLimitException;
    import cn.jbolt.util.IpUtils;
    import cn.jbolt.util.cache.RateLimiterCache;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Component;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    
    import javax.servlet.http.HttpServletRequest;
    import java.util.concurrent.TimeUnit;
    
    @Aspect
    @Component
    public class RateLimiterAspect {
        
        private static final Logger logger = LoggerFactory.getLogger(RateLimiterAspect.class);
        
        @Around("@annotation(rateLimiter)")
        public Object around(ProceedingJoinPoint point, RateLimiter rateLimiter) throws Throwable {
            
            // 检查是否启用限流
            if (!rateLimiter.enabled(http://www.devze.com)) {
                return point.proceed();
            }
            
            // 获取HTTP请求对象
            HttpServletRequest request = getCurrentRequest();
            if (request == null) {
                logger.warn("无法获取HttpServletRequest,跳过限流检查");
                return point.proceed();
            }
            
            // 生成限流键
            String limitKey = generateLimitKey(point, rateLimiter, request);
            
            // 执行主要限流检查
            checkRateLimit(limitKey, rateLimiter.time(), rateLimiter.count(), rateLimiter.msg());
            
            // 执行额外限流检查(如果配置了)
            if (rateLimiter.extraTime() > 0 && rateLimiter.extraCount() > 0) {
                String extraLimitKey = limitKey + ":extra";
                String extraMsg = rateLimiter.extraMsg().isEmpty() ? rateLimiter.msg() : rateLimiter.extraMsg();
                checkRateLimit(extraLimitKey, rateLimiter.extraTime(), rateLimiter.extraCount(), extraMsg);
            }
            
            // 所有限流检查通过,继续执行业务方法
            return point.proceed();
        }
        
        /**
         * 执行限流检查
         */
        private void checkRateLimit(String key, int timeWindow, int maxCount, String message) {
            try {
                // 增加计数器并获取当前访问次数
                int currentCount = RateLimiterCache.incrementAndGet(key, timeWindow, TimeUnit.SECONDS);
                
                logger.debug("限流检查: key={}, 当前次数={}, 限制次数={}", key, currentCount, maxCount);
                
                // 检查是否超过限制
                if (currentCount > maxCount) {
                    long ttl = RateLimiterCache.getTtl(key);
                    logger.warn("触发限流: key={}, 当前次数={}, 限制次数={}, 剩余时间={}秒", 
                              key, currentCount, maxCount, ttl);
                    throw new RateLimitException(message, (int) ttl);
                }
                
            } catch (RateLimitException e) {
                throw e;
            } catch (Exception e) {
                logger.error("限流检查异常: key={}", key, e);
                // 限流服务异常时,选择放行而不是阻塞
            }
        }
        
        /**
         * 生成限流键
         */
        private String generateLimitKey(ProceedingJoinPoint point, RateLimiter rateLimiter, HttpServletRequest request) {
            StringBuilder keyBuilder = new StringBuilder();
            keyBuilder.append(rateLimiter.prefix());
            
            // 添加方法签名
            String methodSignature = point.getSignature().toShortString();
            keyBuilder.append(methodSignature);
            
            // 根据限流类型添加不同的标识
            switch (rateLimiter.limitType()) {
                case IP:
                    keyBuilder.append(":ip:").append(IpUtils.getClientIp(request));
                    break;
                case USER:
                    String userId = getCurrentUserId(request);
                    keyBuilder.append(":user:").append(userId != null ? userId : "anonymous");
                    break;
                case CUSTOM:
                    keyBuilder.append(":custom:").append(rateLimiter.customKey());
                    break;
                case DEFAULT:
                default:
                    keyBuilder.append(":default:global");
                    break;
            }
            
            // 添加时间窗口,确保不同时间窗口的限流独立
            keyBuilder.append(":").append(rateLimiter.time());
            
            String finalKey = keyBuilder.toString();
            logger.debug("生成限流键: {}", finalKey);
            return finalKey;
        }
        
        /**
         * 获取当前HTTP请求
         */
        private HttpServletRequest getCurrentRequest() {
            try {
                ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                return attrs != null ? attrs.getRequest() : null;
            } catch (Exception e) {
                logger.warn("获取HttpServletRequest失败", e);
                return null;
            }
        }
        
        /**
         * 获取当前用户ID
         * 这里需要根据实际的用户认证体系来实现
         */
        private String getCurrentUserId(HttpServletRequest request) {
            // 方案1:从Session中获取
            Object userId = request.getSession().getAttribute("userId");
            if (userId != null) {
                return userId.toString();
            }
            
            // 方案2:从JWT Token中获取
            String token = request.getHeader("Authorization");
            if (token != null && token.startsWith("Bearer ")) {
                // 解析JWT获取用户ID
                // return JwtUtils.getUserIdFromToken(token);
            }
            
            // 方案3:从请求参数中获取
            String userIdParam = request.getParameter("userId");
            if (userIdParam != null) {
                return userIdParam;
            }
            
            return null;
        }
    }

    2. 缓存数据结构

    系统使用一个包装类来存储缓存数据:

    package cn.jbolt.util.cache;
    
    import java.io.Serializable;
    import java.util.concurrent.TimeUnit;
    
    public class CacheWrapper implements Serializable {
        
        private static final long serialVersionUID = 1L;
        
        private Object value;
        private long timestamp;
        private long durationMillis;
        
        public CacheWrapper() {
        }
        
        public CacheWrapper(Object value, long duration, TimeUnit unit) {
            this.value = value;
            this.timestamp = System.currentTimeMillis();
            this.durationMillis = unit.toMillis(duration);
        }
        
        /**
         * 检查是否已过期
         */
        public boolean isExpired() {
            return System.currentTimeMillis() - timestamp > durationMillis;
        }
        
        /**
         * 获取剩余过期时间(毫秒)
         */
        public long getRemainingTime() {
            long elapsed = System.currentTimeMillis() - timestamp;
            return Math.max(0, durationMillis - elapsed);
        }
        
        // getter和setter方法
        public Object getValue() {
            return value;
        }
        
        public void setValue(Object value) {
            this.value = value;
        }
        
        public long getTimestamp() {
            return timestamp;
        }
        
        public void setTimestamp(long timestamp) {
            this.timestamp = timestamp;
        }
        
        public long getDurationMillis() {
            return durationMillis;
        }
        
        public void setDurationMillis(long durationMillis) {
            this.durationMillis = durationMillis;
        }
    }

    完整代码示例

    1. 控制器示例

    package cn.jbolt.controller;
    
    import cn.jbolt.config.anno.rateLimiter.RateLimiter;
    import cn.jbolt.config.anno.rateLimiter.RateLimitType;
    import org.springframework.web.bind.annotation.*;
    
    @RestController
    @RequestMapping("/api")
    public class DemoController {
        
        /**
         * 登录接口 - 防止暴力破解
         * 每个IP每分钟最多尝试5次
         */
        @PostMapping("/login")
        @RateLimiter(
            limitType = RateLimitType.IP,
            time = 60, 
            count = 5,
            msg = "登录尝试过于频繁,请1分钟后重试"
        )
        public Result login(@RequestBody LoginRequest request) {
            // 登录逻辑
            if (isValidUser(request.getUsername(), request.getPassword())) {
                return Result.success("登录成功");
            } else {
                return Result.error("用户名或密码错误");
            }
        }
        
        /**
         * 发送验证码 - 防止恶意发送
         * 每个IP每分钟最多3次
         */
        @PostMapping("/sms/send")
        @RateLimiter(
            limitType = RateLimitType.IP,
            time = 60, 
            count = 3,
            msg = "验证码发送过于频繁,请稍后重试"
        )
        public Result sendSms(@RequestBody SmsRequest request) {
            // 发送短信逻辑
            boolean success = smsService.sendCode(request.getPhone());
            return success ? Result.success("发送成功") : Result.error("发送失败");
        }
        
        /**
         * 查询接口 - 防止爬虫
         * 每个IP每分钟最多100次
         */
        @GetMapping("/products")
        @RateLimiter(
            limitType = RateLimitType.IP,
            time = 60, 
            count = 100,
            msg = "查询过于频繁,请稍后重试"
        )
        public Result getProducts(@RequestParam(defaultValue = "1") int page) {
            // 查询商品逻辑
            List<Product> products = productService.getProducts(page);
            return Result.success(products);
        }
       js 
        /**
         * 用户操作 - 防止频繁操作
         * 每个用户每分钟最多30次
         */
        @PostMapping("/user/update")
        @RateLimiter(
            limitType = RateLimitType.USER,
            time = 60, 
            count = 30,
            msg = "操作过于频繁,请稍后重试"
        )
        public Result updateUser(@RequestBody UserUpdateRequest request) {
            // 更新用户信息逻辑
            boolean success = userService.updateUser(request);
            return success ? Result.success("更新成功") : Result.error("更新失败");
        }
        
        /**
         * 关键操作 - 严格限流
         * 1秒最多1次 + 1分钟最多5次
         */
        @PostMapping("/transfer")
        @RateLimiter(
            limitType = RateLimitType.USER,
            time = 1, count = 1, msg = "操作过于频繁,请稍后再试",
            extraTime = 60, extraCount = 5, extraMsg = "您在1分钟内的操作次数已达上限"
        )
        public Result transfer(@RequestBody TransferRequest request) {
            // 转账逻辑
            boolean success = transferService.transfer(request);
            return success ? Result.success("转账成功") : Result.error("转账失败");
        }
        
        /**
         * 自定义限流 - 按商品限制
         * 每个商品每分钟最多下单20次
         */
        @PostMapping("/order/{productId}")
        @RateLimiter(
            limitType = RateLimitType.CUSTOM,
            customKey = "product_order",
            time = 60, 
            count = 20,
            msg = "该商品下单过于频繁,请稍后重试"
        )
        public Result createOrder(@PathVariable String productId, @RequestBody OrderRequest request) {
            // 创建订单逻辑
            Order order = orderService.createOrder(productId, request);
            return Result.success(order);
        }
        
        // 辅助方法
        private boolean isValidUser(String username, String password) {
            // 实际的用户验证逻辑
            return "admin".equals(username) && "123456".equals(password);
        }
    }

    2. 统一返回对象

    package cn.jbolt.common;
    
    public class Result {
        private int code;
        private String message;
        private Object data;
        
        public static Result success(Object data) {
            Result result = new Result();
            result.code = 200;
            result.message = "success";
            result.data = data;
            return result;
        }
        
        public static Result error(String message) {
            Result result = new Result();
            result.code = 500;
            result.message = message;
            result.data = null;
            return result;
        }
        
        // getter和setter方法
        public int getCode() {
            return code;
        }
        
        public void setCode(int code) {
            this.code = code;
        }
        
        public String getMessage() {
            return message;
        }
        
        public void setMessage(String message) {
            this.message = message;
        }
        
        public Object getData() {
            return data;
        }
        
        public void setData(Object data) {
            this.data = data;
        }
    }

    使用指南

    1. 基本使用

    // 最简单的用法 - 使用默认配置
    @RateLimiter(limitType = RateLimitType.IP)
    public String simpleApi() {
        return "success";
    }
    
    // 自定义时间窗口和次数
    @RateLimiter(
        limitType = RateLimitType.IP,
        time = 60,    // 60秒
        count = 100   // 最多100次
    )
    public String customApi() {
        return "success";
    }

    2. 不同场景的配置建议

    // 登录接口 - 严格限制
    @RateLimiter(
        limitType = RateLimitType.IP,
        time = 60, count = 5,
        msg = "登录尝试过于频繁,请1分钟后重试"
    )
    
    // 查询接口 - 适中限制
    @RateLimiter(
        limitType = RateLimitType.IP,
        time = 60, count = 100,
        msg = "查询过于频繁,请稍后重试"
    )
    
    // 用户操作 - 按用户限制
    @RateLimiter(
        limitType = RateLimitType.USER,
        time = 60, count = 30,
        msg = "操作过于频繁,请稍后重试"
    )
    
    // 全局保护 - 系统级限制
    @RateLimiter(
        limitType = RateLimitType.DEFAULT,
        time = 60, count = 200,
        msg = "系统繁忙,请稍后重试"
    )

    3. 双重限流配置

    // 严格的双重限流:秒级 + 分钟级
    @RateLimiter(
        limitType = RateLimitType.IP,
        time = 1, count = 1, msg = "请求过于频繁,请稍后再试",
        extraTime = 60, extraCount = 10, extraMsg = "您在1分钟内的请求次数已达上限"
    )
    
    // 适中的双重限流:分钟级 + 小时级
    @RateLimiter(
        limitType = RateLimitType.IP,
        time = 60, count = 100, msg = "1分钟内请求过多",
        extraTime = 3600, extraCount = 1000, extraMsg = "1小时内请求过多"
    )

    总结

    以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(wwlbbhqw.cppcns.com)。

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新开发

    开发排行榜