基于Redis+AOP+Lua脚本实现一个服务器限流机制

简介: 基于Redis+AOP+Lua脚本实现一个服务器限流机制

后端某些接口在高并发的压力下往往会导致性能的严重下降,为了维持我们后端服务型的高性能和高可用,我们往往可以对某些接口或某些用户去设计限流机制,控制这些热点接口的访问量,我这里利用Redis的高性能优势,并整合AOP编程和引入Lua限流脚本在SpringBoot中对任意接口或某些用户实现了访问量限流的机制,其中,我这里给出了三种限流机制:用户,IP地址,全局限流

1.定义限流方式

/**
 * 限流类型
 * @Author GuihaoLv
 */
public enum LimitType {
    /**
     * 默认策略全局限流
     */
    DEFAULT,
    /**
     * 根据请求者IP进行限流
     */
    IP,
    /**
     * 根据请求者的用户ID进行限流
     */
    USER,
    /**
     * 根据请求者的部门进行限流
     */
    DEPT,
}

image.gif

2.引入AOP,自定义限流注解和限流处理的切面类

/**
 * 限流注解
 * @Author GuihaoLv
 */
//实例 @RateLimiter(time = 60, count = 5, limitType = LimitType.IP) 效果:同一IP 60秒内最多允许5次登录尝试。
@Target(ElementType.METHOD) //表示该注解仅能标注在方法上,用于对具体方法进行限流控制。
@Retention(RetentionPolicy.RUNTIME) //注解在运行时保留,可通过反射机制读取注解信息,实现动态限流逻辑。
@Documented //注解信息会包含在生成的 JavaDoc 中
public @interface RateLimiter {
    /**
     * 限流key
     */
    public String key() default RedisConstant.RATE_LIMIT_KEY;
    /**
     * 限流时间,单位秒
     */
    public int time() default 60;
    /**
     * 限流次数
     */
    public int count() default 100;
     /**
     * 限流类型
     */
    public LimitType limitType() default LimitType.DEFAULT;
}

image.gif

/**
 * 限流机制 不直接限制 Redis 存储,而是利用 Redis 的 高性能计数 和 自动过期 特性,实现对应用请求频率的控制。
 * Redis 在此场景中作为 临时数据存储工具,其资源消耗可控且可优化,不会对存储系统造成实质性压力。
 */
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.lgh.common.context.UserThreadLocal;
import com.lgh.common.enums.LimitType;
import com.lgh.model.entity.User;
import com.lgh.web.handle.rateLimit.annonation.RateLimiter;
import com.lgh.web.mapper.UserMapper;
import com.lgh.web.service.UserService;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.checkerframework.checker.units.qual.A;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Collections;
import java.util.List;
 /**
 * 限流处理切面
 * @Author GuihaoLv
 */
