场景:增加API接口请求的限流策略,针对每一个用户的每种类型的API请求做限流。
比如:同一用户,每秒钟只允许20次签约请求。当每秒请求超过20次时,会提示"客户请求签约接口次数超限"。
1、作为API调用方,就要对并发进行控制,以防出现无效请求。最常用的并发限流方案是借助redis/jedis。为了保证原子性,这里,我使用Redis+LUA脚本的方式来控制。
2、对于提供方来,当请求量超出设定的限流阈值,则直接返回错误码/错误提示,并终止对请求的处理。
而对于调用方来说呢,我们要做的是:当并发请求超出了限定阈值时,要延迟请求,而不是直接丢弃。
@Slf4j
@Component
public class RedisLimiter {
@Autowired
private RedisTemplate redisTemplate;
/**
* 达到限流时则等待,直到新的间隔。
*/
public void limitWait(String key, int limitCount, int limitSecond) {
boolean ok;//放行标志
do {
ok = limit(key, limitCount, limitSecond);//调用limit返回是否达到限流
if (!ok) {
Long ttl = redisTemplate.getExpire(key, TimeUnit.MILLISECONDS);
if (null != ttl && ttl > 0) {
Thread.sleep(ttl);//睡眠到缓存过期
}
}
} while (!ok);
}
}
/**
限流方法 true-放行;false-限流
key:缓存的key,limitCount:key的最大值,limitSecond:key的过期时间
*/
public boolean limit(String key, int limitCount, int limitSecond) {
List<String> keys = Collections.singletonList(key);
String luaScript = buildLuaScript();
RedisScript<Integer> redisScript = new DefaultRedisScript<Integer>(luaScript, Integer.class);
Integer count = redisTemplate.execute(redisScript, keys, limitCount, limitSecond);
if (count != null && count.intValue() <= limitCount) {//判断是否超过数量
return true;//放行
} else {
return false;//限流
}
}
/**
* 编写redis Lua 限流脚本
* 流程:如果超过指定数量就返回,没有超过就计数+1,返回当前的值。第一次进来的时候还要给缓存设置过期时间。
* c = redis.call('get',KEYS[1]) //从缓存中获取KEY
* if c and tonumber(c) > tonumber(ARGV[1]) then return c end //超过限定的最大值就直接返回
* c = redis.call('incr',KEYS[1]) //没有超过计数器+1,返回当前c的值
* if tonumber(c) == 1 then redis.call('expire',KEYS[1],ARGV[2]) end
* return c;
* 注:Incr命令将key中储存的数字值增一,返回当前key的数值。
* 注:tonumber方法是Lua语法里面的函数,把参数转成10进制的数字
* 注:KEYS[1] 表示key,ARGV[1]表示:value,ARGV[2]:超时时间
*/
public String buildLuaScript() {
StringBuilder lua = new StringBuilder();
lua.append("local c");
lua.append("\nc = redis.call('get',KEYS[1])");
lua.append("\nif c and tonumber(c) > tonumber(ARGV[1]) then");//实际调用次数超过阈值,则直接返回
lua.append("\nreturn tostring(c);");
lua.append("\nend");
lua.append("\nc = redis.call('incr',KEYS[1])");// 执行计算器自加
lua.append("\nif tonumber(c) == 1 then");
lua.append("\nredis.call('expire',KEYS[1],ARGV[2])");//从第一次调用开始限流,设置对应键值的过期
lua.append("\nend");
lua.append("\nreturn tostring(c);");
return lua.toString();
}
多个请求,通过频繁的调用下面的方法来控制速率
@Resource
RedisLimiter redisLimiter
redisLimiter.limitWait(key, 3, 1)
key表示redis的key,3表示最大请求次数,1表示过期时间1秒,1秒时间内最大请求次数是3次。
第一次调用时,缓存数据就是1,再低调用缓存计数就是2。。。第四次调用因为超过了3,所以需要等待缓存过期才会继续往下执行。
缺点:
这个限流有个弊端就是。
比如我们设置1秒只处理三个请求,加入0.1秒来了一个请求,0.9来了2个请求。
1.1秒来了2个请求,1.9秒来了1个请求,虽然第一秒处理了3个请求,第二个一秒处理了3个请求,但是0.9秒到1.1秒处理了4个请求,这就会有问题了。
原文链接:https://www.cnblogs.com/buguge/p/13477151.html