自定义注解 + 拦截器 + Redis 实现限流

前言

在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。

  • 缓存 :缓存的目的是提升系统访问速度和增大系统处理容量
  • 降级 :降级是当服务出现问题或者影响到核心流程时,需要暂时屏蔽掉,待高峰或者问题解决后再打开
  • 限流 :限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理

背景

最近项目生产环境中,我们暴露给第三方的几个接口,被不正常的调用过多,因此决定做一下限流。

常见的限流算法有漏桶算法和令牌桶算法。

  • 漏桶算法:漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。
  • 令牌桶算法:对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。如图所示,令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。

                             漏桶算法原理模型                                                   令牌桶算法原理模型

技术选型

显然令牌桶算法更加优于漏桶算法,由于考虑到我们的是微服务项目,为了以后的扩展考虑,需要单体和分布式均适用才行,所以最后我们决定使用 自定义注解+拦截器+Redis 实现,因为用户实际的访问次数都是存在redis容器里的,和应用的单体或分布式无关。

具体实现

首先,定义一个注解,用于标记需要限流的接口,以及设置流速:

@Documented
@Inherited
@Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLimit {
    //标识 指定sec时间段内的访问次数限制
    int limit() default 5;

    //标识 时间段
    int sec() default 5;
}

接着创建拦截器对需要限流的请求进行拦截:

这里我遇到两个问题:

@Slf4j
public class AccessLimitInterceptor implements HandlerInterceptor {
    @Resource
    private RedisTemplate<String, Integer> redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            if (!method.isAnnotationPresent(AccessLimit.class)) {
                return true;
            }
            AccessLimit accessLimit = method.getAnnotation(AccessLimit.class);
            if (accessLimit == null) {
                return true;
            }
            int limit = accessLimit.limit();
            int sec = accessLimit.sec();
            String key = request.getRequestURI();
            try {
                Integer maxLimit = redisTemplate.opsForValue().get(key);
                if (maxLimit == null) {
                    //set时一定要加过期时间
                    redisTemplate.opsForValue().set(key, 1, sec, TimeUnit.SECONDS);
                } else if (maxLimit < limit) {
                    //方案一:
                    redisTemplate.opsForValue().set(key, maxLimit + 1, Objects.requireNonNull(redisTemplate.getExpire(key)), TimeUnit.SECONDS);

                
                } else {
                    ResponseUtil.addResponse(response, "请求过于频繁,请稍后再试!");
                    return false;
                }
            } catch (NullPointerException e) {
                redisTemplate.opsForValue().set(key, 1, sec, TimeUnit.SECONDS);
            }
        }
        return true;

    }
}

接下来注册拦截器:

这里发现拦截器中注入RedisTemplate失败,解决方案:springboot 拦截器中无法注入 RedisTemplate

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public AccessLimitInterceptor getSessionInterceptor() {
        return new AccessLimitInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(getSessionInterceptor()).addPathPatterns("/accessLimit/*");
    }
}

测试结果

然后我们就可以试一试效果怎么样了:

测试controller:

@RestController
@RequestMapping("/accessLimit")
@Slf4j
public class AccessLimitController {

    @GetMapping("/test001")
    @AccessLimit(limit = 4,sec = 10)
    @ResponseBody
    public String test001(HttpServletRequest request, @RequestParam String name){

        return name + " hello world!!!";
    }
}

从测试结果来看,这套方案是可以的,期待它在线上的表现。 

 

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值