@Aspect
@Component
//确保仅当配置项 spring.cache.type=redis 时,切面才会生效
@ConditionalOnProperty(prefix = "spring.cache", name = { "type" }, havingValue = "redis", matchIfMissing = false)
public class RateLimiterAspect
{
    private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);
    private RedisTemplate<Object, Object> redisTemplate; //redis 操作模板,用于执行 Lua 脚本和 Redis 命令
    private RedisScript<Long> limitScript; //限流核心逻辑的 Lua 脚本
    @Autowired
    public void setRedisTemplate1(RedisTemplate<Object, Object> redisTemplate)
    {
        this.redisTemplate = redisTemplate;
    }
    @Autowired
    public void setLimitScript(RedisScript<Long> limitScript)
    {
        this.limitScript = limitScript;
    }
    @Autowired
    private ObjectMapper jacksonObjectMapper;
    @Before("@annotation(rateLimiter)")
    public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable
    {
        //1. 获取注解参数
        int time = rateLimiter.time(); //时间窗口(秒)
        int count = rateLimiter.count();// 允许的请求次数
        //2. 生成唯一限流 Key
        String combineKey = getCombineKey(rateLimiter, point);
        //将限流 Key 包装为列表,符合 Redis Lua 脚本的参数传递规范
        List<Object> keys = Collections.singletonList(combineKey);
        try
        {
            //3. 以key为参数执行 Lua 脚本(原子性操作) keys:Redis 存储的 Key
            //Lua脚本会检查Key是否存在,如果不存在则创建并设置过期时间,如果存在则递增计数器。
            Long number = redisTemplate.execute(limitScript, keys, count, time);
            if (StringUtils.isEmpty(number) || number.intValue() > count)
            {
                throw new Exception("访问过于频繁,请稍候再试");
            }
            log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), combineKey);
        } catch (RuntimeException e)
        {
            throw new RuntimeException("服务器限流异常,请稍候再试");
        } catch (Exception e)
        {
            throw e;
        }
    }
    //IP + 类名 + 方法名 的拼接方式,确保不同场景的Key不冲突。
    //Key结构清晰,便于调试和监控(如通过Redis直接查看计数器)。
    public String getCombineKey(RateLimiter rateLimiter, JoinPoint point)
    {
        StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
        switch (rateLimiter.limitType()) {
            case IP:
                try {
                    limitByIp(stringBuffer);
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                break;
            case USER:
                limitByUser(stringBuffer);
                break;
            case DEFAULT:
                limitByDefault(stringBuffer,point);
                break;
        }
           return stringBuffer.toString();
    }
     /**
     * 按IP限流
     * @param stringBuffer
     */
    private final HttpClient httpClient = HttpClient.newHttpClient();
    private void limitByIp(StringBuffer stringBuffer) throws IOException, InterruptedException {
        String[] services = {
                "https://api.ipify.org",
                "https://icanhazip.com"
        };
        for (String serviceUrl : services) {
                HttpRequest request = HttpRequest.newBuilder()
                        .uri(URI.create(serviceUrl))
                        .GET()
                        .build();
                HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
                String ip = response.body().trim();
                stringBuffer.append(":ip:").append(ip);
        }
    }
     /**
     * 全局限流
     * @param stringBuffer
     * @param point
     */
    private void limitByDefault(StringBuffer stringBuffer, JoinPoint point) {
        //按方法限流:拼接类名 + 方法名
        //获取方法签名
        MethodSignature signature = (MethodSignature) point.getSignature();
        //基于反射获取方法对象
        Method method = signature.getMethod();
        //获取方法的声明类
        Class<?> targetClass = method.getDeclaringClass();
        stringBuffer.append(targetClass.getName()).append("-").append(method.getName());
    }
    /**
     * 按用户限流
     * @param stringBuffer
     */
    private void limitByUser(StringBuffer stringBuffer) {
            //获取当前用户
            String userSubject = UserThreadLocal.getSubject();
            User user=new User();
            try {
                user = jacksonObjectMapper.readValue(userSubject, User.class);
            } catch (JsonProcessingException e) {
                throw new RuntimeException("无法获取当前用户");
            }
            stringBuffer.append(":user:").append(user.getId());
    }
}

image.gif

3. 在Redis的配置类中整合Lua语言自定义限流脚本的执行

/**
      * 限流脚本定义
      * @return
      */
     @Bean
     public DefaultRedisScript<Long> limitScript() {
         DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
         redisScript.setScriptText(limitScriptText()); //加载Lua脚本
         redisScript.setResultType(Long.class); // 返回类型为Long
         return redisScript;
     }
     /**
      * 限流脚本
      * 固定窗口计数器算法
      * 如果已经超过阈值,触发限流,直接返回当前值
      * 未超过阈值,递增计数器
      * 同时首次访问时设置过期时间(时间窗口),确保自动清理历史计数
      */
     private String limitScriptText() {
         return "local key = KEYS[1]\n" + //获取传入的限流Key(如用户ID、IP)
                 "local count = tonumber(ARGV[1])\n" + //阈值(允许的最大请求次数)
                 "local time = tonumber(ARGV[2])\n" + //时间窗口(秒)
                 "local current = redis.call('get', key);\n" + //获取当前 Key 的计数
                 "if current and tonumber(current) > count then\n" + //若计数存在且超过阈值,直接返回当前值
                 "    return tonumber(current);\n" +
                 "end\n" +
                 "current = redis.call('incr', key)\n" + //原子性递增 Key 的值
                 "if tonumber(current) == 1 then\n" +
                 "    redis.call('expire', key, time)\n" + //当incr后的值为 1(即首次访问),设置 Key 的过期时间为time秒
                 "end\n" +
                 "return tonumber(current);"; //返回当前计数
     }

