限流
简单限流
简单限流的思路是,在规定的时间窗口内,给出规定的最大操作数量限制。使用zset
结构作为一个用户行为的记录。zset
的value
和score
都用来表示操作的时间戳。每次操作前,先把操作时间戳加入zset
结构,然后移除超时的操作时间戳;之后比较总的个数和最大个数的关系,用来表示是否可以操作。
import time
import redis
client = redis.StrictRedis()
def is_action_allowed(usr_id, action_key, priod, max_count):
key = 'hist:%s:%s' % (user_id, action_key)
now_ts = int(time.time() * 1000) # 毫秒时间戳
with client.pipiline() as pipe:
pipe.zadd(key, now_ts, now_ts)
# 移除超时时间
pipe.zremrangebyscore(key, 0, now_ts - period * 1000)
pipe.zcard(key) # 获取当前时间段内总的操作次数
# 设置超时时间,移除冷操作节约空间
pipe.expore(key, period + 1)
_, _, current_count, _ = pipe.execute()
return current_count <= max_count
这种做法的缺陷是,如果规定时间段内允许的操作数非常多,假设1s内可以操作 1 0 5 10^5 105次操作,那么需要对应数量的时间戳来存储,浪费空间。
漏斗限流
介绍漏斗限流的思路。漏斗的滴水速度是匀速的,我们可以往漏斗中加水,如果加水的速度小于漏水的速度,那么这个行为是可以允许的;但是如果超过了这个速度,那么是不允许的。
给出单机漏斗限流的基本实现:
import time
class Funnel(object):
def __init__(self, capacity, leaking_rate):
self.capacity = capacity # 漏斗的容量
self.leaking_rate = leaking_rate # 漏水的速度
self.left_quota = capacity # 初始化的水量
self.leaking_ts = time.time() # 上次漏水的时间
# 加水顺便检验空间,算法的核心
def make_space(self):
now_ts = time.time()
delta_ts = now_ts - self.leaking_ts # 距离上次加水时间的间隔
delta_quota = delta_ts * self.leaking_rate # 减少的水量,可以认为是腾出的空间
if delta_quota < 1: # 腾出的空间太少
return
self.left_quota += delta_quota # 增加剩余的空间
self.leaking_ts = now_ts # 更新漏水的时间戳
if self.left_quota > self.capacity: # 不能超过容量
self.left_quota = self.capacity
def watering(self, quota): # 判断加入的水是否满足
self.make_space()
if self.left_quota >= quota: # 判断剩余的空间是否充足
self.left_quota -= quota
return True
return False
funnels = {}
def is_action_allowed(user_id, action_key, capacity, leaking_rate):
key = '%s:%s' % (user_id, action_key)
funnel = funnels.get(key)
if not funnel:
funnel = Funnel(capacity, leaking_rate)
funnels[key] = funnel
return funnel.watering(1)
for i in range(20):
print is_action_allowed('erick', 'reply', 15, 0.5)
漏斗限流的缺陷:该方式不适用于分布式的系统。因为从funnels
这个hash
中取出结构,然后把数据放到内存中计算,最后再放回数据的过程不是原子的。这意味着我们需要加锁操作。如果加锁失败,需要重试或者放弃,复杂度高。
Redis-Cell实现漏斗限流
redis
的漏斗限流算法,而且提供了原子限流指令。只有一条指令:
cl.throttle erick:reply 15 30 60 1
意义分别是:
erick:reply
:键值15
:漏斗的容量30
:规定时间内最大的操作个数60
:规定的时间,这里是30个/60s1
:默认值是1,表示当前添加的quota
该指令返回5个值,分别是:
0
:0 表示允许,1表示拒绝15
:漏斗容量14
:剩余空间-1
:被拒绝了,需要多久之后再重试2
:多长时间后,漏斗完全空出来
GeoHash算法
该算法用于计算距离,算法的核心思想是把二维空间的距离映射到一维上,然后此时再指定元素时,就可以在一维的距离上进行比较了,减少了复杂度。注意,这里的映射是有损映射,但是损失的精度较小,对于附近的人等的应用,这些误差可以忽略。
总共有6个基本的操作,如下:
geoadd person 111 112 foo
:foo
在person
集合中的经纬度是111 & 112geodist person foo1 foo2 km
:返回foo1
和foo2
的距离geohash person foo
:获取foo
的哈希值georadiusbynumber person foo 20 km count 3 asc
:返回foo
20km内按照升序的3个人,desc
表示降序georadius 111 112 20 km withdist count 3 asc
:把名称换成了经纬度
注意,GeoHash
的底层数据结构是zset
,集群环境中,如果单个key
的数据量过多,会对数据迁移造成卡顿。同时,key
集合的大小一般也不要超过1MB。所以实际部署GeoHash
时,最好单独部署一套,不要和集群混合在一起。
Scan
keys regex
是搜索出所有满足regex
正则表达式的key
值,但是该方式一般在key
很少的时候使用,该方式复杂度是
O
(
N
)
O(N)
O(N),会阻塞redis。
一般对于线上服务来说,更多的是使用scan
命令,该命令的特点
- 复杂度
O(N)
,通过游标分步,不会阻塞线程。 - 提供
limit
参数,控制返回结果的最大数量 - 提供模式匹配
- 返回结果可能有重复,需要客户端去重
- 遍历过程中,如果数据有修改,则结果是不确定的
- 单次返回结果为空,不意味着遍历结束,需要看游标是否是0
比如:
scan 0 match key99* count 1000
redis采用渐进式的rehash技术,每次只是同步部分hash数据;同时,redis的遍历方式是采用比特位的高位加法。具体可以参考下面的博客。
对于hash
和zset
等结构,也有对应的hscan
和zscan
等的方式。
参考博客:
- http://tech-happen.site/32ad6396.html
大key扫描
redis中,如果有很大的hash
或者set
等的结构,那么可能会造成卡顿,可能的原因如下:
- 如果
key
的结构空间不够,则需要重新分配空间,并拷贝数据,耗费时间 - 集群之间迁移数据,速度变慢、
如果redis的内存消耗出现较大波动,或者耗时出现较大波动,说明可能出现大key,此时需要借助scan
的方式扫描并判断:
redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.1