今天分享一个之前做的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