漏斗限流
上次学习的使用zset存储时间戳,比对滑动(时间)窗口内的操作数是否超出规定的方式来限流对于规定时间内操作不多的情况还是比较好用的,可是如果时间窗口内允许的规定操作次数太多了,就会引起耗费大量的内存空间,这时就需要使用到漏斗限流的方式。
思想上大概就是,想象一个漏斗,如果容量(允许操作的次数之类)满了,为了不让它溢出必然只能暂停灌水(用户行为或者某种限流),漏斗下面也也有开口,当它流速大于灌水速度时,就不会暂停灌水,可是如果灌水速度大于了流速,那么就需要暂停一段时间灌水直到有可以容纳下一次灌水空间为止。
对比简单限流的好处是它不随着固定时间内允许操作数的增多而增大耗费的空间大小。它恒为一个常量。
用Java模拟的代码如下所示:
public class FunnelReteLimiter {
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);
//如果int值溢出了
if(deltaQuota<0){
//剩余容量置为容量
this.leftQuota = capacity;
//更新最近一次漏水时间
this.leakingRate = nowTs;
return;
}
//如果没有漏足至少一个单位的水
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;
}
return false;
}
}
private Map<String,Funnel> funnels = new HashMap<>();
//定制某个用户某种行为的漏斗
public boolean isActionAllowed(String userId,String actionKey,int capacity,float leakingRate){
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(1);
}
public static void main(String[] args) {
FunnelReteLimiter funnelReteLimiter = new FunnelReteLimiter();
for(int i=0;i<100;i++){
System.out.println(System.currentTimeMillis()+" 188号用户尝试fff"+(funnelReteLimiter.isActionAllowed("188","fff",5,0.1f)?"成功":"失败"));
}
}
}
不过需要注意,这是个单机版,在实际应用的场景下肯定不行呀,都是分布式的场景,如果在这里加锁的处理的话,就会变得性能低下或者影响用户体验,便可使用Redis的Redis-Cell模块(所以不要自己重复造轮子)。
命令 cl.throttle key capacity leakingRate [quota(可不设置,默认为1)]
详细可看这篇文章
GeoHash
地理位置模块,可以保存地理坐标信息,实现计算附近的人,附近的目标之类的功能。
GeoHash算法
将二维的经纬坐标映射到一维的整数上,然后进行它们之间的相对距离关系与映射后的关系一致,所以可以根据这个整数的远近来判断是否为附近的位置。
那么最关键的就是如何映射。
书中作者举例了一个切蛋糕的算法,如果一次一次将一块方形蛋糕切成4块那么就可以分为00,01,10,11.如果这太粗糙了,可以具体取一块后再分成四块同样的方式,直到达到所需的精度值为止。每层需要2bit来存储,整个序列的长度就是 层数x2 bit
。自然切得越多描述得越精准。
不过需要注意,这只是个举例,真实的算法与上述不是完全一样的,只是原理与之类似,不过“刀法”不一样,使用的表示方式也可能不一样。
GeoHash算法会对整数做一次base32变为一个字符串。在Redis当中经纬度使用52位的整数进行编码,放进zset中,因为zset可以排序和去重能够很好的满足功能需求。还可将zset中的score还原位原始坐标(存在一定的误差)。
指令的基本用法
使用 geoadd key longitude latitude member[longitude latitude member...]
可以像geo中添加经纬度以及它对应的标识。
因为geo的底层是一个zset所以它支持zset的删除命令,zrem key member[member...]
使用geodist key member1 member2 [unit]
获取两个位置间的距离,可指定单位m,km,ml(英里),ft(尺)
使用geopos key member [member...]
来获取一个或者多个member的经纬度坐标,这是存在一定误差的。
使用geohash key member[member]
来获取某个或者多个member的hash值
使用georadiusbymember key radius member m|km|ft|mi [WITHCOORD][WITHDIST][WITHHASH][COUNT count][ASC|DES]
获取指定位置的一定范围内的一定数量的其他位置。
还可以使用georadius key company longitude latitude radius m|km|ft|mi [WITHCOORD][WITHDIST][WITHHASH][COUNT count]
指定一个经纬度坐标寻找它附近的一定范围内存在的一定数量的位置。
注意
一般集群的环境下不建议一个key的大小超过1MB,否则集群迁移会出现卡顿现象。所以最好不要将Geo放在集群环境中,而是一个单独的Redis实例,如果数据量过大应该进行基于一定规则的拆分(比如地区等)