redis限流实例 附github地址

本文介绍了基于Redis实现的限流方案,提供了一个Redis限流Maven工程的GitHub地址,以及操作步骤。读者可以克隆项目并自行尝试。限流主要涉及RedisCurrentLimit.java、RedisCurrentLimitFactory.java、自定义注解AspectLimit和ControllerLimit,以及WebIntercept.java拦截器。限流判断通过lua脚本实现,确保原子性操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

今天分享一个之前做的redis限流包,最近加班比较忙,先把git地址传上来,后续再详细介绍逻辑,有兴趣的小伙伴可以clone下来自己试试。

 

redis限流maven工程github地址:

https://github.com/oldwang666666/redis-limit-module

 

redis限流包引用示例代码github地址:

https://github.com/oldwang666666/springboot-redis-limit-demo

 

操作步骤

1、clone redis限流maven工程,执行install打出jar包

2、clone redis限流包引用示例工程,修改redis地址

3、启动 SpringbootRedisLimitDemoApplication

4、调用http://127.0.0.1:8080/test/getLimit  或  http://127.0.0.1:8080/test/getControllerLimit 查看demo的限流过程。

1)、正常执行如下图

2)、多次操作限流返回如下图、目前限流以秒为单位

 

 redis限流maven包redis-limit-module结构

1、我们先看下限流方法类实现 RedisCurrentLimit.java,目前我只自测了单机模式,集群模式等有时间再进行测试和调整

package com.wang.limit.fuse;

import com.wang.limit.intercept.Message;
import com.wang.limit.util.ScriptUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.connection.RedisClusterConnection;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPool;

import java.io.IOException;
import java.util.Collections;

/**
 * redis限流实现类
 * Description:
 * Created by longzhang.wang
 * Date: 2019-09-29
 */
public class RedisCurrentLimit {

    private static final Logger logger= LoggerFactory.getLogger(RedisCurrentLimit.class);

    /**
     * 默认为单机模式
     */
    private boolean isCluster = false;
    /**
     * 用于单机模式进行redis连接池管理
     */
    private JedisPool jedisPool = null;
    /**
     * 用于单机模式进行redis连接池管理
     */
    private JedisConnectionFactory jedisConnectionFactory = null;
    /**
     * 获取限流lua脚本
     */
    private final static String script = ScriptUtil.getLuaScript("currentLimit.lua");
    /**
     * 限流标志,如果值为0则被限流
     */
    private final static int LIMIT_FALG = 0;
    /**
     * 默认qps最大值
     */
    private int limitNum = 500;

    private RedisCurrentLimit() {}

    protected RedisCurrentLimit(boolean isCluster, int limitNum, JedisPool jedisPool, JedisConnectionFactory jedisConnectionFactory) {
        this.isCluster = isCluster;
        this.limitNum = limitNum;
        this.jedisPool = jedisPool;
        this.jedisConnectionFactory = jedisConnectionFactory;
    }

    /**
     * 限流方法
     * true 限流, false 不限流
     * @param methodName 方法名
     * @return
     */
    public boolean currentLimitHandle(String methodName) {
        return currentLimitHandle(limitNum, methodName);
    }

    /**
     * 限流方法 - 带限制次数
     * true 限流, false 不限流
     * @param limitNum 每秒限制次数
     * @param methodName 方法名
     * @return
     */
    public boolean currentLimitHandle(Integer limitNum, String methodName) {
        //计算失败默认通过
        Long result = -1L;
        //以秒为时间单位,此处的key实际使用需要用方法名 + 时间 用于做方法的唯一识别
        String key = methodName + String.valueOf(System.currentTimeMillis() / 1000);
        if (!isCluster){
            //已测试,可用
            result = this.standAloneCurrentLimitHandle(key, limitNum);
        }else {
            //未测试
            result = this.clusterCurrentLimitHandle(key, limitNum);
        }

        return LIMIT_FALG == result.intValue() ? true : false;
    }

    /**
     * 单机版本限流计算
     * @param key
     * @param limitNum
     * @return
     */
    private Long standAloneCurrentLimitHandle(String key ,int limitNum) {
        Jedis jedis = jedisPool.getResource();
        try {
            //调用lua脚本
            Object result = jedis.eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(limitNum)));
            return (Long) result;
        } catch (Exception e){
            logger.error(Message.get("message.limit.verification.fail"), e);
        }finally {
            jedis.close();
        }
        return -1L;
    }

    /**
     * 集群版本限流计算
     * @param key
     * @param limitNum
     * @return
     */
    private Long clusterCurrentLimitHandle(String key ,int limitNum) {

        RedisClusterConnection redisClusterConnection = jedisConnectionFactory.getClusterConnection();
        Object conn = redisClusterConnection.getNativeConnection();
        JedisCluster jedisCluster = (JedisCluster)conn;
        try {
            Object result = jedisCluster.eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(limitNum)));
            return (Long) result;
        } catch (Exception e){
            logger.error(Message.get("message.limit.verification.fail"), e);
        }finally {
            try {
                jedisCluster.close();
            } catch (IOException e) {
                logger.error(Message.get("message.jedisCluster.close.fail"), e);
            }
        }
        return -1L;
    }
}

2、redis限流实例工厂  RedisCurrentLimitFactory.java

package com.wang.limit.fuse;

import com.wang.limit.intercept.Message;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import redis.clients.jedis.JedisPool;

/**
 * redis限流实例工厂
 * Description:
 * Created by longzhang.wang
 * Date: 2019-09-29
 */
public class RedisCurrentLimitFactory {

    protected final static int LIMIT_MAX = 50000000;

