
public class FunnelRateLimiter {
static class Funnel{
int capacity; //漏斗容量
float leakingRate; //漏嘴流水速率
int leftQuota; //漏斗剩余空间
long leakingTs; //上一次漏水时间
public Funnel(int capacity, float leakingRate) {
this.capacity = capacity;
this.leakingRate = leakingRate;
this.leftQuota = capacity;
this.leakingTs = System.currentTimeMillis();
}
void makeSpace(){
long nowTs = System.currentTimeMillis();
long deltaTs = nowTs - leakingTs;
int deltaQuota = (int)(deltaTs * leakingRate);
//间隔时间太长,整数数字过大溢出
if(deltaQuota < 0){
this.leftQuota = capacity;
this.leakingTs = nowTs;
return;
}
//腾出空间太小,最小单位是1
if(deltaQuota < 1){
return;
}
this.leftQuota += deltaQuota;
this.leakingTs = nowTs;
if(this.leftQuota > this.capacity){
this.leftQuota = this.capacity;
}
}
boolean watering(int quota){
makeSpace();
if(this.leftQuota >= quota){
this.leftQuota -= quota;
return true;
}else{
return false;
}
}
}
private Map<String,Funnel> funnels = new HashMap<>();
public boolean isActionAllowed(String userId,String actionKey,int capacity,float leakingRate,int quota){
String key = String.format("%s:%s", userId, actionKey);
Funnel funnel = funnels.get(key);
if(funnel == null){
funnel = new Funnel(capacity,leakingRate);
funnels.put(key,funnel);
}
return funnel.watering(quota);
}
}
例如,限制用户一分钟只允许连续发帖10次,写一个循环来模拟一直向系统发送请求,漏斗容量是10,说明最开始用户可以连续发帖10次,然后再次发帖的时候,就会判断漏斗中是否有剩余空间,如果漏斗已经满了,返回false,设定漏嘴漏水速率为10f/(60*1000),单位是“操作次数/毫秒”,表示每60秒10次。
/**
* 测试漏斗算法
* @throws Exception
*/
static void testFunnel() throws Exception{
//一分钟只允许连续发帖10次
FunnelRateLimiter funnelRateLimiter = new FunnelRateLimiter();
for(int i=1;i<=10000;i++){//模拟发请求
boolean actionAllowed = funnelRateLimiter.isActionAllowed("sixj", "发帖", 10, 10f/(60*1000),1);
Thread.sleep(1000);
System.out.println(i+":"+actionAllowed);
}
}
redis-cell
Redis4.0以后开始支持扩展模块,redis-cell模块实现限流功能,需要安装,提供了原子的限流指令,支持分布式。
> cl.throttle sixj:reply 10 10 60 1
可以与上面漏桶算法做个对比,
第一个参数 sixj:reply #key
第二个参数 10 #漏斗容量
第三、四个参数 10 60 #每60秒10次操作
第五个参数 1 #每次进入漏斗的量(可选参数,默认为1)
返回结果
1)(integer) 0 #0表示允许,1表示拒绝
2)(integer)10 #漏斗容量
3)(integer)9 #漏斗剩余空间
4)(integer)-1 #如果被拒绝了,需要多长时间再试(漏斗有空间了,单位秒)
5)(integer)6 #多长时间后,漏斗完全空出来
如果被拒绝了,就需要丢弃或重试,直接取出返回结果的第四个值进行sleep即可,如果不想阻塞线程,也可以异步定时任务来重试。
