https://www.cnblogs.com/xrq730/p/11025029.html
ratelimiter
特别注意RateLimiter是单机的,也就是说它无法跨JVM使用,设置的1000QPS,那也在单机中保证平均1000QPS的流量
集群流控最常见的方法是使用强大的Redis
redis限流:
setnx:setnx指令,如果成功则返回1,失败则返回0
一个常用的问题就是如果一个服务setnx成功了,但是在解锁的时候如果发生了宕机或者一些特殊因素,导致无法解锁,那么其他服务将陷入死锁的状态。所以,我们在用 setnx 的同时想着去用 expire 指令对锁进行一个过期操作:
setnx test ukey
expire test 10
从指令可以看出 setnx 和 expire 指令是分开的,如果在这中间的空隙过程中如果有特殊因素导致指令无法继续,也会导致死锁的产生。
Redis 2.8 版本中作者加入了 set 指令的扩展参数,使得 setnx 和 expire 指令可以一起执行,彻底解决了分布式锁的乱象(https://blog.csdn.net/lmx125254/article/details/89604638)
弊端:
弊端是很多的,比如当统计1-10秒的时候,无法统计2-11秒之内,如果需要统计N秒内的M个请求,那么我们的Redis中需要保持N个key等等问题
zset:
用Redis的list数据结构可以轻而易举的实现该功能
我们可以将请求打造成一个zset数组,当每一次请求进来的时候,value保持唯一,可以用UUID生成,而score可以用当前时间戳表示,因为score我们可以用来计算当前时间戳之内有多少的请求数量。而zset数据结构也提供了range方法让我们可以很轻易的获取到2个时间戳内有多少请求(ZRANGEBYSCORE zset (1 5
返回所有符合条件 1 < score <= 5 的成员)
public Response limitFlow(){
Long currentTime = new Date().getTime();
System.out.println(currentTime);
if(redisTemplate.hasKey("limit")) {
Integer count = redisTemplate.opsForZSet().rangeByScore("limit", currentTime - intervalTime, currentTime).size(); // intervalTime是限流的时间
System.out.println(count);
if (count != null && count > 5) {
return Response.ok("每分钟最多只能访问5次");
}
}
redisTemplate.opsForZSet().add("limit",UUID.randomUUID().toString(),currentTime);
return Response.ok("访问成功");
}
缺点:
上述代码可以做到滑动窗口的效果,并且能保证每N秒内至多M个请求,缺点就是zset的数据结构会越来越大。实现方式相对也是比较简单的。
基于Redis的令牌桶算法:
提到限流就不得不提到令牌桶算法了。令牌桶算法又称之为水桶算法,具体可以参照度娘的解释 令牌桶算法
令牌桶算法提及到输入速率和输出速率,当输出速率大于输入速率,那么就是超出流量限制了。
也就是说我们每访问一次请求的时候,可以从Redis中获取一个令牌,如果拿到令牌了,那就说明没超出限制,而如果拿不到,则结果相反。
依靠上述的思想,我们可以结合Redis的List数据结构很轻易的做到这样的代码
依靠List的leftPop来获取令牌
// 输出令牌
public Response limitFlow2(Long id){
Object result = redisTemplate.opsForList().leftPop("limit_list"); //移除集合中的左边第一个元素。 result 为移除的元素
if(result == null){ /表示没有元素
return Response.ok("当前令牌桶中无令牌");
}
//获取到了令牌
return Response.ok(articleDescription2);
}
//添加令牌
再依靠Java的定时任务,定时往List中rightPush令牌,当然令牌也需要唯一性,所以我这里还是用UUID进行了生成
// 10S的速率往令牌桶中添加UUID,只为保证唯一性
@Scheduled(fixedDelay = 10_000,initialDelay = 0)
public void setIntervalTimeTask(){
redisTemplate.opsForList().rightPush("limit_list",UUID.randomUUID().toString()); //向集合最右边添加元素。
}
https://www.cnblogs.com/shuangyueliao/p/11595198.html
redis加锁分类:incr,setnx,set
incr加锁思路:
key不存在,会被初始化为1,然后再支持incr操作进行加1,如果其他用户在执行incr操作的时候加1如果返回的值大于1,表示这个锁正在被使用中。
setnx加锁思路:
如果key不存在,则将key设置value,返回成功,如果key存在 setnx不做任何操作,返回失败
set加锁思路:
上面两种都需要加上有效期防止死锁操作,会导致不是原子性操作,这样的话,在设置expire的过程中,如果出现特殊原因导致指令无法继续执行,也会导致死锁。
jedisCluster.set(key, value, "NX", "EX", expireSeconds); // SET IF NOT EXIST,而且还是原子的 ex表示秒
将setnx和过期时间一起执行,set key value ex 10 nx 解决了原子性问题。
问题:
1:加锁失败怎么办?中断请求还是循环请求?
这个要看业务,有的循环,有的中断
2:循环请求如果有一个获取了锁,其它的在去获取锁的时候,是不是容易发生抢锁的可能?
循环请求的时候,加入睡眠功能,
3:锁提前过期了,客户端a还没有执行完,然后客户端b获取了锁,这时候a执行完了,会不会删锁的时候把b的锁删掉了
每次在删除key的时候判断下存入的key里的value和自己存的是否一样