限流实现方案探讨
限流算法在分布式领域是⼀个经常被提起的话题,当系统的处理能⼒ 有限时,如何阻⽌计划外的请求继续对系统施压,这是⼀个需要重视 的问题。
除了控制流量,限流还有⼀个应⽤⽬的是⽤于控制⽤户⾏为,避免垃圾请求。⽐如在 UGC 社区,⽤户的发帖、回复、点赞等⾏为都要严 格受控,⼀般要严格限定某⾏为在规定时间内允许的次数,超过了次 数那就是⾮法⾏为。对⾮法⾏为,业务必须规定适当的惩处策略。
定义接口
public interface BehaviorContollerInterface {
/**
*
* @param userId 用户id
* @param action 行为
* @param period 特定时间内,秒
* @param maxCount 发生次数
* @return
*/
boolean isActionAllowed(String userId,String action,int period,int maxCount);
}
1)基于redis的实现
package com.example.demo;
import org.springframework.data.redis.core.RedisTemplate;
import java.time.Duration;
public class NormalBehaviorController implements BehaviorContollerInterface{
RedisTemplate redisTemplate;
public NormalBehaviorController(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public boolean isActionAllowed(String userId, String action, int period, int maxCount) {
String key = String.format("hit:%s:%s",userId,action);
long nowTs = System.currentTimeMillis();
//记录行为
redisTemplate.opsForZSet().add(key,nowTs,nowTs);
//通过设置过期时间自动剔除掉时间窗口外的数据
redisTemplate.expire(key, Duration.ofMillis(period*1000));
//比较时间窗口内数据条数和设置的数据条数
return redisTemplate.opsForZSet().zCard(key)<=maxCount;
}
}
这种⽅案也有缺点,因 为它要记录时间窗⼝内所有的⾏为记录,如果这个量很⼤,⽐如限定 60s 内操作不得超过 100w 次这样的参数,它是不适合做这样的限流的,因为会消耗⼤量的存储空间。
2)漏斗算法实现
漏⽃的容量是有限的,如果将漏嘴堵住,然后⼀直往⾥⾯灌⽔,它就 会变满,直⾄再也装不进去。如果将漏嘴放开,⽔就会往下流,流⾛ ⼀部分之后,就⼜可以继续往⾥⾯灌⽔。如果漏嘴流⽔的速率⼤于灌 ⽔的速率,那么漏⽃永远都装不满。如果漏嘴流⽔速率⼩于灌⽔的速 率,那么⼀旦漏⽃满了,灌⽔就需要暂停并等待漏⽃腾空。
所以,漏⽃的剩余空间就代表着当前⾏为可以持续进⾏的数量,漏嘴 的流⽔速率代表着系统允许该⾏为的最⼤频率。
package com.example.demo;
import java.util.HashMap;
import java.util.Map;
public class FunnelBehaviorController implements BehaviorContollerInterface {
static Map<String,Funnel> funnelMap = new HashMap();
static class Funnel{
public Funnel(int capacity, float leakingRate, int leftQuota) {
this.capacity = capacity;
this.leakingRate = leakingRate;
this.leftQuota = leftQuota;
this.leakingTs = System.currentTimeMillis();
}
/**
* 灌水
* @param quota
* @return
*/
public boolean watering(int quota){
makingSpace();
if( leftQuota >= quota ){
leftQuota -= quota;
return true;
}
return false;
}
/**
* 计算漏水量并腾出空间
*/
private void makingSpace(){
long nowTs = System.currentTimeMillis();
long deltaTs = (nowTs - leakingTs)/1000;
int deltaQuota = (int) (deltaTs * leakingRate);
if( deltaQuota < 1 ){
return;
}
if( deltaQuota + leftQuota > capacity ){
leftQuota = capacity;
return;
}
leftQuota += deltaQuota;
leakingTs = System.currentTimeMillis();
}
/**
* 漏斗容量
*/
int capacity;
/**
* 漏水速率 单位:次/秒
*/
float leakingRate;
/**
* 漏斗剩余空间 单位:次
*/
int leftQuota;
/**
* 上一次漏水时间
*/
long leakingTs;
}
@Override
public boolean isActionAllowed(String userId, String action, int period, int maxCount) {
String key = String.format("hit:%s:%s",userId,action);
float leakingRate = maxCount/period;
Funnel funnel = null;
if(funnelMap.get(key)!=null){
funnel = funnelMap.get(key);
}else {
funnel = new Funnel(maxCount,leakingRate,maxCount);
funnelMap.put(key,funnel);
}
return funnel.watering(1);
}
}
但是有个问题,我们⽆法保证整个过程的原⼦性。
3)最终方案
Redis 4.0 提供了⼀个限流 Redis 模块,它叫 redis-cell。该模块使⽤了令牌桶算法,并提供了原⼦的限流指令。
CL.THROTTLE u1:apply 15 30 60 1
▲ ▲ ▲ ▲ ▲
| | | | └───── apply 1 token (default if omitted)
| | └──┴─────── 30 tokens / 60 seconds
| └───────────── 15 max_burst
└─────────────────── key "u1:apply"
还没有评论,来说两句吧
