断尾求生–简单限流
背景:定义一个简单接口,表示系统要限定用户的某个行为在指定的时间里只能允许发生N次。
# 指定用户 user_id 的某个行为 action_key 在特定的时间内 period 只允许发生最多的次数 max_count
def is_action_allowed(user_id,action_key,period,max_count):
return true
# 调用这个接口,一分钟内只允许最多回复5个帖子
can_reply = is_action_allowed("laoqian","reply",60,5)
if can_reply:
do_reply()
else:
raise ActionThresholdOverflow()
解决方案: 从上述代码可以看到限流需求中存在一个滑动时间窗口(定宽),因此考虑zset数据结构的score值,value值保持唯一性即可,用uuid会比较浪费空间,可以改用毫秒时间戳。
var (
RD = redis.Pool{
Dial: func() (conn redis.Conn, err error) {
conn, err = redis.Dial("tcp", "127.0.0.1")
return
}}
)
func main() {
isActionAllowed("laoqian", "reply", 60, 5)
}
/**
* @Description: 是否允许行为
*/
func isActionAllowed(userId, actionKey string, period, maxCount int64) bool {
key := fmt.Sprintf("hist:%s:%s", userId, actionKey)
nowTime := time.Now().Unix()
conn := RD.Get()
defer conn.Close()
_, _ = conn.Do("ZADD", key, nowTime, nowTime)
_ = conn.Send("ZCOUNT", "action", nowTime-1000*period, nowTime)
_ = conn.Flush()
result, _ := conn.Receive()
count := cast.ToInt64(result)
return count <= maxCount
}
使用pipeline可以显著提升Redis存取效率。但这种方案也有缺点,因为它要记录时间窗口内所有的行为记录,如果这个量很大,比如 "限定60s内操作不得超过100万次"之类,它是不适合做这样的限流的,因为会消耗大量的存储空间。
一毛不拔-漏斗限流
Golang版本单机漏斗算法如下:
var (
funnels = make(map[string]*Funnel)
)
/**
* @Description: 是否允许此行为
*/
func isActionAllowed(userId, actionKey string, capacity int, leakingRate float64) bool {
key := fmt.Sprintf("%s:%s", userId, actionKey)
if funnel, ok := funnels[key]; !ok {
funnel = NewFunnel(capacity, leakingRate)
funnels[key] = funnel
return funnel.watering(1)
} else {
return funnel.watering(1)
}
}
/**
* @Description: 漏斗对象
*/
type Funnel struct {
capacity int //漏斗容量
leakingRate float64 //漏嘴流水速率
leftQuota int //漏斗剩余空间
leakingTs int64 //上一次漏水时间
}
/**
* @Description: 创建一个漏斗对象
*/
func NewFunnel(capacity int, leakingRate float64) *Funnel {
return &Funnel{
capacity: capacity,
leakingRate: leakingRate,
leftQuota: capacity,
leakingTs: time.Now().Unix(),
}
}
/**
* @Description: 计算漏斗空间
*/
func (f *Funnel) makeSpace() {
nowTs := time.Now().Unix()
deltaTs := nowTs - f.leakingTs
deltaQuota := int(float64(deltaTs) * f.leakingRate)
//间隔时间太长,整数数字过大溢出
if deltaQuota < 0 {
f.leftQuota = f.capacity
f.leakingTs = nowTs
return
}
//腾出空间太小,最小单位是1
if deltaQuota < 1 {
return
}
f.leftQuota += deltaQuota
f.leakingTs = nowTs
if f.leftQuota > f.capacity {
f.leftQuota = f.capacity
}
}
/**
* @Description: 通过漏斗往漏斗里面进行灌水
*/
func (f *Funnel) watering(quota int) bool {
f.makeSpace()
if f.leftQuota >= quota {
f.leftQuota -= quota
return true
}
return false
}
redis4.0提供了一个限流Redis模块,它叫Redis-Cell。该模块也使用了漏斗算法,并提供了原子的限流指令。
> cl.throttle laoqian:reply 15 30 60 1
1) (integer) 0 # 0表示允许,1表示拒绝
2) (integer) 15 # 漏斗容量 capacity
3) (integer) 14 # 漏斗剩余空间 left_quota
4) (integer) -1 # 如果被拒绝了,需要多长时间后再试(漏斗有空间了,单位秒)
5) (integer) 2 # 多长时间后,漏斗完全空出来(left_quota==capacity,单位秒)
- 15 表示 capacity 漏斗容量
- 30 operations /60 seconds 这是漏水速率
cl.throttle 指令的重试时间是返回结果数组的第四个值进行sleep即可