SpringBoot 基于Redis实现分布式限流

需求:XX接口访问量太大,需要在一定时间内不让那么多的请求进来

实现原理:

用Redis作为限流组件的核心的原理,将接口名称当Key,一段时间内访问次数为value,同时设置该Key过期时间。

限制 XX接口在TT时间内访问次数
第一次访问 操作redis,key:接口名称 value:次数  expire设置过期时间 TT
第二次访问 操作redis,  value + 1,如果过期则按照第一次处理

通过lua脚本 来保证原子性

推荐使用Lua脚本。

  1. 减少网络开销: 不使用 Lua 的代码需要向 Redis 发送多次请求, 而脚本只需一次即可, 减少网络传输;
  2. 原子操作: Redis 将整个脚本作为一个原子执行, 无需担心并发, 也就无需事务;
  3. 复用: 脚本会永久保存 Redis 中, 其他客户端可继续使用.

Redis添加了对Lua的支持,能够很好的满足原子性、事务性的支持,让我们免去了很多的异常逻辑处理。

源码地址:https://gitee.com/love_yu_0698/ratelimiter-demo.git

 

实现方式:添加注解,通过AOP切面完成接口限流

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {

    /**
     * 限流key
     * @return
     */
    String key() default "rate:limiter";
    /**
     * 单位时间限制通过请求数
     * @return
     */
    long limit() default 3L;

    /**
     * 过期时间,单位秒
     * @return
     */
    long expire() default 30L;

    /**
     * 返回值
     * @return
     */
    String message() default "false";
}

AOP 处理 限流次数

init() 在应用启动时会初始化DefaultRedisScript,并加载Lua脚本,方便进行调用。
Lua脚本放置在classpath下,通过ClassPathResource进行加载。
获取 @RateLimiter 注解配置的属性:key、limit、expire,并通过 redisTemplate.execute(RedisScript script, List keys, Object... args) 方法传递给Lua脚本进行限流相关操作 脚本返回状态为0则为触发限流 返回message 默认为 false,1表示正常请求。 

@Aspect
@Component
@Slf4j
public class RateLimterAspect {

    @Resource
    private RedisTemplate redisTemplate;

    private DefaultRedisScript<Long> getRedisScript;

    @PostConstruct
    public void init() {
        getRedisScript = new DefaultRedisScript<>();
        getRedisScript.setResultType(Long.class);
        getRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rateLimter.lua")));
        log.info("RateLimterAspect[分布式限流处理器]脚本加载完成");
    }

    @Pointcut("@annotation(com.crayon.ratelimiterdemo.annotation.RateLimiter)")
    public void rateLimiter() {}

    @Around("@annotation(rateLimiter)")
    public Object around(ProceedingJoinPoint proceedingJoinPoint, RateLimiter rateLimiter) throws Throwable {
        if (log.isDebugEnabled()){
            log.debug("RateLimterAspect[分布式限流处理器]开始执行限流操作");
        }
        Signature signature = proceedingJoinPoint.getSignature();
        if (!(signature instanceof MethodSignature)) {
            throw new IllegalArgumentException("the Annotation @RateLimter must used on method!");
        }

        // 限流模块key
        String limitKey = rateLimiter.key();
        Preconditions.checkNotNull(limitKey);
        // 限流阈值
        long limitTimes = rateLimiter.limit();
        // 限流超时时间
        long expireTime = rateLimiter.expire();
        if (log.isDebugEnabled()){
            log.debug("RateLimterAspect[分布式限流处理器]参数值为-limitTimes={},limitTimeout={}", limitTimes, expireTime);
        }

        // 限流提示语
        String message = rateLimiter.message();
        if (message == null || "".equalsIgnoreCase(message.replace(" ",""))) {
            message = "false";
        }

        //执行Lua脚本
        List<String> keyList = new ArrayList<>();

        // 设置key值为注解中的值
        keyList.add(limitKey);

        //调用脚本并执行
        @SuppressWarnings("unchecked")
        Long result = (Long) redisTemplate.execute(getRedisScript, keyList, expireTime, limitTimes);
        if (result != null && result == 0) {
            String msg = "由于超过单位时间=" + expireTime + "-允许的请求次数=" + limitTimes + "[触发限流]";
            log.debug(msg);
            return message;
        }
        if (log.isDebugEnabled()){
            log.debug("RateLimterAspect[分布式限流处理器]限流执行结果-result={},请求[正常]响应", result);
        }
        return proceedingJoinPoint.proceed();
    }
}

Lua脚本

限流操作的核心,通过执行一个Lua脚本进行限流的操作

  1. 首先脚本获取Java代码中传递而来的要限流的模块的key,不同的模块key值一定不能相同,否则会覆盖!
  2. redis.call('incr', key1)对传入的key做incr操作,如果key首次生成,设置超时时间ARGV[1];(初始值为1)
  3. ttl是为防止某些key在未设置超时时间并长时间已经存在的情况下做的保护的判断;
  4. 每次请求都会做+1操作,当限流的值val大于我们注解的阈值,则返回0表示已经超过请求限制,触发限流。否则为正常请求。
    --获取KEY
    local key1 = KEYS[1]

    local val = redis.call('incr', key1)
    local ttl = redis.call('ttl', key1)

    --获取ARGV内的参数并打印
    local expire = ARGV[1]
    local times = ARGV[2]

    redis.log(redis.LOG_DEBUG,tostring(times))
    redis.log(redis.LOG_DEBUG,tostring(expire))

    redis.log(redis.LOG_NOTICE, "incr "..key1.." "..val);
    if val == 1 then
        redis.call('expire', key1, tonumber(expire))
    else
        if ttl == -1 then
            redis.call('expire', key1, tonumber(expire))
        end
    end

    if val > tonumber(times) then
        return 0
    end

    return 1

启动项目 测试 有效

PS:如果多个模块中的接口都要限流的话 则需要整理成一个 starter 避免写重复代码

自定义starter 源码地址:https://gitee.com/love_yu_0698/ratelimter-spring-boot-starter.git

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值