image.gif

4. 限流注解的使用

案例1:登录接口IP限流

@RateLimiter( key = "login_attempt", time = 300, // 5分钟 count = 5, 
limitType = LimitType.IP ) 
@PostMapping("/login") 
public Response login(@RequestBody LoginDTO dto) { // 登录逻辑 }

image.gif

案例2:API用户维度限流

@RateLimiter( key = "api_v1:data_export", time = 3600, // 1小时 count = 10,
limitType = LimitType.USER )
@GetMapping("/export")
public void exportData() { // 数据导出逻辑 }

image.gif

这样就能对上述接口实现对应的限流机制了

相关文章
|
2月前
|
消息中间件 Dubbo Java
深入剖析RocketMQ2-实战案例
本文介绍了一个基于电商场景的订单和支付系统实现方案。系统采用SpringBoot+Dubbo+Zookeeper+RocketMQ技术栈,重点解决分布式系统中的数据一致性问题。在订单模块,通过预订单生成、库存扣减、优惠券使用、余额扣减等步骤完成下单流程,并利用RocketMQ实现失败补偿机制。支付模块处理第三方支付回调,通过消息队列异步更新订单状态。系统通过分布式事务保证数据一致性,采用线程池优化消息发送性能,并提供完整的测试方案验证系统功能。本文详细阐述了技术架构、数据库设计、核心业务流程以及异常处理机制
|
2月前
|
Linux API 云计算
零基础保姆级|阿里云计算巢+MacOS/Linux/Windows11部署OpenClaw 技能集成+大模型配置全流程
2026年,AI自动化框架OpenClaw(原Clawdbot)凭借云端+本地双部署、多模型兼容与Skills插件化扩展能力,成为个人与团队实现复杂任务自动化的核心工具。阿里云计算巢提供OpenClaw官方一键部署方案,无需手动配置环境,5分钟即可完成云端部署;本地则支持MacOS、Linux、Windows11全系统部署,搭配阿里云千问、免费Coding Plan大模型API,再通过Skills扩展能力,可实现从信息查询、文件处理到流程自动化的全场景能力。
1034 15
|
2月前
|
人工智能 自然语言处理 Java
大模型应用开发5-SpringAIalibaba实战
本文介绍了SpringAIAlibaba开源项目,该项目基于SpringAI构建,为阿里云通义系列模型提供Java开发实践。主要内容包括: 基础使用:配置模型API、依赖引入、调用示例,支持同步和流式调用; 多种集成方式:对接本地Ollama模型、ChatClient高级API、SSE流式输出; 核心功能实现:提示词模板、结构化输出、持久化内存、文本生成图片/语音; 高级能力:向量数据库、RAG增强检索、工具调用(Tool Calling); MCP协议:标准化工具调用方案,实现服务端工具共享;
|
2月前
|
安全 NoSQL Java
基于JWT+SpringSecurity整合一个单点认证授权机制
本文介绍了基于JWT和SpringSecurity的授权认证机制架构设计。系统采用RBAC权限模型,通过5张表描述用户-角色-权限关系。认证流程包含登录验证、IP检查、密码匹配等环节,使用JWT生成token并保存用户信息到Redis。授权部分利用@PreAuthorize注解和PermissionService实现权限校验,支持单权限、多权限及角色验证。整体架构通过过滤器链实现无状态认证,兼顾安全性和灵活性,为开发者提供了完整的认证授权解决方案。
|
2月前
|
消息中间件 存储 监控
详解Kafka2-进阶机制
本文摘要: 文章详细介绍了Kafka的分区与副本机制,包括生产者分区写入策略(轮询、随机、按key分配、自定义)、消费者组Rebalance机制及其影响,以及消费者分区分配策略(Range、RoundRobin、Sticky)。同时,阐述了Kafka的副本机制、数据存储形式(Segment结构)、消息不丢失机制(生产者ACK、消费者Offset管理)和数据积压问题。此外,还介绍了Kafka的数据清理策略(日志删除与压缩)、配额限速机制(Quotas)及监控工具Kafka-Eagle的使用方法。通过原理分析与
|
2月前
|
人工智能 NoSQL Java
大模型应用开发2-SpringAI实战
本文介绍了SpringAI框架如何整合大语言模型,并详细讲解了应用开发的关键技术。主要内容包括: 核心功能 支持OpenAI、Ollama等主流平台 封装对话模型、向量计算等功能 提供同步/异步调用方式 关键技术实现 会话记忆管理(内存/Redis) 工具调用(Function Calling) 知识增强(RAG)架构 多模态交互(文本/图像) 典型应用场景 文献阅读助手实现 智能客服系统 文档知识库问答 开发实践 配置向量数据库 处理PDF文档 实现工具调用 兼容阿里云平台 该框架显著简化了大模型应用开发
|
2月前
|
人工智能 Linux API
OpenClaw多Agent协作系统实操:本地+阿里云部署与千问/Coding Plan API配置全指南
2026年OpenClaw(原Clawdbot)推出的多Agent协作系统,彻底打破了单一AI智能体的能力边界,让多个AI Agent像人类团队一样实现智能分工、实时信息同步与灵活角色配置,可高效完成内容创作、软件开发、数据分析等复杂复合型任务。在实际落地过程中,开发者不仅需要掌握多Agent协作系统的基础使用逻辑,更需要完成OpenClaw在本地多系统(MacOS/Linux/Windows11)与阿里云的稳定部署,同时实现与阿里云千问大模型API、免费Coding Plan API的无缝对接,才能真正发挥多Agent协作的核心价值。本文将深度解析OpenClaw多Agent协作系统的核心
975 10
|
2月前
|
人工智能 Linux API
阿里云+本地三系统部署 OpenClaw 及精准参数调优手册:千问/Coding Plan模型对接教程
在日常使用OpenClaw(原Clawdbot)的过程中,很多用户都会遇到输出随机、答非所问、重复啰嗦、长度失控等问题,这些并非模型能力不足,而是**温度、最大生成长度、采样参数、惩罚系数**没有匹配场景。与此同时,正确完成云端与本地部署、对接稳定大模型,是让AI稳定输出的前提。
801 17
|
2月前
|
存储 JSON 自然语言处理
大模型应用开发-LangChain框架基础
本文摘要: 文章系统介绍了大模型技术应用与开发的全流程,涵盖云端/本地模型部署、Prompt工程、LangChain框架及RAG项目实战。主要内容包括: 模型部署 阿里云百炼平台API接入与安全配置 Ollama本地模型部署方案 OpenAI兼容SDK的多平台调用方法 Prompt工程 Zero-shot/Few-shot提示技巧 金融文本分类/信息抽取实战案例 JSON数据结构处理与模板设计 LangChain框架 组件化架构:Models/Prompts/Memory/Vectorstores 链式调用
|
2月前
|
机器学习/深度学习 人工智能 缓存
Alibaba Cloud Linux 4 LTS 64位 Deb 版是什么系统镜像?兼容Debian和Ubuntu吗?
Alibaba Cloud Linux 4 LTS 64位Deb版是阿里云首个兼容Debian生态的LTS系统,深度适配Ubuntu 24.04,专为AI/深度学习优化。预装KeenTune智能调优框架、AI加速内核及kmod-fuse,支持百万IOPS与40GB/s缓存带宽,提供2025–2038年长期支持。(239字)

热门文章

最新文章