    /**
     * 创建单机限流对象
     * @param limitNum   默认限流大小
     * @param jedisPool  jedis连接池
     * @return
     * @throws Exception
     */
    public RedisCurrentLimit standAloneInstance(int limitNum, JedisPool jedisPool) throws Exception {

        this.checkMaximum(limitNum);
        return this.buildRedisCurrentLimit(false, limitNum, jedisPool, null);
    }

    /**
     * 创建集群限流对象
     * @param limitNum  默认限流大小
     * @param jedisConnectionFactory  jedis集群工厂
     * @return
     * @throws Exception
     */
    public RedisCurrentLimit clusterInstance(int limitNum, JedisConnectionFactory jedisConnectionFactory) throws Exception {

        this.checkMaximum(limitNum);
        return this.buildRedisCurrentLimit(true, limitNum, null, jedisConnectionFactory);
    }

    private RedisCurrentLimit buildRedisCurrentLimit(boolean isCluster, int limitNum, JedisPool jedisPool
            , JedisConnectionFactory jedisConnectionFactory) {

        return new RedisCurrentLimit(isCluster, limitNum, jedisPool, jedisConnectionFactory);
    }

    /**
     * 校验初始化时,输入的限流数值是否超过最大值
     * @param limitNum
     * @throws Exception
     */
    private void checkMaximum(int limitNum) throws Exception {
        if(limitNum > LIMIT_MAX) {
            throw new Exception(Message.get("message.limitConfig.over.maximum"));
        }
    }
}

3、针对限流的方法,我们自定义自己的注解AspectLimit和ControllerLimit做限流实验,小伙伴也可以定义其他注解模拟不同的应用场景。

1、AspectLimit用于做切面,具体的限流参数在初始化完成

2、ControllerLimit可以在注解重配置简单的限流参数,灵活性更强

package com.wang.limit.annotation;

import java.lang.annotation.*;

/**
 * 控制层拦截注解
 * Description:
 * Created by longzhang.wang
 * Date: 2019-09-29
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ControllerLimit {

	/**
     * 错误码500
     * @return
     * code
     */
    int errorCode() default 500;

    /**
     * 错误描述
     * @return
     */
    String errorMessage() default "请求次数超过限制,请稍后重试";

    /**
     * 限流最大值
     * @return
     */
    int limit() default 500;
}

3、有了注解,我们自定义一个拦截器 WebIntercept.java,用于拦截需要限流的类,校验是否存在ControllerLimit注解,存在则进行限流判断

package com.wang.limit.intercept;

import com.wang.limit.annotation.ControllerLimit;
import com.wang.limit.fuse.RedisCurrentLimit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * redis限流拦截器
 * Description:
 * Created by longzhang.wang
 * Date: 2019-09-29
 */
@Component
public class WebIntercept extends WebMvcConfigurerAdapter {

    private static Logger logger = LoggerFactory.getLogger(WebIntercept.class);

    @Autowired
    private RedisCurrentLimit redisCurrentLimit;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        /**
         * addPathPatterns添加符合规则的路径进入CheckControllerLimitInterceptor方法
         * excludePathPatterns排除符合规则的路径进入CheckControllerLimitInterceptor方法
         */
        registry.addInterceptor(new CheckControllerLimitInterceptor())
                .addPathPatterns("/**");
    }

    private class CheckControllerLimitInterceptor extends HandlerInterceptorAdapter {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                                 Object handler) throws Exception {

            if (redisCurrentLimit == null) {
                throw new NullPointerException(Message.get("message.redisCurentLimit.isnull"));
            }

            if (handler instanceof HandlerMethod) {
                HandlerMethod method = (HandlerMethod) handler;

                ControllerLimit annotation = method.getMethodAnnotation(ControllerLimit.class);
                if (annotation == null) {
                    //是否存在ControllerLimit注解
                    return true;
                }
                //实际使用去除日志打印
                logger.info(Message.get("message.redisCurentLimit.methodName")
                        + method.getBeanType().getName() + method.getMethod().getName());

                boolean limit = redisCurrentLimit.currentLimitHandle(annotation.limit(),
                        method.getBeanType().getName() + method.getMethod().getName());
                if (limit) {
                    logger.warn(annotation.errorMessage());
                    response.setCharacterEncoding("utf-8");
                    response.sendError(annotation.errorCode(), annotation.errorMessage());
                    return false;
                }
            }

            return true;
        }
    }
}

4、currentLimit.lua脚本,用于进行限流判断,基于lua脚本可以达到原子性操作,如果是在java做临界值判断,不具备原子性操作,在并发度高时容易出现问题,同时我们在lua脚本中加上了防止不正规操作,出现key有效期为-1的判断。

-- lua 下标从 1 开始
-- 限流 key
local key = KEYS[1]
-- 限流大小
local limit = tonumber(ARGV[1])

-- 获取当前流量大小
local currentLimit = tonumber(redis.call('get', key) or "0")

if (limit + 10000 < currentLimit) then

    -- 达到限流大小超过限流大小10000,可能是有效期出了问题
    local ttlTime = redis.call('ttl',key)
    if (ttlTime == -1) then
        -- 如果发现有限期变成-1 设置有效期设置为2秒
        redis.call("EXPIRE", key, 2)
    end
    return 0;
elseif (currentLimit + 1 > limit) then

    -- 达到限流大小 返回
    return 0;
elseif (currentLimit == 0) then
    -- 没有达到阈值 value + 1 有效期设置为2秒
    redis.call("INCRBY", key, 1)
    redis.call("EXPIRE", key, 2)
    return currentLimit + 1
else
    -- 没有达到阈值 value + 1
    redis.call("INCRBY", key, 1)
    return currentLimit + 1
end

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值