sunny123456

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
  1796 随笔 :: 22 文章 :: 24 评论 :: 226万 阅读
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

使用Redis调用Lua脚本的方式对SpringBoot接口进行限流

前言

在日常开发中,限流功能时常被使用,用于对某些接口进行限流熔断,譬如限制单位时间内接口访问次数;或者按照某种规则进行限流,如限制ip的单位时间访问次数等,在SpringBoot中,常用的限流方式有:Lua脚本限流、阿里巴巴开源的限流器Sentinel等等,本篇主要介绍使用Lua脚本进行接口限流。


一、步骤

1、自定义限流注解 Limit.java,用于标注在需要限流的接口上

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Limit {
    /**
     * 资源key,用于自定义限流标识时使用,当limitType为key或key+ip时,key不能为空
     */
    String key() default "";
    /**
     * redis中key的前缀
     */
    String prefix() default "limit_";
    /**
     * 时间,以毫秒为限流单位
     */
    int period();
    /**
     * 限制访问次数
     */
    int count();
    /**
     * 类型
     */
    LimitType limitType();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

2、编写限流类型枚举类 LimitType.java

public enum LimitType {
    /**
     * 自定义key限流,即限制所有用户在单位时间内对同一限流标识标识的接口的访问次数
     */
    KEY,
    /**
     * 根据请求者ip限流,即限制同一ip在单位时间内对限流类型为IP类型接口的访问次数
     */
    IP,
    /**
     * key+ip限流,即自定义一个限流标识,限制同一ip在单位时间内对同一限流标识标识的接口的访问次数
     */
    KEYIP,
    /**
     * 根据方法名限流,即限制所有用户在单位时间内对同一接口的访问次数
     */
    METHOD,
    /**
     * 根据方法名+ip限流,即限制同一ip在单位时间内对同一接口的访问次数
     */
    METHODIP;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

3、编写限流具体实现类 LimitAspect.java,通过AOP方式进行限流

@Aspect
@Order(1)
@Component
public class LimitAspect {
    private final RedisTemplate<Object, Object> redisTemplate;
    public LimitAspect(RedisTemplate<Object, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    @Pointcut("@annotation(com.tsing.bootadmin.common.annotations.Limit)")//这里是限流注解所在的路径名
    public void pointcut() {
    }
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Limit limitAnnotation = method.getAnnotation(Limit.class);
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String key;
        switch (limitAnnotation.limitType()) {
            case KEY:
                key = limitAnnotation.key();
                break;
            case IP:
                key = ServletUtil.getClientIP(request);
                break;
            case KEYIP:
                key = limitAnnotation.key() + "_" + ServletUtil.getClientIP(request);
                break;
            case METHOD:
                key = method.getName();
                break;
            default:
                key = method.getName() + "_" + ServletUtil.getClientIP(request);
        }
        List<Object> keys = Collections.singletonList(limitAnnotation.prefix() + key);
        RedisScript<Boolean> redisScript = new DefaultRedisScript<>(buildLuaScript(), Boolean.class);
        if (redisTemplate.execute(redisScript, keys, limitAnnotation.count(), limitAnnotation.period())) {
            return joinPoint.proceed();
        } else {
            throw new BusinessException(CommonException.Proxy.SERVER_IS_TOO_BUSY);//此处为达到限流次数的逻辑处理,我这里是直接返回异常给前端,可根据实际情况做调整
        }
    }
    /**
     * lua限流脚本
     */
    private String buildLuaScript() {
        return "local num = redis.call('incr',KEYS[1])\n" +
                "if tonumber(num) == 1 then\n" +
                "redis.call('pexpire',KEYS[1],ARGV[2])\n" +
                "return true\n" +
                "elseif tonumber(num) > tonumber(ARGV[1]) then\n" +
                "return false\n" +
                "else\n" +
                "return true\n" +
                "end";
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63

4、Controller限流测试,我以登录接口为例,实现每个ip在一秒内只能访问一次登录接口

@PostMapping("/login")
@ApiOperation(value = "用户登录")
@Limit(period = 1000, count = 1, limitType = LimitType.METHODIP)
public ResultData<String> login(@Validated @RequestBody UserLoginReqVo reqVo, HttpServletRequest request) {
	return ResultUtil.success(userService.login(reqVo, request));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

5、Swagger限流测试

正常访问:
在这里插入图片描述
快速连续点击两次:
在这里插入图片描述


总结

如果这篇博客对你有帮助的话,记得给我点个赞,你的鼓励是对我最大的支持!谢谢。◕‿◕。

原文链接:https://blog.csdn.net/qq_42375133/article/details/122935969
posted on   sunny123456  阅读(93)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)
历史上的今天:
2022-09-02 <dependencyManagement>正确使用方法 多个子项目都引用同一样依赖,则可以避免在每个使用的子项目里都声明一个版本号。当想升级或切换到另一个版本时,只需要在顶层父容器里更新,而不需要逐个修改子项目
2022-09-02 vue.config.js的proxy为什么不起作用
2022-09-02 vue.config.js 的完整配置(超详细) 个性化 设置ip和端口
点击右上角即可分享
微信分享提示