近期由于项目中调用第三方接口,第三方限制QPS为5,所以在我们系统中需要进行限制,在分布式环境下,首先想到了用redis,当然,redis也提供了现成的限流工具,但是并未用他的那个,而是自己设计了一个工具类
import org.example.function.ExcuteMethodParent;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.StaticScriptSource;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
public class RLimitUtils {
private static ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(8);
private String taskListKey ;
/**
* redis 工具类
*/
private RedisTemplate redisTemplate;
/**
* 窗口期数量
*/
private Integer allTaskNum;
/**
* 窗口期数量
*/
private Integer windowNum;
/**
* 窗口期单位
*/
private TimeUnit timeUnit;
/**
* 窗口期毫秒数
*/
private Long windowsStampNum;
/**
* 每个任务之间休眠的数量
*/
private Long sleepTimePerTask;
/**
* redis中的是否可执行状态
*/
private String taskExcuteFlagKey;
private AtomicBoolean activeFlag = new AtomicBoolean(false);
/**
* 执行结果数据
*/
public static Map<String,String> taskResultMap = new ConcurrentHashMap<>();
/**
* 方法队列
*/
public static Map<String, ExcuteMethodParent> functionMap = new ConcurrentHashMap<>();
/**
* @Param allTaskNum 窗口期内所有可执行任务数 5
* @Param windowNum 窗口期数值 例如 1 s
* @Param timeUnit 窗口期单位 s
* @Param redisTemplate redis的实体类
* @Param taskListKey redis 中队列名称
*/
public RLimitUtils(Integer allTaskNum, Integer windowNum, TimeUnit timeUnit, RedisTemplate redisTemplate,String taskListKey){
this.allTaskNum = allTaskNum;
this.windowNum = windowNum;
this.timeUnit = timeUnit;
this.redisTemplate = redisTemplate;
this.windowsStampNum = replaceWindowNumToWindowStampNum(windowNum,timeUnit);
this.sleepTimePerTask = windowsStampNum / allTaskNum;
this.taskListKey = taskListKey;
this.taskExcuteFlagKey = taskListKey+"_excuteFlag";
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(taskExcuteFlagKey, 1);
//System.out.println("设置标识符:"+aBoolean);
}
/**
* 计算频率数值
*/
private Long replaceWindowNumToWindowStampNum(Integer windowNum,TimeUnit timeUnit){
if (timeUnit.equals(TimeUnit.SECONDS)){
return windowNum.longValue() * 1000;
}
if (timeUnit.equals(TimeUnit.MINUTES)){
return windowNum.longValue() * 1000 * 60;
}
if (timeUnit.equals(TimeUnit.HOURS)){
return windowNum.longValue() * 1000 * 60 * 60;
}
if (timeUnit.equals(TimeUnit.DAYS)){
return windowNum.longValue() * 1000 * 60 * 24;
}
return windowNum.longValue();
}
/**
* 外部使用的入口
*/
public String addFunction(Object expressConfig,ExcuteMethodParent function){
String functionKey = Thread.currentThread().getName() +":"+ System.currentTimeMillis();
// System.out.println("放入了key:"+functionKey);
functionMap.put(functionKey,function);
redisTemplate.opsForList().rightPush(taskListKey, functionKey);
// 激活监控
if (!activeFlag.get()) {
synchronized (activeFlag) {
if (!activeFlag.get()) {
activeFlag.set(!activeFlag.get());
excuteFunction(expressConfig);
}
}
}
String result = "";
while (true){
if (taskResultMap.containsKey(functionKey)){
result = taskResultMap.get(functionKey);
taskResultMap.remove(functionKey);
break;
} else {
// 判断当前是否是非激活状态,非激活状态,则应该进行激活,防止存在某个突然的请求,刚好到达边界值,而不进行执行
if (!activeFlag.get()) {
synchronized (activeFlag) {
if (!activeFlag.get()) {
activeFlag.set(!activeFlag.get());
excuteFunction(expressConfig);
}
}
}
}
}
return result;
}
/**
* 执行限流方法
*/
private void excuteFunction(Object expressConfig) {
// 从队列中获取数据
Object functionKey = redisTemplate.opsForList().leftPop(taskListKey);
while (functionKey!= null ){
if (functionMap.containsKey(functionKey.toString())) {
// 从redis中获取当前执行状态
//调用lua脚本并执行
List<String> keys = new ArrayList<>();
keys.add(this.taskExcuteFlagKey);
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setResultType(Long.class);//返回类型是Long
//lua文件存放在resources目录下的redis文件夹内
redisScript.setScriptSource(new StaticScriptSource(getLuaText()));
Object execute = redisTemplate.execute(redisScript, keys);
// 监控是否可以执行
while ("0".equals(execute.toString())) {
execute = redisTemplate.execute(redisScript, keys);
}
try {
// 拿到执行的权力,进行休眠
Thread.sleep(sleepTimePerTask);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 若为可以执行的状态,执行方法调用执行方法交给新的线程池
scheduledExecutorService.execute(new MyExcute(functionKey.toString(),expressConfig,redisTemplate,taskExcuteFlagKey));
} else {
// 因为是从redis中获取的key,所以可能是其他集群的数据,因此还回去,继续下一个处理
redisTemplate.opsForList().rightPush(taskListKey,functionKey.toString());
}
functionKey = redisTemplate.opsForList().leftPop(taskListKey);
}
activeFlag.set(!activeFlag.get());
}
/**
* 组合lua脚本,执行redis操作
* lua脚本执行redis操作时候,具有原子性,整体操作结束后,才会允许其他线程访问
* @return
*/
private String getLuaText(){
// 定义可以执行标识变量
String text = "local excuteFlag = KEYS[1];\n" +
// 获取可执行变量的值
"local flag = redis.call('get',excuteFlag);\n" +
// 若执行状态变量为0,则返回0,不可执行
"if tonumber(flag)==0 then \n" +
" return 0;\n" +
// 否则,执行变量值减 1 ,因为当前线程拿到了可以执行的标识符,并且要将执行标识符改为不可执行状态,因此 减 1 操作
"else \n"+
" redis.call('decr',excuteFlag); \n"+
"end \n" +
// 表示当前线程可以执行,返回 1
"return 1";
return text;
}
}
class MyExcute implements Runnable{
/**
* 方法标识符的key
*/
private String functionKey;
/**
* 方法执行依赖对象,因为我的执行方法中,需要一个对象调用,所以才传递进来
*/
private Object expressConfig;
/**
* redis的操作对象
*/
private RedisTemplate redisTemplate;
/**
* redis中的是否可执行状态
*/
private String taskExcuteFlagKey;
public MyExcute(String functionKey,Object expressConfig,RedisTemplate redisTemplate,String taskExcuteFlagKey){
this.functionKey = functionKey;
this.expressConfig = expressConfig;
this.redisTemplate = redisTemplate;
this.taskExcuteFlagKey = taskExcuteFlagKey;
}
@Override
public void run() {
ExcuteMethodParent sendMethodFather = RLimitUtils.functionMap.get(functionKey);
RLimitUtils.functionMap.remove(functionKey);
// 开始执行方法
String result = excute(expressConfig,sendMethodFather);
RLimitUtils.taskResultMap.put(functionKey,result);
}
/**
* 执行方法开始
*/
private String excute(Object expressConfig,ExcuteMethodParent father){
// 修改状态为可以下一个执行
redisTemplate.opsForValue().increment(taskExcuteFlagKey);
Object excute = father.excute(expressConfig);
return excute.toString();
}
}
实际测试利用线程模拟并发数量,搭建nginx模拟多个实例
import org.example.util.RLimitUtils;
import org.example.util.RedisTokenFactory;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.TimeUnit;
public class MyThread implements Runnable{
private RLimitUtils tokenUtils;
public MyThread(Integer allTaskNum,Integer windowNum, TimeUnit timeUnit, RedisTemplate redisTemplate,String taskKey){
this.tokenUtils = RedisTokenFactory.getRLimitUtils(allTaskNum,windowNum,timeUnit,redisTemplate,taskKey);
}
@Override
public void run() {
// 此处可以定义一个对象,传递到执行方法内部
String config = "ddddddd";
String result = tokenUtils.addFunction(config, object -> "执行时间:"+System.currentTimeMillis());
// System.out.println(result);
}
}
@GetMapping("checkRedisLimit")
public String checkRedisLimit(){
// 线程池模拟并发
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(8);
for (int i = 0; i < 100; i++) {
scheduledExecutorService.execute(new MyThread(5,1, TimeUnit.SECONDS, redisTemplate, "redisTemplate2"));
}
return "asdfasdf";
}
点击下载源码